Empact-ar-extensions 0.9.2

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 +167 -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.rb +5 -0
  14. data/lib/ar-extensions/adapters/abstract_adapter.rb +146 -0
  15. data/lib/ar-extensions/adapters/mysql.rb +10 -0
  16. data/lib/ar-extensions/adapters/oracle.rb +14 -0
  17. data/lib/ar-extensions/adapters/postgresql.rb +9 -0
  18. data/lib/ar-extensions/adapters/sqlite.rb +7 -0
  19. data/lib/ar-extensions/create_and_update.rb +508 -0
  20. data/lib/ar-extensions/create_and_update/mysql.rb +7 -0
  21. data/lib/ar-extensions/csv.rb +309 -0
  22. data/lib/ar-extensions/delete.rb +143 -0
  23. data/lib/ar-extensions/delete/mysql.rb +3 -0
  24. data/lib/ar-extensions/extensions.rb +509 -0
  25. data/lib/ar-extensions/finder_options.rb +275 -0
  26. data/lib/ar-extensions/finder_options/mysql.rb +6 -0
  27. data/lib/ar-extensions/finders.rb +96 -0
  28. data/lib/ar-extensions/foreign_keys.rb +70 -0
  29. data/lib/ar-extensions/fulltext.rb +62 -0
  30. data/lib/ar-extensions/fulltext/mysql.rb +44 -0
  31. data/lib/ar-extensions/import.rb +354 -0
  32. data/lib/ar-extensions/import/mysql.rb +50 -0
  33. data/lib/ar-extensions/import/postgresql.rb +0 -0
  34. data/lib/ar-extensions/import/sqlite.rb +22 -0
  35. data/lib/ar-extensions/insert_select.rb +178 -0
  36. data/lib/ar-extensions/insert_select/mysql.rb +7 -0
  37. data/lib/ar-extensions/synchronize.rb +30 -0
  38. data/lib/ar-extensions/temporary_table.rb +131 -0
  39. data/lib/ar-extensions/temporary_table/mysql.rb +3 -0
  40. data/lib/ar-extensions/union.rb +204 -0
  41. data/lib/ar-extensions/union/mysql.rb +6 -0
  42. data/lib/ar-extensions/util/sql_generation.rb +27 -0
  43. data/lib/ar-extensions/util/support_methods.rb +32 -0
  44. data/lib/ar-extensions/version.rb +9 -0
  45. metadata +128 -0
@@ -0,0 +1,62 @@
1
+ require 'forwardable'
2
+
3
+ # FullTextSearching provides fulltext searching capabilities
4
+ # if the underlying database adapter supports it. Currently
5
+ # only MySQL is supported.
6
+ module ActiveRecord::Extensions::FullTextSearching
7
+
8
+ module FullTextSupport # :nodoc:
9
+ def supports_full_text_searching? #:nodoc:
10
+ true
11
+ end
12
+ end
13
+
14
+ end
15
+
16
+ class ActiveRecord::Base
17
+ class FullTextSearchingNotSupported < StandardError ; end
18
+
19
+ class << self
20
+
21
+ # Adds fulltext searching capabilities to the current model
22
+ # for the given fulltext key and option hash.
23
+ #
24
+ # == Parameters
25
+ # * +fulltext_key+ - the key/attribute to be used to as the fulltext index
26
+ # * +options+ - the options hash.
27
+ #
28
+ # ==== Options
29
+ # * +fields+ - an array of field names to be used in the fulltext search
30
+ #
31
+ # == Example
32
+ #
33
+ # class Book < ActiveRecord::Base
34
+ # fulltext :title, :fields=>%W( title publisher author_name )
35
+ # end
36
+ #
37
+ # # To use the fulltext index
38
+ # Book.find :all, :conditions=>{ :match_title => 'Zach' }
39
+ #
40
+ def fulltext( fulltext_key, options )
41
+ connection.register_fulltext_extension( fulltext_key, options )
42
+ rescue NoMethodError
43
+ logger.warn "FullTextSearching is not supported for adapter!"
44
+ raise FullTextSearchingNotSupported.new
45
+ end
46
+
47
+ # Returns true if the current connection adapter supports full
48
+ # text searching, otherwise returns false.
49
+ def supports_full_text_searching?
50
+ connection.supports_full_text_searching?
51
+ rescue NoMethodError
52
+ false
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+
59
+
60
+
61
+
62
+
@@ -0,0 +1,44 @@
1
+ # This adds FullText searching functionality for the MySQLAdapter.
2
+ class ActiveRecord::Extensions::FullTextSearching::MySQLFullTextExtension
3
+ extend Forwardable
4
+
5
+ class << self
6
+ extend Forwardable
7
+
8
+ def register( fulltext_key, options ) # :nodoc:
9
+ @fulltext_registry ||= ActiveRecord::Extensions::Registry.new
10
+ @fulltext_registry.register( fulltext_key, options )
11
+ end
12
+
13
+ def registry # :nodoc:
14
+ @fulltext_registry
15
+ end
16
+
17
+ def_delegator :@fulltext_registry, :registers?, :registers?
18
+ end
19
+
20
+ RGX = /^match_(.+)/
21
+
22
+ def process( key, val, caller ) # :nodoc:
23
+ match_data = key.to_s.match( RGX )
24
+ return nil unless match_data
25
+ fulltext_identifier = match_data.captures[0].to_sym
26
+ if self.class.registers?( fulltext_identifier )
27
+ fields = self.class.registry.options( fulltext_identifier )[:fields]
28
+ str = "MATCH ( #{fields.join( ',' )} ) AGAINST (#{caller.connection.quote(val)})"
29
+ return ActiveRecord::Extensions::Result.new( str, nil )
30
+ end
31
+ nil
32
+ end
33
+
34
+ def_delegator 'ActiveRecord::Extensions::FullTextSupport::MySQLFullTextExtension', :register
35
+ end
36
+ ActiveRecord::Extensions.register ActiveRecord::Extensions::FullTextSearching::MySQLFullTextExtension.new, :adapters=>[:mysql]
37
+
38
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
39
+ include ActiveRecord::Extensions::FullTextSearching::FullTextSupport
40
+
41
+ def register_fulltext_extension( fulltext_key, options ) # :nodoc:
42
+ ActiveRecord::Extensions::FullTextSearching::MySQLFullTextExtension.register( fulltext_key, options )
43
+ end
44
+ end
@@ -0,0 +1,354 @@
1
+ module ActiveRecord::Extensions::ConnectionAdapters ; end
2
+
3
+ module ActiveRecord::Extensions::Import #:nodoc:
4
+
5
+ module ImportSupport #:nodoc:
6
+ def supports_import? #:nodoc:
7
+ true
8
+ end
9
+ end
10
+
11
+ module OnDuplicateKeyUpdateSupport #:nodoc:
12
+ def supports_on_duplicate_key_update? #:nodoc:
13
+ true
14
+ end
15
+ end
16
+
17
+ end
18
+
19
+ class ActiveRecord::Base
20
+ class << self
21
+
22
+ # use tz as set in ActiveRecord::Base
23
+ tproc = @@default_timezone == :utc ? lambda { Time.now.utc } : lambda { Time.now }
24
+ AREXT_RAILS_COLUMNS = {
25
+ :create => { "created_on" => tproc ,
26
+ "created_at" => tproc },
27
+ :update => { "updated_on" => tproc ,
28
+ "updated_at" => tproc }
29
+ }
30
+ AREXT_RAILS_COLUMN_NAMES = AREXT_RAILS_COLUMNS[:create].keys + AREXT_RAILS_COLUMNS[:update].keys
31
+
32
+ # Returns true if the current database connection adapter
33
+ # supports import functionality, otherwise returns false.
34
+ def supports_import?
35
+ connection.supports_import?
36
+ rescue NoMethodError
37
+ false
38
+ end
39
+
40
+ # Returns true if the current database connection adapter
41
+ # supports on duplicate key update functionality, otherwise
42
+ # returns false.
43
+ def supports_on_duplicate_key_update?
44
+ connection.supports_on_duplicate_key_update?
45
+ rescue NoMethodError
46
+ false
47
+ end
48
+
49
+ # Imports a collection of values to the database.
50
+ #
51
+ # This is more efficient than using ActiveRecord::Base#create or
52
+ # ActiveRecord::Base#save multiple times. This method works well if
53
+ # you want to create more than one record at a time and do not care
54
+ # about having ActiveRecord objects returned for each record
55
+ # inserted.
56
+ #
57
+ # This can be used with or without validations. It does not utilize
58
+ # the ActiveRecord::Callbacks during creation/modification while
59
+ # performing the import.
60
+ #
61
+ # == Usage
62
+ # Model.import array_of_models
63
+ # Model.import column_names, array_of_values
64
+ # Model.import column_names, array_of_values, options
65
+ #
66
+ # ==== Model.import array_of_models
67
+ #
68
+ # With this form you can call _import_ passing in an array of model
69
+ # objects that you want updated.
70
+ #
71
+ # ==== Model.import column_names, array_of_values
72
+ #
73
+ # The first parameter +column_names+ is an array of symbols or
74
+ # strings which specify the columns that you want to update.
75
+ #
76
+ # The second parameter, +array_of_values+, is an array of
77
+ # arrays. Each subarray is a single set of values for a new
78
+ # record. The order of values in each subarray should match up to
79
+ # the order of the +column_names+.
80
+ #
81
+ # ==== Model.import column_names, array_of_values, options
82
+ #
83
+ # The first two parameters are the same as the above form. The third
84
+ # parameter, +options+, is a hash. This is optional. Please see
85
+ # below for what +options+ are available.
86
+ #
87
+ # == Options
88
+ # * +validate+ - true|false, tells import whether or not to use \
89
+ # ActiveRecord validations. Validations are enforced by default.
90
+ # * +on_duplicate_key_update+ - an Array or Hash, tells import to \
91
+ # use MySQL's ON DUPLICATE KEY UPDATE ability. See On Duplicate\
92
+ # Key Update below.
93
+ # * +synchronize+ - an array of ActiveRecord instances for the model
94
+ # that you are currently importing data into. This synchronizes
95
+ # existing model instances in memory with updates from the import.
96
+ # * +timestamps+ - true|false, tells import to not add timestamps \
97
+ # (if false) even if record timestamps is disabled in ActiveRecord::Base
98
+ #
99
+ # == Examples
100
+ # class BlogPost < ActiveRecord::Base ; end
101
+ #
102
+ # # Example using array of model objects
103
+ # posts = [ BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT',
104
+ # BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT2',
105
+ # BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT3' ]
106
+ # BlogPost.import posts
107
+ #
108
+ # # Example using column_names and array_of_values
109
+ # columns = [ :author_name, :title ]
110
+ # values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
111
+ # BlogPost.import columns, values
112
+ #
113
+ # # Example using column_names, array_of_value and options
114
+ # columns = [ :author_name, :title ]
115
+ # values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
116
+ # BlogPost.import( columns, values, :validate => false )
117
+ #
118
+ # # Example synchronizing existing instances in memory
119
+ # post = BlogPost.find_by_author_name( 'zdennis' )
120
+ # puts post.author_name # => 'zdennis'
121
+ # columns = [ :author_name, :title ]
122
+ # values = [ [ 'yoda', 'test post' ] ]
123
+ # BlogPost.import posts, :synchronize=>[ post ]
124
+ # puts post.author_name # => 'yoda'
125
+ #
126
+ # == On Duplicate Key Update (MySQL only)
127
+ #
128
+ # The :on_duplicate_key_update option can be either an Array or a Hash.
129
+ #
130
+ # ==== Using an Array
131
+ #
132
+ # The :on_duplicate_key_update option can be an array of column
133
+ # names. The column names are the only fields that are updated if
134
+ # a duplicate record is found. Below is an example:
135
+ #
136
+ # BlogPost.import columns, values, :on_duplicate_key_update=>[ :date_modified, :content, :author ]
137
+ #
138
+ # ==== Using A Hash
139
+ #
140
+ # The :on_duplicate_key_update option can be a hash of column name
141
+ # to model attribute name mappings. This gives you finer grained
142
+ # control over what fields are updated with what attributes on your
143
+ # model. Below is an example:
144
+ #
145
+ # BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :title => :title }
146
+ #
147
+ # = Returns
148
+ # This returns an object which responds to +failed_instances+ and +num_inserts+.
149
+ # * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed.
150
+ # * num_inserts - the number of insert statements it took to import the data
151
+ def import( *args )
152
+ @logger = Logger.new(STDOUT)
153
+ @logger.level = Logger::DEBUG
154
+ options = { :validate=>true, :timestamps=>true }
155
+ options.merge!( args.pop ) if args.last.is_a? Hash
156
+
157
+ # assume array of model objects
158
+ if args.last.is_a?( Array ) and args.last.first.is_a? ActiveRecord::Base
159
+ if args.length == 2
160
+ models = args.last
161
+ column_names = args.first
162
+ else
163
+ models = args.first
164
+ column_names = self.column_names.dup
165
+ end
166
+
167
+ array_of_attributes = []
168
+ models.each do |model|
169
+ # this next line breaks sqlite.so with a segmentation fault
170
+ # if model.new_record? || options[:on_duplicate_key_update]
171
+ attributes = []
172
+ column_names.each do |name|
173
+ attributes << model.send( "#{name}_before_type_cast" )
174
+ end
175
+ array_of_attributes << attributes
176
+ # end
177
+ end
178
+ # supports 2-element array and array
179
+ elsif args.size == 2 and args.first.is_a?( Array ) and args.last.is_a?( Array )
180
+ column_names, array_of_attributes = args
181
+ else
182
+ raise ArgumentError.new( "Invalid arguments!" )
183
+ end
184
+
185
+ # Force the primary key col into the insert if it's not
186
+ # on the list and we are using a sequence and stuff a nil
187
+ # value for it into each row so the sequencer will fire later
188
+ if !column_names.include?(primary_key) && sequence_name && connection.prefetch_primary_key?
189
+ column_names << primary_key
190
+ array_of_attributes.each { |a| a << nil }
191
+ end
192
+
193
+ is_validating = options.delete( :validate )
194
+
195
+ # dup the passed in array so we don't modify it unintentionally
196
+ array_of_attributes = array_of_attributes.dup
197
+
198
+ # record timestamps unless disabled in ActiveRecord::Base
199
+ if record_timestamps && options.delete( :timestamps )
200
+ add_special_rails_stamps column_names, array_of_attributes, options
201
+ end
202
+
203
+ return_obj = if is_validating
204
+ import_with_validations( column_names, array_of_attributes, options )
205
+ else
206
+ num_inserts = import_without_validations_or_callbacks( column_names, array_of_attributes, options )
207
+ OpenStruct.new :failed_instances=>[], :num_inserts=>num_inserts
208
+ end
209
+ if options[:synchronize]
210
+ synchronize( options[:synchronize] )
211
+ end
212
+
213
+ return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
214
+ return_obj
215
+ end
216
+
217
+ # TODO import_from_table needs to be implemented.
218
+ def import_from_table( options ) # :nodoc:
219
+ end
220
+
221
+ # Imports the passed in +column_names+ and +array_of_attributes+
222
+ # given the passed in +options+ Hash with validations. Returns an
223
+ # object with the methods +failed_instances+ and +num_inserts+.
224
+ # +failed_instances+ is an array of instances that failed validations.
225
+ # +num_inserts+ is the number of inserts it took to import the data. See
226
+ # ActiveRecord::Base.import for more information on
227
+ # +column_names+, +array_of_attributes+ and +options+.
228
+ def import_with_validations( column_names, array_of_attributes, options={} )
229
+ failed_instances = []
230
+
231
+ # create instances for each of our column/value sets
232
+ arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
233
+
234
+ # keep track of the instance and the position it is currently at. if this fails
235
+ # validation we'll use the index to remove it from the array_of_attributes
236
+ arr.each_with_index do |hsh,i|
237
+ instance = new( hsh )
238
+ if not instance.valid?
239
+ array_of_attributes[ i ] = nil
240
+ failed_instances << instance
241
+ end
242
+ end
243
+ array_of_attributes.compact!
244
+
245
+ num_inserts = array_of_attributes.empty? ? 0 : import_without_validations_or_callbacks( column_names, array_of_attributes, options )
246
+ OpenStruct.new :failed_instances=>failed_instances, :num_inserts => num_inserts
247
+ end
248
+
249
+ # Imports the passed in +column_names+ and +array_of_attributes+
250
+ # given the passed in +options+ Hash. This will return the number
251
+ # of insert operations it took to create these records without
252
+ # validations or callbacks. See ActiveRecord::Base.import for more
253
+ # information on +column_names+, +array_of_attributes_ and
254
+ # +options+.
255
+ def import_without_validations_or_callbacks( column_names, array_of_attributes, options={} )
256
+ escaped_column_names = quote_column_names( column_names )
257
+ columns = []
258
+ array_of_attributes.first.each_with_index { |arr,i| columns << columns_hash[ column_names[i] ] }
259
+
260
+ if not supports_import?
261
+ columns_sql = "(" + escaped_column_names.join( ',' ) + ")"
262
+ insert_statements, values = [], []
263
+ number_inserted = 0
264
+ array_of_attributes.each do |arr|
265
+ my_values = []
266
+ arr.each_with_index do |val,j|
267
+ if !sequence_name.blank? && column_names[j] == primary_key && val.nil?
268
+ my_values << connection.next_value_for_sequence(sequence_name)
269
+ else
270
+ my_values << connection.quote( val, columns[j] )
271
+ end
272
+ end
273
+ insert_statements << "INSERT INTO #{quoted_table_name} #{columns_sql} VALUES(" + my_values.join( ',' ) + ")"
274
+ begin
275
+ connection.create_savepoint
276
+ connection.execute( insert_statements.last )
277
+ rescue ActiveRecord::StatementInvalid
278
+ connection.rollback_to_savepoint
279
+ raise
280
+ end
281
+ number_inserted += 1
282
+ end
283
+ else
284
+ # generate the sql
285
+ insert_sql = connection.multiple_value_sets_insert_sql( quoted_table_name, escaped_column_names, options )
286
+ values_sql = connection.values_sql_for_column_names_and_attributes( columns, array_of_attributes )
287
+ post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
288
+
289
+ # perform the inserts
290
+ number_inserted = connection.insert_many( [ insert_sql, post_sql_statements ].flatten,
291
+ values_sql,
292
+ "#{self.class.name} Create Many Without Validations Or Callbacks" )
293
+ end
294
+
295
+ number_inserted
296
+ end
297
+
298
+ # Returns an array of quoted column names
299
+ def quote_column_names( names )
300
+ names.map{ |name| connection.quote_column_name( name ) }
301
+ end
302
+
303
+
304
+ private
305
+
306
+
307
+ def add_special_rails_stamps( column_names, array_of_attributes, options )
308
+ AREXT_RAILS_COLUMNS[:create].each_pair do |key, blk|
309
+ if self.column_names.include?(key)
310
+ value = blk.call
311
+ if index=column_names.index(key)
312
+ # replace every instance of the array of attributes with our value
313
+ array_of_attributes.each{ |arr| arr[index] = value }
314
+ else
315
+ column_names << key
316
+ array_of_attributes.each { |arr| arr << value }
317
+ end
318
+ end
319
+ end
320
+
321
+ AREXT_RAILS_COLUMNS[:update].each_pair do |key, blk|
322
+ if self.column_names.include?(key)
323
+ value = blk.call
324
+ if index=column_names.index(key)
325
+ # replace every instance of the array of attributes with our value
326
+ array_of_attributes.each{ |arr| arr[index] = value }
327
+ else
328
+ column_names << key
329
+ array_of_attributes.each { |arr| arr << value }
330
+ end
331
+
332
+ if options[:on_duplicate_key_update]
333
+ options[:on_duplicate_key_update] << key.to_sym if options[:on_duplicate_key_update].is_a?(Array)
334
+ options[:on_duplicate_key_update][key.to_sym] = key.to_sym if options[:on_duplicate_key_update].is_a?(Hash)
335
+ else
336
+ options[:on_duplicate_key_update] = [ key.to_sym ]
337
+ end
338
+ end
339
+ end
340
+ end
341
+
342
+ # Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
343
+ def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
344
+ arr = []
345
+ array_of_attributes.each do |attributes|
346
+ c = 0
347
+ hsh = attributes.inject( {} ){|hsh,attr| hsh[ column_names[c] ] = attr ; c+=1 ; hsh }
348
+ arr << hsh
349
+ end
350
+ arr
351
+ end
352
+
353
+ end
354
+ end