ibm_db 0.4.6 → 0.6.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.
@@ -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