ghazel-ar-extensions 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. data/ChangeLog +145 -0
  2. data/README +169 -0
  3. data/Rakefile +61 -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 +97 -0
  9. data/db/migrate/mysql_schema.rb +32 -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/adapters/abstract_adapter.rb +146 -0
  14. data/lib/ar-extensions/adapters/mysql.rb +10 -0
  15. data/lib/ar-extensions/adapters/oracle.rb +14 -0
  16. data/lib/ar-extensions/adapters/postgresql.rb +9 -0
  17. data/lib/ar-extensions/adapters/sqlite.rb +7 -0
  18. data/lib/ar-extensions/create_and_update/mysql.rb +7 -0
  19. data/lib/ar-extensions/create_and_update.rb +509 -0
  20. data/lib/ar-extensions/csv.rb +309 -0
  21. data/lib/ar-extensions/delete/mysql.rb +3 -0
  22. data/lib/ar-extensions/delete.rb +143 -0
  23. data/lib/ar-extensions/extensions.rb +513 -0
  24. data/lib/ar-extensions/finder_options/mysql.rb +6 -0
  25. data/lib/ar-extensions/finder_options.rb +275 -0
  26. data/lib/ar-extensions/finders.rb +94 -0
  27. data/lib/ar-extensions/foreign_keys.rb +70 -0
  28. data/lib/ar-extensions/fulltext/mysql.rb +44 -0
  29. data/lib/ar-extensions/fulltext.rb +62 -0
  30. data/lib/ar-extensions/import/mysql.rb +50 -0
  31. data/lib/ar-extensions/import/postgresql.rb +0 -0
  32. data/lib/ar-extensions/import/sqlite.rb +22 -0
  33. data/lib/ar-extensions/import.rb +348 -0
  34. data/lib/ar-extensions/insert_select/mysql.rb +7 -0
  35. data/lib/ar-extensions/insert_select.rb +178 -0
  36. data/lib/ar-extensions/synchronize.rb +30 -0
  37. data/lib/ar-extensions/temporary_table/mysql.rb +3 -0
  38. data/lib/ar-extensions/temporary_table.rb +131 -0
  39. data/lib/ar-extensions/union/mysql.rb +6 -0
  40. data/lib/ar-extensions/union.rb +204 -0
  41. data/lib/ar-extensions/util/sql_generation.rb +27 -0
  42. data/lib/ar-extensions/util/support_methods.rb +32 -0
  43. data/lib/ar-extensions/version.rb +9 -0
  44. data/lib/ar-extensions.rb +5 -0
  45. metadata +110 -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,3 @@
1
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
2
+ include ActiveRecord::Extensions::Delete::DeleteSupport
3
+ 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
+