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.
- 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
|
+
|