jorahood-ar-extensions 0.9.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/ChangeLog +145 -0
  2. data/README +167 -0
  3. data/Rakefile +79 -0
  4. data/config/database.yml +7 -0
  5. data/config/database.yml.template +7 -0
  6. data/config/mysql.schema +72 -0
  7. data/config/postgresql.schema +39 -0
  8. data/db/migrate/generic_schema.rb +96 -0
  9. data/db/migrate/mysql_schema.rb +31 -0
  10. data/db/migrate/oracle_schema.rb +5 -0
  11. data/db/migrate/version.rb +4 -0
  12. data/init.rb +31 -0
  13. data/lib/ar-extensions/create_and_update.rb +509 -0
  14. data/lib/ar-extensions/csv.rb +309 -0
  15. data/lib/ar-extensions/delete.rb +143 -0
  16. data/lib/ar-extensions/extensions.rb +506 -0
  17. data/lib/ar-extensions/finder_options.rb +275 -0
  18. data/lib/ar-extensions/finders.rb +94 -0
  19. data/lib/ar-extensions/foreign_keys.rb +70 -0
  20. data/lib/ar-extensions/fulltext.rb +62 -0
  21. data/lib/ar-extensions/import.rb +352 -0
  22. data/lib/ar-extensions/insert_select.rb +178 -0
  23. data/lib/ar-extensions/synchronize.rb +30 -0
  24. data/lib/ar-extensions/temporary_table.rb +124 -0
  25. data/lib/ar-extensions/union.rb +204 -0
  26. data/lib/ar-extensions/version.rb +9 -0
  27. data/tests/connections/native_mysql/connection.rb +16 -0
  28. data/tests/connections/native_oracle/connection.rb +16 -0
  29. data/tests/connections/native_postgresql/connection.rb +19 -0
  30. data/tests/connections/native_sqlite/connection.rb +14 -0
  31. data/tests/connections/native_sqlite3/connection.rb +14 -0
  32. data/tests/fixtures/addresses.yml +25 -0
  33. data/tests/fixtures/books.yml +46 -0
  34. data/tests/fixtures/developers.yml +20 -0
  35. data/tests/fixtures/unit/active_record_base_finders/addresses.yml +25 -0
  36. data/tests/fixtures/unit/active_record_base_finders/books.yml +64 -0
  37. data/tests/fixtures/unit/active_record_base_finders/developers.yml +20 -0
  38. data/tests/fixtures/unit/synchronize/books.yml +16 -0
  39. data/tests/fixtures/unit/to_csv_headers/addresses.yml +8 -0
  40. data/tests/fixtures/unit/to_csv_headers/developers.yml +6 -0
  41. data/tests/fixtures/unit/to_csv_with_common_options/addresses.yml +40 -0
  42. data/tests/fixtures/unit/to_csv_with_common_options/developers.yml +13 -0
  43. data/tests/fixtures/unit/to_csv_with_common_options/languages.yml +29 -0
  44. data/tests/fixtures/unit/to_csv_with_common_options/teams.yml +3 -0
  45. data/tests/fixtures/unit/to_csv_with_default_options/developers.yml +7 -0
  46. data/tests/models/address.rb +4 -0
  47. data/tests/models/animal.rb +2 -0
  48. data/tests/models/book.rb +3 -0
  49. data/tests/models/cart_item.rb +4 -0
  50. data/tests/models/developer.rb +8 -0
  51. data/tests/models/group.rb +3 -0
  52. data/tests/models/language.rb +5 -0
  53. data/tests/models/mysql/book.rb +3 -0
  54. data/tests/models/mysql/test_innodb.rb +3 -0
  55. data/tests/models/mysql/test_memory.rb +3 -0
  56. data/tests/models/mysql/test_myisam.rb +3 -0
  57. data/tests/models/project.rb +2 -0
  58. data/tests/models/shopping_cart.rb +4 -0
  59. data/tests/models/team.rb +4 -0
  60. data/tests/models/topic.rb +13 -0
  61. data/tests/mysql/test_create_and_update.rb +290 -0
  62. data/tests/mysql/test_delete.rb +142 -0
  63. data/tests/mysql/test_finder_options.rb +121 -0
  64. data/tests/mysql/test_finders.rb +29 -0
  65. data/tests/mysql/test_import.rb +354 -0
  66. data/tests/mysql/test_insert_select.rb +173 -0
  67. data/tests/mysql/test_mysql_adapter.rb +45 -0
  68. data/tests/mysql/test_union.rb +81 -0
  69. data/tests/oracle/test_adapter.rb +14 -0
  70. data/tests/postgresql/test_adapter.rb +14 -0
  71. metadata +147 -0
@@ -0,0 +1,309 @@
1
+ begin
2
+ require 'faster_csv'
3
+ require 'ar-extensions/csv'
4
+ rescue LoadError => ex
5
+ STDERR.puts "FasterCSV is not installed. CSV functionality will not be included."
6
+ raise ex
7
+ end
8
+
9
+
10
+ # Adds CSV export options to ActiveRecord::Base models.
11
+ #
12
+ # === Example 1, exporting all fields
13
+ # class Book < ActiveRecord::Base ; end
14
+ #
15
+ # book = Book.find( 1 )
16
+ # book.to_csv
17
+ #
18
+ # === Example 2, only exporting certain fields
19
+ # class Book < ActiveRecord::Base ; end
20
+ #
21
+ # book = Book.find( 1 )
22
+ # book.to_csv( :only=>%W( title isbn )
23
+ #
24
+ # === Example 3, exporting a model including a belongs_to association
25
+ # class Book < ActiveRecord::Base
26
+ # belongs_to :author
27
+ # end
28
+ #
29
+ # book = Book.find( 1 )
30
+ # book.to_csv( :include=>:author )
31
+ #
32
+ # This also works for a has_one relationship. The :include
33
+ # option can also be an array of has_one/belongs_to
34
+ # associations. This by default includes all fields
35
+ # on the belongs_to association.
36
+ #
37
+ # === Example 4, exporting a model including a has_many association
38
+ # class Book < ActiveRecord::Base
39
+ # has_many :tags
40
+ # end
41
+ #
42
+ # book = Book.find( 1 )
43
+ # book.to_csv( :include=>:tags )
44
+ #
45
+ # This by default includes all fields on the has_many assocaition.
46
+ # This can also be an array of multiple has_many relationships. The
47
+ # array can be mixed with has_one/belongs_to associations array
48
+ # as well. IE: :include=>[ :author, :sales ]
49
+ #
50
+ # === Example 5, nesting associations
51
+ # class Book < ActiveRecord::Base
52
+ # belongs_to :author
53
+ # has_many :tags
54
+ # end
55
+ #
56
+ # book = Book.find( 1 )
57
+ # book.to_csv( :includes=>{
58
+ # :author => { :only=>%W( name ) },
59
+ # :tags => { :only=>%W( tagname ) } )
60
+ #
61
+ # Each included association can receive an options Hash. This
62
+ # allows you to nest the associations as deep as you want
63
+ # for your CSV export.
64
+ #
65
+ # It is not recommended to nest multiple has_many associations,
66
+ # although nesting multiple has_one/belongs_to associations.
67
+ #
68
+ module ActiveRecord::Extensions::FindToCSV
69
+
70
+ def self.included(base)
71
+ if !base.respond_to?(:find_with_csv)
72
+ base.class_eval do
73
+ extend ClassMethods
74
+ include InstanceMethods
75
+ end
76
+ class << base
77
+ alias_method_chain :find, :csv
78
+ end
79
+ end
80
+ end
81
+
82
+ class FieldMap# :nodoc:
83
+ attr_reader :fields, :fields_to_headers
84
+
85
+ def initialize( fields, fields_to_headers ) # :nodoc:
86
+ @fields, @fields_to_headers = fields, fields_to_headers
87
+ end
88
+
89
+ def headers # :nodoc:
90
+ @headers ||= fields.inject( [] ){ |arr,field| arr << fields_to_headers[ field ] }
91
+ end
92
+
93
+ end
94
+
95
+ module ClassMethods # :nodoc:
96
+ private
97
+
98
+ def to_csv_fields_for_nil # :nodoc:
99
+ self.columns.map{ |column| column.name }.sort
100
+ end
101
+
102
+ def to_csv_headers_for_included_associations( includes ) # :nodoc:
103
+ get_class = proc { |str| Object.const_get( self.reflections[ str.to_sym ].class_name ) }
104
+
105
+ case includes
106
+ when Symbol
107
+ [ get_class.call( includes ).to_csv_headers( :headers=>true, :naming=>":model[:header]" ) ]
108
+ when Array
109
+ includes.map do |association|
110
+ clazz = get_class.call( association )
111
+ clazz.to_csv_headers( :headers=>true, :naming=>":model[:header]" )
112
+ end
113
+ when Hash
114
+ includes.sort_by{ |k| k.to_s }.inject( [] ) do |arr,(association,options)|
115
+ clazz = get_class.call( association )
116
+ if options[:headers].is_a?( Hash )
117
+ options.merge!( :naming=>":header" )
118
+ else
119
+ options.merge!( :naming=>":model[:header]" )
120
+ end
121
+ arr << clazz.to_csv_headers( options )
122
+ end
123
+ else
124
+ []
125
+ end
126
+ end
127
+
128
+ public
129
+
130
+ def find_with_csv( *args ) # :nodoc:
131
+ results = find_without_csv( *args )
132
+ results.extend( ArrayInstanceMethods ) if results.is_a?( Array )
133
+ results
134
+ end
135
+
136
+ def to_csv_fields( options={} ) # :nodoc:
137
+ fields_to_headers, fields = {}, []
138
+
139
+ headers = options[:headers]
140
+ case headers
141
+ when Array
142
+ fields = headers.map{ |e| e.to_s }
143
+ when Hash
144
+ headers = headers.inject( {} ){ |hsh,(k,v)| hsh[k.to_s] = v ; hsh }
145
+ fields = headers.keys.sort
146
+ fields.each { |field| fields_to_headers[field] = headers[field] }
147
+ else
148
+ fields = to_csv_fields_for_nil
149
+ end
150
+
151
+ if options[:only]
152
+ specified_fields = options[:only].map{ |e| e.to_s }
153
+ fields.delete_if{ |field| not specified_fields.include?( field ) }
154
+ elsif options[:except]
155
+ excluded_fields = options[:except].map{ |e| e.to_s }
156
+ fields.delete_if{ |field| excluded_fields.include?( field ) }
157
+ end
158
+
159
+ fields.each{ |field| fields_to_headers[field] = field } if fields_to_headers.empty?
160
+
161
+ FieldMap.new( fields, fields_to_headers )
162
+ end
163
+
164
+ # Returns an array of CSV headers passed in the array of +options+.
165
+ def to_csv_headers( options={} )
166
+ options = { :headers=>true, :naming=>":header" }.merge( options )
167
+ return nil if not options[:headers]
168
+
169
+ fieldmap = to_csv_fields( options )
170
+ headers = fieldmap.headers
171
+ headers.push( *to_csv_headers_for_included_associations( options[ :include ] ).flatten )
172
+ headers.map{ |header| options[:naming].gsub( /:header/, header ).gsub( /:model/, self.name.downcase ) }
173
+ end
174
+
175
+ end
176
+
177
+
178
+ module InstanceMethods
179
+
180
+ private
181
+
182
+ def add_to_csv_association_methods!(association_name)
183
+ association = self.send association_name
184
+ association.send( :extend, ArrayInstanceMethods ) if association.is_a?( Array )
185
+ association
186
+ end
187
+
188
+ def add_to_csv_association_data! data, to
189
+ if to.empty?
190
+ to.push( *data )
191
+ else
192
+ originals = to.dup
193
+ to.clear
194
+ data.each do |assoc_csv|
195
+ originals.each do |sibling|
196
+ to.push( sibling + assoc_csv )
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ def to_csv_association_is_blank?(association)
203
+ association.nil? or (association.respond_to?( :empty? ) and association.empty?)
204
+ end
205
+
206
+ def to_csv_data_for_included_associations( includes ) # :nodoc:
207
+ get_class = proc { |str| Object.const_get( self.class.reflections[ str.to_sym ].class_name ) }
208
+
209
+ case includes
210
+ when Symbol
211
+ association = add_to_csv_association_methods! includes
212
+ if to_csv_association_is_blank?(association)
213
+ [ get_class.call( includes ).columns.map{ '' } ]
214
+ else
215
+ [ *association.to_csv_data ]
216
+ end
217
+ when Array
218
+ siblings = []
219
+ includes.each do |association_name|
220
+ association = add_to_csv_association_methods! association_name
221
+ if to_csv_association_is_blank?(association)
222
+ association_data = [ get_class.call( association_name ).columns.map{ '' } ]
223
+ else
224
+ association_data = association.to_csv_data
225
+ end
226
+
227
+ add_to_csv_association_data! association_data, siblings
228
+ end
229
+ siblings
230
+ when Hash
231
+ sorted_includes = includes.sort_by{ |k| k.to_s }
232
+ siblings = []
233
+ sorted_includes.each do |(association_name,options)|
234
+ association = add_to_csv_association_methods! association_name
235
+ if to_csv_association_is_blank?(association)
236
+ association_data = [ get_class.call( association_name ).columns.map{ '' } ]
237
+ else
238
+ association_data = association.to_csv_data( options )
239
+ end
240
+ add_to_csv_association_data! association_data, siblings
241
+ end
242
+ siblings
243
+ else
244
+ []
245
+ end
246
+ end
247
+
248
+ public
249
+
250
+ # Returns CSV data without any header rows for the passed in +options+.
251
+ def to_csv_data( options={} )
252
+ fields = self.class.to_csv_fields( options ).fields
253
+ data, model_data = [], fields.inject( [] ) { |arr,field| arr << attributes[field].to_s }
254
+ if options[:include]
255
+ to_csv_data_for_included_associations( options[:include ] ).map do |assoc_csv_data|
256
+ data << model_data + assoc_csv_data
257
+ end
258
+ else
259
+ data << model_data
260
+ end
261
+ data
262
+ end
263
+
264
+ # Returns CSV data including header rows for the passed in +options+.
265
+ def to_csv( options={} )
266
+ FasterCSV.generate do |csv|
267
+ headers = self.class.to_csv_headers( options )
268
+ csv << headers if headers
269
+ to_csv_data( options ).each{ |data| csv << data }
270
+ end
271
+ end
272
+
273
+ end
274
+
275
+ module ArrayInstanceMethods # :nodoc:
276
+ class NoRecordsError < StandardError ; end #:nodoc:
277
+
278
+ # Returns CSV headers for an array of ActiveRecord::Base
279
+ # model objects by calling to_csv_headers on the first
280
+ # element.
281
+ def to_csv_headers( options={} )
282
+ first.class.to_csv_headers( options )
283
+ end
284
+
285
+ # Returns CSV data without headers for an array of
286
+ # ActiveRecord::Base model objects by iterating over them and
287
+ # calling to_csv_data with the passed in +options+.
288
+ def to_csv_data( options={} )
289
+ inject( [] ) do |arr,model_instance|
290
+ arr.push( *model_instance.to_csv_data( options ) )
291
+ end
292
+ end
293
+
294
+ # Returns CSV data with headers for an array of ActiveRecord::Base
295
+ # model objects by iterating over them and calling to_csv with
296
+ # the passed in +options+.
297
+ def to_csv( options={} )
298
+ FasterCSV.generate do |csv|
299
+ headers = to_csv_headers( options )
300
+ csv << headers if headers
301
+ each do |model_instance|
302
+ model_instance.to_csv_data( options ).each{ |data| csv << data }
303
+ end
304
+ end
305
+ end
306
+
307
+ end
308
+
309
+ end
@@ -0,0 +1,143 @@
1
+ module ActiveRecord::Extensions::Delete#:nodoc:
2
+ mattr_accessor :delete_batch_size
3
+ self.delete_batch_size = 15000
4
+
5
+ module DeleteSupport #:nodoc:
6
+ def supports_delete? #:nodoc:
7
+ true
8
+ end
9
+ end
10
+ end
11
+
12
+ class ActiveRecord::Base
13
+ supports_extension :delete
14
+
15
+ class << self
16
+
17
+ # Delete all specified records with options
18
+ #
19
+ # == Parameters
20
+ # * +conditions+ - the conditions normally specified to +delete_all+
21
+ # * +options+ - hash map of additional parameters
22
+ #
23
+ # == Options
24
+ # * <tt>:limit</tt> - the maximum number of records to delete.
25
+ # * <tt>:batch</tt> - delete in batches specified to avoid database contention
26
+ # Multiple sql deletions are executed in order to avoid database contention
27
+ # This has no affect if used inside a transaction
28
+ #
29
+ # Delete up to 65 red tags
30
+ # Tag.delete_all ['name like ?', '%red%'], :limit => 65
31
+ #
32
+ # Delete up to 65 red tags in batches of 20. This will execute up to
33
+ # 4 delete statements: 3 batches of 20 and the final batch of 5.
34
+ # Tag.delete_all ['name like ?', '%red%'], :limit => 65, :batch => 20
35
+ def delete_all_with_extension(conditions = nil, options={})
36
+
37
+ #raise an error if delete is not supported and options are specified
38
+ supports_delete! if options.any?
39
+
40
+ #call the base method if no options specified
41
+ return delete_all_without_extension(conditions) unless options.any?
42
+
43
+ #batch delete
44
+ return delete_all_batch(conditions, options[:batch], options[:limit]) if options[:batch]
45
+
46
+ #regular delete with limit
47
+ connection.delete(delete_all_extension_sql(conditions, options), "#{name} Delete All")
48
+ end
49
+
50
+ alias_method_chain :delete_all, :extension
51
+
52
+
53
+ # Utility function to delete all but one of the duplicate records
54
+ # matching the fields specified. This method will make the records
55
+ # unique for the specified fields.
56
+ #
57
+ # == Options
58
+ # * <tt>:fields</tt> - the fields to match on
59
+ # * <tt>:conditions</tt> - additional conditions
60
+ # * <tt>:winner_clause</tt> - the part of the query specifying what wins. Default winner is that with the greatest id.
61
+ # * <tt>:query_field</tt> -> the field to use to determine the winner. Defaults to primary_key (id). The tables are aliased
62
+ # to c1 and c2 respectively
63
+ # == Examples
64
+ # Make all the phone numbers of contacts unique by deleting the duplicates with the highest ids
65
+ # Contacts.delete_duplicates(:fields=>['phone_number_id'])
66
+ #
67
+ # Delete all tags that are the same preserving the ones with the highest id
68
+ # Tag.delete_duplicates :fields => [:name], :winner_clause => "c1.id < c2.id"
69
+ #
70
+ # Remove duplicate invitations (those that from the same person and to the same recipient)
71
+ # preseving the first ones inserted
72
+ # Invitation.delete_duplicates :fields=>[:event_id, :from_id, :recipient_id]
73
+ def delete_duplicates(options={})
74
+ supports_delete!
75
+
76
+ options[:query_field]||= primary_key
77
+
78
+ query = "DELETE FROM"
79
+ query << " c1 USING #{quoted_table_name} c1, #{quoted_table_name} c2"
80
+ query << " WHERE ("
81
+ query << options[:fields].collect{|field| "c1.#{field} = c2.#{field}" }.join(" and ")
82
+ query << " and (#{sanitize_sql(options[:conditions])})" unless options[:conditions].blank?
83
+ query << " and "
84
+ query << (options[:winner_clause]||"c1.#{options[:query_field]} > c2.#{options[:query_field]}")
85
+ query << ")"
86
+
87
+ self.connection.execute(self.send(:sanitize_sql, query))
88
+ end
89
+
90
+ protected
91
+
92
+
93
+ # Delete all records specified in batches
94
+ #
95
+ # == Parameters
96
+ # * +conditions+ - the conditions normally specified to +delete_all+
97
+ # * +batch+ - the size of the batches to delete. defaults to 15000
98
+ # * +limit+ - the maximum number of records to delete
99
+ #
100
+ def delete_all_batch(conditions=nil, batch=nil, limit=nil)#:nodoc:
101
+
102
+ #update the batch size if batch is nil or true or 0
103
+ if batch.nil? || !batch.is_a?(Fixnum) || batch.to_i == 0
104
+ batch = ActiveRecord::Extensions::Delete.delete_batch_size
105
+ end
106
+
107
+
108
+ sql = delete_all_extension_sql(conditions, :limit => batch)
109
+ page_num = total = 0
110
+
111
+ loop {
112
+ page_num += 1
113
+
114
+ #if this is the last batch query and limit is set
115
+ #only delete the remainer
116
+ if limit && (total + batch > limit)
117
+ sql = delete_all_extension_sql(conditions, :limit => (limit - total))
118
+ end
119
+
120
+ count = connection.delete(sql, "#{name} Delete All Batch #{page_num}")
121
+ total += count
122
+
123
+ # Return if
124
+ # * last query did not return the batch size (meaning nothing left to delete)
125
+ # * we have reached our limit
126
+ if (count < batch) || (limit && (total >= limit))
127
+ return total
128
+ end
129
+ }
130
+ end
131
+
132
+ #generate the delete SQL with limit
133
+ def delete_all_extension_sql(conditions, options={})#:nodoc:
134
+ sql = "DELETE FROM #{quoted_table_name} "
135
+ add_conditions!(sql, conditions, scope(:find))
136
+ connection.add_limit_offset!(sql, options)
137
+ sql
138
+ end
139
+
140
+ end
141
+
142
+ end
143
+