ibm_db 0.4.6 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -260,13 +260,11 @@ module ActiveRecord
260
260
  when /DB2/i
261
261
  case server_info.DBMS_VER
262
262
  when /09/
263
- @servertype = IBM_DB2_ZOS.new(@connection, self)
263
+ @servertype = IBM_DB2_ZOS.new(@connection, self)
264
264
  when /08/
265
265
  @servertype = IBM_DB2_ZOS_8.new(@connection, self)
266
- when /07/
267
- @servertype = IBM_DB2_ZOS_7.new(@connection, self)
268
266
  else
269
- raise "Only DB2 z/OS version 7 and above are currently supported"
267
+ raise "Only DB2 z/OS version 8 and above are currently supported"
270
268
  end
271
269
  # DB2 on i5
272
270
  when /AS/i
@@ -290,7 +288,7 @@ module ActiveRecord
290
288
  unless name == @app_user
291
289
  option = {IBM_DB::SQL_ATTR_INFO_USERID => "#{name}"}
292
290
  if IBM_DB::set_option( @connection, option, 1 )
293
- @app_user = IBM_DB::get_option( @connection, IBM_DB::SQL_ATTR_INFO_USERID )
291
+ @app_user = IBM_DB::get_option( @connection, IBM_DB::SQL_ATTR_INFO_USERID, 1 )
294
292
  end
295
293
  end
296
294
  end
@@ -300,7 +298,7 @@ module ActiveRecord
300
298
  unless name == @account
301
299
  option = {IBM_DB::SQL_ATTR_INFO_ACCTSTR => "#{name}"}
302
300
  if IBM_DB::set_option( @connection, option, 1 )
303
- @account = IBM_DB::get_option( @connection, IBM_DB::SQL_ATTR_INFO_ACCTSTR )
301
+ @account = IBM_DB::get_option( @connection, IBM_DB::SQL_ATTR_INFO_ACCTSTR, 1 )
304
302
  end
305
303
  end
306
304
  end
@@ -310,7 +308,7 @@ module ActiveRecord
310
308
  unless name == @application
311
309
  option = {IBM_DB::SQL_ATTR_INFO_APPLNAME => "#{name}"}
312
310
  if IBM_DB::set_option( @connection, option, 1 )
313
- @application = IBM_DB::get_option( @connection, IBM_DB::SQL_ATTR_INFO_APPLNAME )
311
+ @application = IBM_DB::get_option( @connection, IBM_DB::SQL_ATTR_INFO_APPLNAME, 1 )
314
312
  end
315
313
  end
316
314
  end
@@ -320,14 +318,16 @@ module ActiveRecord
320
318
  unless name == @workstation
321
319
  option = {IBM_DB::SQL_ATTR_INFO_WRKSTNNAME => "#{name}"}
322
320
  if IBM_DB::set_option( @connection, option, 1 )
323
- @workstation = IBM_DB::get_option( @connection, IBM_DB::SQL_ATTR_INFO_WRKSTNNAME )
321
+ @workstation = IBM_DB::get_option( @connection, IBM_DB::SQL_ATTR_INFO_WRKSTNNAME, 1 )
324
322
  end
325
323
  end
326
324
  end
327
325
 
328
326
  # This adapter supports migrations.
329
- # Current limitations: +remove_column+ is not currently supported
330
- # while DB2 for zOS does not support +rename_column+
327
+ # Current limitations:
328
+ # +rename_column+ is not currently supported by the IBM data servers
329
+ # +remove_column+ is not currently supported by the DB2 for zOS data server
330
+ # Tables containing columns of XML data type do not support +remove_column+
331
331
  def supports_migrations?
332
332
  true
333
333
  end
@@ -404,13 +404,7 @@ module ActiveRecord
404
404
  # and get a IBM_DB::Statement from which is possible to fetch the results
405
405
  if stmt = execute(sql, name)
406
406
  begin
407
- # Fetches all the results available. IBM_DB::fetch_assoc(stmt) returns
408
- # an hash for each single record.
409
- # The loop stops when there aren't any more valid records to fetch
410
- while single_hash = IBM_DB::fetch_assoc(stmt)
411
- # Add the record to the +results+ array
412
- results << single_hash
413
- end
407
+ @servertype.select_all(sql, name, stmt, results)
414
408
  ensure
415
409
  # Ensures to free the resources associated with the statement
416
410
  IBM_DB::free_result stmt
@@ -450,20 +444,7 @@ module ActiveRecord
450
444
  # Logs and execute the sql instructions.
451
445
  # The +log+ method is defined in the parent class +AbstractAdapter+
452
446
  log(sql, name) do
453
- begin
454
- if stmt = IBM_DB::exec(@connection, sql)
455
- stmt # Return the statement object
456
- else
457
- raise IBM_DB::stmt_errormsg
458
- end
459
- rescue StandardError
460
- error_msg = IBM_DB::conn_errormsg ? IBM_DB::conn_errormsg : IBM_DB::stmt_errormsg
461
- if error_msg && !error_msg.empty?
462
- raise "Failed to execute statement due to error: #{error_msg}"
463
- else
464
- raise
465
- end
466
- end
447
+ @servertype.execute(sql, name)
467
448
  end
468
449
  end
469
450
 
@@ -555,16 +536,7 @@ module ActiveRecord
555
536
  # Private method used by +add_limit_offset!+ to create a
556
537
  # sql query given an offset and a limit
557
538
  def query_offset_limit(sql, offset, limit)
558
- # Defines what will be the last record
559
- last_record = offset + limit
560
- # Transforms the SELECT query in order to retrieve/fetch only
561
- # a number of records after the specified offset.
562
- # 'select' or 'SELECT' is replaced with the partial query below that adds the sys_row_num column
563
- # to select with the condition of this column being between offset+1 and the offset+limit
564
- sql.gsub!(/SELECT/i,"SELECT O.* FROM (SELECT I.*, ROW_NUMBER() OVER () sys_row_num FROM (SELECT")
565
- # The final part of the query is appended to include a WHERE...BETWEEN...AND condition,
566
- # and retrieve only a LIMIT number of records starting from the OFFSET+1
567
- sql << ") AS I) AS O WHERE sys_row_num BETWEEN #{offset+1} AND #{last_record}"
539
+ @servertype.query_offset_limit(sql, offset, limit)
568
540
  end
569
541
  private :query_offset_limit
570
542
 
@@ -600,7 +572,7 @@ module ActiveRecord
600
572
  "BLOB('?')"
601
573
  else
602
574
  # Quoting required for the default value of a column
603
- "BLOB('#{value}')"
575
+ @servertype.set_blob_default(value)
604
576
  end
605
577
  elsif column && column.type == :text
606
578
  unless caller[0] =~ /add_column_options/i
@@ -678,13 +650,14 @@ module ActiveRecord
678
650
  }
679
651
  end
680
652
 
681
- # DB2 does not support limits on the integer and double data types
682
- # unlike MySQL. It does support limits on float and decimal/numeric
683
- def type_to_sql(type, limit = nil, precision = nil, scale = nil)
684
- if type == :integer && (!limit.nil? && limit > 0)
685
- return 'integer'
686
- elsif type == :double && (!limit.nil? && limit > 0)
687
- return 'double'
653
+ # IBM data servers do not support limits on certain data types (unlike MySQL)
654
+ # Limit is supported for the {float, decimal, numeric, varchar, clob, blob} data types.
655
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
656
+ return super if limit.nil?
657
+
658
+ # strip off limits on data types not supporting them
659
+ if [:integer, :double, :date, :time, :timestamp, :xml].include? type
660
+ return type.to_s
688
661
  else
689
662
  return super
690
663
  end
@@ -938,37 +911,54 @@ module ActiveRecord
938
911
  @caller = caller
939
912
  end
940
913
 
941
- # Implemented by concrete DataServer if applicable
942
914
  def last_generated_id
943
915
  end
944
916
 
945
- # Implemented by concrete DataServer if applicable
946
917
  def create_index_after_table (table_name, caller)
947
918
  end
948
919
 
949
- # Implemented by concrete DataServer if applicable
950
920
  def setup_for_lob_table ()
951
921
  end
952
922
 
953
- # Implemented by concrete DataServer if applicable
954
923
  def reorg_table(table_name)
955
924
  end
956
925
 
957
- # Implemented by concrete DataServer if applicable
958
926
  def check_reserved_words(col_name)
959
927
  col_name
960
928
  end
961
929
 
962
- # This is supported by LUW and i5
930
+ # This is supported by the DB2 for Linux, UNIX, Windows data servers
931
+ # and by the DB2 for i5 data servers
963
932
  def remove_column(table_name, column_name)
964
- @caller.execute "ALTER TABLE #{table_name} DROP #{column_name}"
965
- reorg_table(table_name)
933
+ begin
934
+ @caller.execute "ALTER TABLE #{table_name} DROP #{column_name}"
935
+ reorg_table(table_name)
936
+ rescue StandardError => exec_err
937
+ # Provide details on the current XML columns support
938
+ if exec_err.message.include?('SQLCODE=-1242') && exec_err.message.include?('42997')
939
+ raise StatementInvalid, "A column that is part of a table containing an XML column cannot be dropped. To remove the column, the table must be dropped and recreated without the #{column_name} column: #{exec_err}"
940
+ else
941
+ raise
942
+ end
943
+ end
966
944
  end
967
945
 
968
946
  def set_schema(schema)
969
947
  @caller.execute("SET SCHEMA #{schema}")
970
948
  end
971
949
 
950
+ def select_all(sql, name, stmt, results)
951
+ end
952
+
953
+ def execute(sql, name)
954
+ end
955
+
956
+ def query_offset_limit(sql, offset, limit)
957
+ end
958
+
959
+ def set_blob_default(value)
960
+ "BLOB('#{value}')"
961
+ end
972
962
  end # class IBM_DataServer
973
963
 
974
964
  class IBM_DB2 < IBM_DataServer
@@ -1009,6 +999,105 @@ module ActiveRecord
1009
999
  @caller.change_column_default(table_name, column_name, options[:default])
1010
1000
  end
1011
1001
  end
1002
+
1003
+ # Fetches all the results available. IBM_DB::fetch_assoc(stmt) returns
1004
+ # an hash for each single record.
1005
+ # The loop stops when there aren't any more valid records to fetch
1006
+ def select_all(sql, name, stmt, results)
1007
+ if (!@offset.nil? && @offset >= 0) || (!@limit.nil? && @limit > 0)
1008
+ # We know at this point that there is an offset and/or a limit
1009
+ # Check if the cursor type is set correctly
1010
+ cursor_type = IBM_DB::get_option stmt, IBM_DB::SQL_ATTR_CURSOR_TYPE, 0
1011
+ if (cursor_type == IBM_DB::SQL_CURSOR_STATIC)
1012
+ index = 0
1013
+ # Get @limit rows starting at @offset
1014
+ while (index < @limit)
1015
+ # We increment the offset by 1 because for DB2 the offset of the initial row is 1 instead of 0
1016
+ if single_hash = IBM_DB::fetch_assoc(stmt, @offset + index + 1)
1017
+ # Add the record to the +results+ array
1018
+ results << single_hash
1019
+ index = index + 1
1020
+ else
1021
+ # break from the while loop
1022
+ break
1023
+ end
1024
+ end
1025
+ else # cursor != IBM_DB::SQL_CURSOR_STATIC
1026
+ # If the result set contains a LOB, the cursor type will never be SQL_CURSOR_STATIC
1027
+ # because DB2 does not allow this. We can't use the offset mechanism because the cursor
1028
+ # is not scrollable. In this case, ignore first @offset rows and return rows starting
1029
+ # at @offset to @offset + @limit
1030
+ index = 0
1031
+ while (index < @offset + @limit)
1032
+ if single_hash = IBM_DB::fetch_assoc(stmt)
1033
+ # Add the record to the +results+ array only from row @offset to @offset + @limit
1034
+ if (index >= @offset)
1035
+ results << single_hash
1036
+ end
1037
+ index = index + 1
1038
+ else
1039
+ # break from the while loop
1040
+ break
1041
+ end
1042
+ end
1043
+ end
1044
+ # This is the case where limit is set to zero
1045
+ # Simply return an empty +results+
1046
+ elsif (!@limit.nil? && @limit == 0)
1047
+ results
1048
+ # No limits or offsets specified
1049
+ else
1050
+ while single_hash = IBM_DB::fetch_assoc(stmt)
1051
+ # Add the record to the +results+ array
1052
+ results << single_hash
1053
+ end
1054
+ end
1055
+ # Assign the instance variables to nil. We will not be using them again
1056
+ @offset = nil
1057
+ @limit = nil
1058
+ end
1059
+
1060
+ def execute(sql, name)
1061
+ # Check if there is a limit and/or an offset
1062
+ # If so then make sure and use a static cursor type
1063
+ if (!@offset.nil? && @offset >= 0) || (!@limit.nil? && @limit > 0)
1064
+ begin
1065
+ # Set the cursor type to static so we can later utilize the offset and limit correctly
1066
+ if stmt = IBM_DB::exec(@connection, sql, {IBM_DB::SQL_ATTR_CURSOR_TYPE => IBM_DB::SQL_CURSOR_STATIC})
1067
+ stmt # Return the statement object
1068
+ else
1069
+ raise StatementInvalid, IBM_DB::stmt_errormsg
1070
+ end
1071
+ rescue StandardError
1072
+ error_msg = IBM_DB::conn_errormsg ? IBM_DB::conn_errormsg : IBM_DB::stmt_errormsg
1073
+ if error_msg && !error_msg.empty?
1074
+ raise "Failed to execute statement due to error: #{error_msg}"
1075
+ else
1076
+ raise
1077
+ end
1078
+ end
1079
+ else
1080
+ begin
1081
+ if stmt = IBM_DB::exec(@connection, sql)
1082
+ stmt # Return the statement object
1083
+ else
1084
+ raise StatementInvalid, IBM_DB::stmt_errormsg
1085
+ end
1086
+ rescue StandardError
1087
+ error_msg = IBM_DB::conn_errormsg ? IBM_DB::conn_errormsg : IBM_DB::stmt_errormsg
1088
+ if error_msg && !error_msg.empty?
1089
+ raise "Failed to execute statement due to error: #{error_msg}"
1090
+ else
1091
+ raise
1092
+ end
1093
+ end
1094
+ end
1095
+ end
1096
+
1097
+ def query_offset_limit(sql, offset, limit)
1098
+ @limit = limit
1099
+ @offset = offset
1100
+ end
1012
1101
  end # class IBM_DB2
1013
1102
 
1014
1103
  class IBM_DB2_LUW < IBM_DB2
@@ -1016,6 +1105,46 @@ module ActiveRecord
1016
1105
  def reorg_table(table_name)
1017
1106
  @caller.execute("CALL ADMIN_CMD('REORG TABLE #{table_name}')")
1018
1107
  end
1108
+
1109
+ def select_all(sql, name, stmt, results)
1110
+ # Fetches all the results available. IBM_DB::fetch_assoc(stmt) returns
1111
+ # an hash for each single record.
1112
+ # The loop stops when there aren't any more valid records to fetch
1113
+ while single_hash = IBM_DB::fetch_assoc(stmt)
1114
+ # Add the record to the +results+ array
1115
+ results << single_hash
1116
+ end
1117
+ end
1118
+
1119
+ def execute(sql, name)
1120
+ begin
1121
+ if stmt = IBM_DB::exec(@connection, sql)
1122
+ stmt # Return the statement object
1123
+ else
1124
+ raise StatementInvalid, IBM_DB::stmt_errormsg
1125
+ end
1126
+ rescue StandardError
1127
+ error_msg = IBM_DB::conn_errormsg ? IBM_DB::conn_errormsg : IBM_DB::stmt_errormsg
1128
+ if error_msg && !error_msg.empty?
1129
+ raise "Failed to execute statement due to error: #{error_msg}"
1130
+ else
1131
+ raise
1132
+ end
1133
+ end
1134
+ end
1135
+
1136
+ def query_offset_limit(sql, offset, limit)
1137
+ # Defines what will be the last record
1138
+ last_record = offset + limit
1139
+ # Transforms the SELECT query in order to retrieve/fetch only
1140
+ # a number of records after the specified offset.
1141
+ # 'select' or 'SELECT' is replaced with the partial query below that adds the sys_row_num column
1142
+ # to select with the condition of this column being between offset+1 and the offset+limit
1143
+ sql.gsub!(/SELECT/i,"SELECT O.* FROM (SELECT I.*, ROW_NUMBER() OVER () sys_row_num FROM (SELECT")
1144
+ # The final part of the query is appended to include a WHERE...BETWEEN...AND condition,
1145
+ # and retrieve only a LIMIT number of records starting from the OFFSET+1
1146
+ sql << ") AS I) AS O WHERE sys_row_num BETWEEN #{offset+1} AND #{last_record}"
1147
+ end
1019
1148
  end # class IBM_DB2_LUW
1020
1149
 
1021
1150
  module HostedDataServer
@@ -1042,37 +1171,39 @@ module ActiveRecord
1042
1171
  caller.add_index(table_name, "id", :unique => true)
1043
1172
  end
1044
1173
 
1045
- # This call is needed on DB2 z/OS v8 and earlier for the creation of tables
1046
- # with LOBs. When issued, this call does the following:
1047
- # DB2 creates LOB table spaces, auxiliary tables, and indexes on auxiliary
1048
- # tables for LOB columns.
1049
- def setup_for_lob_table()
1050
- @caller.execute "SET CURRENT RULES = 'STD'"
1051
- end
1052
-
1053
1174
  def remove_column(table_name, column_name)
1054
- raise NotImplementedError, "remove_column is not supported for DB2/zOS server"
1055
- end
1175
+ raise NotImplementedError, "remove_column is not supported by the DB2 for zOS data server"
1176
+ end
1056
1177
 
1057
- # Setting the SQLID on z/OS will also update the CURRENT SCHEMA
1058
- # special register, but not vice versa
1059
- def set_schema(schema)
1060
- @caller.execute("SET CURRENT SQLID ='#{schema}'")
1178
+ # DB2 z/OS only allows a "null" or an empty binary string
1179
+ # as a default for a BLOB column
1180
+ # if value is not empty or equivalent to the string "null",
1181
+ # the server will complain
1182
+ def set_blob_default(value)
1183
+ "#{value}"
1061
1184
  end
1062
-
1063
1185
  end # class IBM_DB2_ZOS
1064
1186
 
1065
1187
  class IBM_DB2_ZOS_8 < IBM_DB2_ZOS
1066
1188
  include HostedDataServer
1189
+ # Setting the SQLID on z/OS will also update the CURRENT SCHEMA
1190
+ # special register, but not vice versa
1191
+ def set_schema(schema)
1192
+ @caller.execute("SET CURRENT SQLID ='#{schema.upcase}'")
1193
+ end
1194
+
1195
+ # This call is needed on DB2 z/OS v8 for the creation of tables
1196
+ # with LOBs. When issued, this call does the following:
1197
+ # DB2 creates LOB table spaces, auxiliary tables, and indexes on auxiliary
1198
+ # tables for LOB columns.
1199
+ def setup_for_lob_table()
1200
+ @caller.execute "SET CURRENT RULES = 'STD'"
1201
+ end
1067
1202
  end # class IBM_DB2_ZOS_8
1068
1203
 
1069
- class IBM_DB2_ZOS_7 < IBM_DB2_ZOS
1070
- include HostedDataServer
1071
- end # class IBM_DB2_ZOS_7
1072
-
1073
1204
  class IBM_DB2_I5 < IBM_DB2
1074
1205
  include HostedDataServer
1075
1206
  end # class IBM_DB2_I5
1076
1207
 
1077
1208
  end # module ConnectionAdapters
1078
- end # module ActiveRecord
1209
+ end # module ActiveRecord
Binary file
@@ -0,0 +1,445 @@
1
+ require 'abstract_unit'
2
+ require 'fixtures/post'
3
+ require 'fixtures/comment'
4
+ require 'fixtures/author'
5
+ require 'fixtures/category'
6
+ require 'fixtures/company'
7
+ require 'fixtures/person'
8
+ require 'fixtures/reader'
9
+
10
+ class EagerAssociationTest < Test::Unit::TestCase
11
+ fixtures :posts, :comments, :authors, :categories, :categories_posts,
12
+ :companies, :accounts, :tags, :people, :readers
13
+
14
+ def test_loading_with_one_association
15
+ posts = Post.find(:all, :include => :comments)
16
+ post = posts.find { |p| p.id == 1 }
17
+ assert_equal 2, post.comments.size
18
+ assert post.comments.include?(comments(:greetings))
19
+
20
+ post = Post.find(:first, :include => :comments, :conditions => "posts.title = 'Welcome to the weblog'")
21
+ assert_equal 2, post.comments.size
22
+ assert post.comments.include?(comments(:greetings))
23
+ end
24
+
25
+ def test_loading_conditions_with_or
26
+ posts = authors(:david).posts.find(:all, :include => :comments, :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE} = 'SpecialComment'")
27
+ assert_nil posts.detect { |p| p.author_id != authors(:david).id },
28
+ "expected to find only david's posts"
29
+ end
30
+
31
+ def test_with_ordering
32
+ list = Post.find(:all, :include => :comments, :order => "posts.id DESC")
33
+ [:eager_other, :sti_habtm, :sti_post_and_comments, :sti_comments,
34
+ :authorless, :thinking, :welcome
35
+ ].each_with_index do |post, index|
36
+ assert_equal posts(post), list[index]
37
+ end
38
+ end
39
+
40
+ def test_loading_with_multiple_associations
41
+ posts = Post.find(:all, :include => [ :comments, :author, :categories ], :order => "posts.id")
42
+ assert_equal 2, posts.first.comments.size
43
+ assert_equal 2, posts.first.categories.size
44
+ assert posts.first.comments.include?(comments(:greetings))
45
+ end
46
+
47
+ def test_loading_from_an_association
48
+ posts = authors(:david).posts.find(:all, :include => :comments, :order => "posts.id")
49
+ assert_equal 2, posts.first.comments.size
50
+ end
51
+
52
+ def test_loading_with_no_associations
53
+ assert_nil Post.find(posts(:authorless).id, :include => :author).author
54
+ end
55
+
56
+ def test_eager_association_loading_with_belongs_to
57
+ comments = Comment.find(:all, :include => :post)
58
+ assert_equal 10, comments.length
59
+ titles = comments.map { |c| c.post.title }
60
+ assert titles.include?(posts(:welcome).title)
61
+ assert titles.include?(posts(:sti_post_and_comments).title)
62
+ end
63
+
64
+ def test_eager_association_loading_with_belongs_to_and_limit
65
+ comments = Comment.find(:all, :include => :post, :limit => 5, :order => 'comments.id')
66
+ assert_equal 5, comments.length
67
+ assert_equal [1,2,3,5,6], comments.collect { |c| c.id }
68
+ end
69
+
70
+ def test_eager_association_loading_with_belongs_to_and_limit_and_conditions
71
+ comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :order => 'comments.id')
72
+ assert_equal 3, comments.length
73
+ assert_equal [5,6,7], comments.collect { |c| c.id }
74
+ end
75
+
76
+ def test_eager_association_loading_with_belongs_to_and_limit_and_offset
77
+ comments = Comment.find(:all, :include => :post, :limit => 3, :offset => 2, :order => 'comments.id')
78
+ assert_equal 3, comments.length
79
+ assert_equal [3,5,6], comments.collect { |c| c.id }
80
+ end
81
+
82
+ def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions
83
+ comments = Comment.find(:all, :include => :post, :conditions => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id')
84
+ assert_equal 3, comments.length
85
+ assert_equal [6,7,8], comments.collect { |c| c.id }
86
+ end
87
+
88
+ def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array
89
+ comments = Comment.find(:all, :include => :post, :conditions => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id')
90
+ assert_equal 3, comments.length
91
+ assert_equal [6,7,8], comments.collect { |c| c.id }
92
+ end
93
+
94
+ def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations
95
+ posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :order => 'posts.id')
96
+ assert_equal 1, posts.length
97
+ assert_equal [1], posts.collect { |p| p.id }
98
+ end
99
+
100
+ def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations
101
+ posts = Post.find(:all, :include => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id')
102
+ assert_equal 1, posts.length
103
+ assert_equal [2], posts.collect { |p| p.id }
104
+ end
105
+
106
+ def test_eager_with_has_many_through
107
+ posts_with_comments = people(:michael).posts.find(:all, :include => :comments)
108
+ posts_with_author = people(:michael).posts.find(:all, :include => :author )
109
+ posts_with_comments_and_author = people(:michael).posts.find(:all, :include => [ :comments, :author ])
110
+ assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size }
111
+ assert_equal authors(:david), assert_no_queries { posts_with_author.first.author }
112
+ assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author }
113
+ end
114
+
115
+ def test_eager_with_has_many_through_an_sti_join_model
116
+ author = Author.find(:first, :include => :special_post_comments, :order => 'authors.id')
117
+ assert_equal [comments(:does_it_hurt)], assert_no_queries { author.special_post_comments }
118
+ end
119
+
120
+ def test_eager_with_has_many_through_an_sti_join_model_with_conditions_on_both
121
+ author = Author.find(:first, :include => :special_nonexistant_post_comments, :order => 'authors.id')
122
+ assert_equal [], author.special_nonexistant_post_comments
123
+ end
124
+
125
+ def test_eager_with_has_many_through_join_model_with_conditions
126
+ assert_equal Author.find(:first, :include => :hello_post_comments,
127
+ :order => 'authors.id').hello_post_comments.sort_by(&:id),
128
+ Author.find(:first, :order => 'authors.id').hello_post_comments.sort_by(&:id)
129
+ end
130
+
131
+ def test_eager_with_has_many_and_limit
132
+ posts = Post.find(:all, :order => 'posts.id asc', :include => [ :author, :comments ], :limit => 2)
133
+ assert_equal 2, posts.size
134
+ assert_equal 3, posts.inject(0) { |sum, post| sum += post.comments.size }
135
+ end
136
+
137
+ def test_eager_with_has_many_and_limit_and_conditions
138
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.body = 'hello'", :order => "posts.id")
139
+ assert_equal 2, posts.size
140
+ assert_equal [4,5], posts.collect { |p| p.id }
141
+ end
142
+
143
+ def test_eager_with_has_many_and_limit_and_conditions_array
144
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "posts.body = ?", 'hello' ], :order => "posts.id")
145
+ assert_equal 2, posts.size
146
+ assert_equal [4,5], posts.collect { |p| p.id }
147
+ end
148
+
149
+ def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers
150
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ])
151
+ assert_equal 2, posts.size
152
+
153
+ count = Post.count(:include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ])
154
+ assert_equal count, posts.size
155
+ end
156
+
157
+ def test_eager_with_has_many_and_limit_ond_high_offset
158
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ])
159
+ assert_equal 0, posts.size
160
+ end
161
+
162
+ def test_count_eager_with_has_many_and_limit_ond_high_offset
163
+ posts = Post.count(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10, :conditions => [ "authors.name = ?", 'David' ])
164
+ assert_equal 0, posts
165
+ end
166
+
167
+ def test_eager_with_has_many_and_limit_with_no_results
168
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :conditions => "posts.title = 'magic forest'")
169
+ assert_equal 0, posts.size
170
+ end
171
+
172
+ def test_eager_with_has_and_belongs_to_many_and_limit
173
+ posts = Post.find(:all, :include => :categories, :order => "posts.id", :limit => 3)
174
+ assert_equal 3, posts.size
175
+ assert_equal 2, posts[0].categories.size
176
+ assert_equal 1, posts[1].categories.size
177
+ assert_equal 0, posts[2].categories.size
178
+ assert posts[0].categories.include?(categories(:technology))
179
+ assert posts[1].categories.include?(categories(:general))
180
+ end
181
+
182
+ def test_eager_with_has_many_and_limit_and_conditions_on_the_eagers
183
+ posts = authors(:david).posts.find(:all,
184
+ :include => :comments,
185
+ :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
186
+ :limit => 2
187
+ )
188
+ assert_equal 2, posts.size
189
+
190
+ count = Post.count(
191
+ :include => [ :comments, :author ],
192
+ :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')",
193
+ :limit => 2
194
+ )
195
+ assert_equal count, posts.size
196
+ end
197
+
198
+ def test_eager_with_has_many_and_limit_and_scoped_conditions_on_the_eagers
199
+ posts = nil
200
+ Post.with_scope(:find => {
201
+ :include => :comments,
202
+ :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'"
203
+ }) do
204
+ posts = authors(:david).posts.find(:all, :limit => 2)
205
+ assert_equal 2, posts.size
206
+ end
207
+
208
+ Post.with_scope(:find => {
209
+ :include => [ :comments, :author ],
210
+ :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')"
211
+ }) do
212
+ count = Post.count(:limit => 2)
213
+ assert_equal count, posts.size
214
+ end
215
+ end
216
+
217
+ def test_eager_with_has_many_and_limit_and_scoped_and_explicit_conditions_on_the_eagers
218
+ Post.with_scope(:find => { :conditions => "1=1" }) do
219
+ posts = authors(:david).posts.find(:all,
220
+ :include => :comments,
221
+ :conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
222
+ :limit => 2
223
+ )
224
+ assert_equal 2, posts.size
225
+
226
+ count = Post.count(
227
+ :include => [ :comments, :author ],
228
+ :conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')",
229
+ :limit => 2
230
+ )
231
+ assert_equal count, posts.size
232
+ end
233
+ end
234
+ def test_eager_association_loading_with_habtm
235
+ posts = Post.find(:all, :include => :categories, :order => "posts.id")
236
+ assert_equal 2, posts[0].categories.size
237
+ assert_equal 1, posts[1].categories.size
238
+ assert_equal 0, posts[2].categories.size
239
+ assert posts[0].categories.include?(categories(:technology))
240
+ assert posts[1].categories.include?(categories(:general))
241
+ end
242
+
243
+ def test_eager_with_inheritance
244
+ posts = SpecialPost.find(:all, :include => [ :comments ])
245
+ end
246
+
247
+ def test_eager_has_one_with_association_inheritance
248
+ post = Post.find(4, :include => [ :very_special_comment ])
249
+ assert_equal "VerySpecialComment", post.very_special_comment.class.to_s
250
+ end
251
+
252
+ def test_eager_has_many_with_association_inheritance
253
+ post = Post.find(4, :include => [ :special_comments ])
254
+ post.special_comments.each do |special_comment|
255
+ assert_equal "SpecialComment", special_comment.class.to_s
256
+ end
257
+ end
258
+
259
+ def test_eager_habtm_with_association_inheritance
260
+ post = Post.find(6, :include => [ :special_categories ])
261
+ assert_equal 1, post.special_categories.size
262
+ post.special_categories.each do |special_category|
263
+ assert_equal "SpecialCategory", special_category.class.to_s
264
+ end
265
+ end
266
+
267
+ def test_eager_with_has_one_dependent_does_not_destroy_dependent
268
+ assert_not_nil companies(:first_firm).account
269
+ f = Firm.find(:first, :include => :account,
270
+ :conditions => ["companies.name = ?", "37signals"])
271
+ assert_not_nil f.account
272
+ assert_equal companies(:first_firm, :reload).account, f.account
273
+ end
274
+
275
+ def test_eager_with_invalid_association_reference
276
+ assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
277
+ post = Post.find(6, :include=> :monkeys )
278
+ }
279
+ assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
280
+ post = Post.find(6, :include=>[ :monkeys ])
281
+ }
282
+ assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
283
+ post = Post.find(6, :include=>[ 'monkeys' ])
284
+ }
285
+ assert_raises(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
286
+ post = Post.find(6, :include=>[ :monkeys, :elephants ])
287
+ }
288
+ end
289
+
290
+ def find_all_ordered(className, include=nil)
291
+ className.find(:all, :order=>"#{className.table_name}.#{className.primary_key}", :include=>include)
292
+ end
293
+
294
+ def test_limited_eager_with_order
295
+ unless current_adapter?(:IBM_DBAdapter)
296
+ assert_equal [posts(:thinking), posts(:sti_comments)], Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title)', :limit => 2, :offset => 1)
297
+ assert_equal [posts(:sti_post_and_comments), posts(:sti_comments)], Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title) DESC', :limit => 2, :offset => 1)
298
+ else
299
+ # LUW: [IBM][CLI Driver][DB2/LINUX] SQL0214N
300
+ # An expression in the ORDER BY clause in the following position,
301
+ # or starting with "UPPER..." in the "ORDER BY" clause is not valid.
302
+ # Reason code = "2". SQLSTATE=42822 SQLCODE=-214:
303
+ # SELECT O.* FROM (SELECT I.*, ROW_NUMBER() OVER () sys_row_num
304
+ # FROM (SELECT DISTINCT posts.id FROM posts
305
+ # LEFT OUTER JOIN authors ON authors.id = posts.author_id
306
+ # LEFT OUTER JOIN comments ON comments.post_id = posts.id
307
+ # WHERE (authors.name = 'David')
308
+ # ORDER BY UPPER(posts.title)) AS I) AS O WHERE sys_row_num BETWEEN 2 AND 3
309
+ #
310
+ # i5: ActiveRecord::RecordNotFound: Couldn't find Post with ID=2
311
+ #
312
+ # zOS v9: ActiveRecord::RecordNotFound: Couldn't find Post with ID=2
313
+ #
314
+ end
315
+ end
316
+
317
+ def test_limited_eager_with_multiple_order_columns
318
+ unless current_adapter?(:IBM_DBAdapter)
319
+ assert_equal [posts(:thinking), posts(:sti_comments)], Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title), posts.id', :limit => 2, :offset => 1)
320
+ assert_equal [posts(:sti_post_and_comments), posts(:sti_comments)], Post.find(:all, :include => [:author, :comments], :conditions => "authors.name = 'David'", :order => 'UPPER(posts.title) DESC, posts.id', :limit => 2, :offset => 1)
321
+ else
322
+ # LUW: [IBM][CLI Driver][DB2/LINUX] SQL0214N
323
+ # An expression in the ORDER BY clause in the following position,
324
+ # or starting with "UPPER..." in the "ORDER BY" clause is not valid.
325
+ # Reason code = "2". SQLSTATE=42822 SQLCODE=-214:
326
+ # SELECT O.* FROM (SELECT I.*, ROW_NUMBER() OVER () sys_row_num
327
+ # FROM (SELECT DISTINCT posts.id FROM posts
328
+ # LEFT OUTER JOIN authors ON authors.id = posts.author_id
329
+ # LEFT OUTER JOIN comments ON comments.post_id = posts.id
330
+ # WHERE (authors.name = 'David')
331
+ # ORDER BY UPPER(posts.title), posts.id) AS I) AS O WHERE sys_row_num BETWEEN 2 AND 3
332
+ #
333
+ # i5: [IBM][CLI Driver][AS] SQL0214N
334
+ # An expression in the ORDER BY clause in the following position,
335
+ # or starting with "1" in the " OBY0002" clause is not valid.
336
+ # Reason code = "2". SQLSTATE=42822 SQLCODE=-214:
337
+ # SELECT DISTINCT posts.id FROM posts
338
+ # LEFT OUTER JOIN authors ON authors.id = posts.author_id
339
+ # LEFT OUTER JOIN comments ON comments.post_id = posts.id
340
+ # WHERE (authors.name = 'David')
341
+ # ORDER BY UPPER(posts.title), posts.id
342
+ #
343
+ # zOS 9: [IBM][CLI Driver][DB2] SQL0214N
344
+ # An expression in the ORDER BY clause in the following position,
345
+ # or starting with "1" in the "ORDER BY" clause is not valid.
346
+ # Reason code = "2". SQLSTATE=42822 SQLCODE=-214:
347
+ # SELECT DISTINCT posts.id FROM posts
348
+ # LEFT OUTER JOIN authors ON authors.id = posts.author_id
349
+ # LEFT OUTER JOIN comments ON comments.post_id = posts.id
350
+ # WHERE (authors.name = 'David')
351
+ # ORDER BY UPPER(posts.title), posts.id
352
+ #
353
+ end
354
+ end
355
+
356
+ def test_eager_with_multiple_associations_with_same_table_has_many_and_habtm
357
+ # Eager includes of has many and habtm associations aren't necessarily sorted in the same way
358
+ def assert_equal_after_sort(item1, item2, item3 = nil)
359
+ assert_equal(item1.sort{|a,b| a.id <=> b.id}, item2.sort{|a,b| a.id <=> b.id})
360
+ assert_equal(item3.sort{|a,b| a.id <=> b.id}, item2.sort{|a,b| a.id <=> b.id}) if item3
361
+ end
362
+ # Test regular association, association with conditions, association with
363
+ # STI, and association with conditions assured not to be true
364
+ post_types = [:posts, :other_posts, :special_posts]
365
+ # test both has_many and has_and_belongs_to_many
366
+ [Author, Category].each do |className|
367
+ d1 = find_all_ordered(className)
368
+ # test including all post types at once
369
+ d2 = find_all_ordered(className, post_types)
370
+ d1.each_index do |i|
371
+ assert_equal(d1[i], d2[i])
372
+ assert_equal_after_sort(d1[i].posts, d2[i].posts)
373
+ post_types[1..-1].each do |post_type|
374
+ # test including post_types together
375
+ d3 = find_all_ordered(className, [:posts, post_type])
376
+ assert_equal(d1[i], d3[i])
377
+ assert_equal_after_sort(d1[i].posts, d3[i].posts)
378
+ assert_equal_after_sort(d1[i].send(post_type), d2[i].send(post_type), d3[i].send(post_type))
379
+ end
380
+ end
381
+ end
382
+ end
383
+
384
+ def test_eager_with_multiple_associations_with_same_table_has_one
385
+ d1 = find_all_ordered(Firm)
386
+ d2 = find_all_ordered(Firm, :account)
387
+ d1.each_index do |i|
388
+ assert_equal(d1[i], d2[i])
389
+ assert_equal(d1[i].account, d2[i].account)
390
+ end
391
+ end
392
+
393
+ def test_eager_with_multiple_associations_with_same_table_belongs_to
394
+ firm_types = [:firm, :firm_with_basic_id, :firm_with_other_name, :firm_with_condition]
395
+ d1 = find_all_ordered(Client)
396
+ d2 = find_all_ordered(Client, firm_types)
397
+ d1.each_index do |i|
398
+ assert_equal(d1[i], d2[i])
399
+ firm_types.each { |type| assert_equal(d1[i].send(type), d2[i].send(type)) }
400
+ end
401
+ end
402
+ def test_eager_with_valid_association_as_string_not_symbol
403
+ assert_nothing_raised { Post.find(:all, :include => 'comments') }
404
+ end
405
+
406
+ def test_preconfigured_includes_with_belongs_to
407
+ author = posts(:welcome).author_with_posts
408
+ assert_equal 5, author.posts.size
409
+ end
410
+
411
+ def test_preconfigured_includes_with_has_one
412
+ comment = posts(:sti_comments).very_special_comment_with_post
413
+ assert_equal posts(:sti_comments), comment.post
414
+ end
415
+
416
+ def test_preconfigured_includes_with_has_many
417
+ posts = authors(:david).posts_with_comments
418
+ one = posts.detect { |p| p.id == 1 }
419
+ assert_equal 5, posts.size
420
+ assert_equal 2, one.comments.size
421
+ end
422
+
423
+ def test_preconfigured_includes_with_habtm
424
+ posts = authors(:david).posts_with_categories
425
+ one = posts.detect { |p| p.id == 1 }
426
+ assert_equal 5, posts.size
427
+ assert_equal 2, one.categories.size
428
+ end
429
+
430
+ def test_preconfigured_includes_with_has_many_and_habtm
431
+ posts = authors(:david).posts_with_comments_and_categories
432
+ one = posts.detect { |p| p.id == 1 }
433
+ assert_equal 5, posts.size
434
+ assert_equal 2, one.comments.size
435
+ assert_equal 2, one.categories.size
436
+ end
437
+
438
+ def test_count_with_include
439
+ if current_adapter?(:SQLServerAdapter, :SybaseAdapter)
440
+ assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "len(comments.body) > 15")
441
+ else
442
+ assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "length(comments.body) > 15")
443
+ end
444
+ end
445
+ end