ghazel-ar-extensions 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- data/ChangeLog +145 -0
- data/README +169 -0
- data/Rakefile +61 -0
- data/config/database.yml +7 -0
- data/config/database.yml.template +7 -0
- data/config/mysql.schema +72 -0
- data/config/postgresql.schema +39 -0
- data/db/migrate/generic_schema.rb +97 -0
- data/db/migrate/mysql_schema.rb +32 -0
- data/db/migrate/oracle_schema.rb +5 -0
- data/db/migrate/version.rb +4 -0
- data/init.rb +31 -0
- data/lib/ar-extensions/adapters/abstract_adapter.rb +146 -0
- data/lib/ar-extensions/adapters/mysql.rb +10 -0
- data/lib/ar-extensions/adapters/oracle.rb +14 -0
- data/lib/ar-extensions/adapters/postgresql.rb +9 -0
- data/lib/ar-extensions/adapters/sqlite.rb +7 -0
- data/lib/ar-extensions/create_and_update/mysql.rb +7 -0
- data/lib/ar-extensions/create_and_update.rb +509 -0
- data/lib/ar-extensions/csv.rb +309 -0
- data/lib/ar-extensions/delete/mysql.rb +3 -0
- data/lib/ar-extensions/delete.rb +143 -0
- data/lib/ar-extensions/extensions.rb +513 -0
- data/lib/ar-extensions/finder_options/mysql.rb +6 -0
- data/lib/ar-extensions/finder_options.rb +275 -0
- data/lib/ar-extensions/finders.rb +94 -0
- data/lib/ar-extensions/foreign_keys.rb +70 -0
- data/lib/ar-extensions/fulltext/mysql.rb +44 -0
- data/lib/ar-extensions/fulltext.rb +62 -0
- data/lib/ar-extensions/import/mysql.rb +50 -0
- data/lib/ar-extensions/import/postgresql.rb +0 -0
- data/lib/ar-extensions/import/sqlite.rb +22 -0
- data/lib/ar-extensions/import.rb +348 -0
- data/lib/ar-extensions/insert_select/mysql.rb +7 -0
- data/lib/ar-extensions/insert_select.rb +178 -0
- data/lib/ar-extensions/synchronize.rb +30 -0
- data/lib/ar-extensions/temporary_table/mysql.rb +3 -0
- data/lib/ar-extensions/temporary_table.rb +131 -0
- data/lib/ar-extensions/union/mysql.rb +6 -0
- data/lib/ar-extensions/union.rb +204 -0
- data/lib/ar-extensions/util/sql_generation.rb +27 -0
- data/lib/ar-extensions/util/support_methods.rb +32 -0
- data/lib/ar-extensions/version.rb +9 -0
- data/lib/ar-extensions.rb +5 -0
- 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,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
|
+
|