ar-extensions 0.8.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ # Although the finder options actually override ActiveRecord::Base functionality instead of
2
+ # connector functionality, the methods are included here to keep the syntax of 0.8.0 intact
3
+ #
4
+ # To include finder options, use:
5
+ # require 'ar-extensions/create_and_update/mysql.rb'
6
+
7
+ ActiveRecord::Base.send :include, ActiveRecord::Extensions::CreateAndUpdate
@@ -179,14 +179,37 @@ module ActiveRecord::Extensions::FindToCSV
179
179
 
180
180
  private
181
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
+
182
206
  def to_csv_data_for_included_associations( includes ) # :nodoc:
183
207
  get_class = proc { |str| Object.const_get( self.class.reflections[ str.to_sym ].class_name ) }
184
208
 
185
209
  case includes
186
210
  when Symbol
187
- association = self.send( includes )
188
- association.send( :extend, ArrayInstanceMethods ) if association.is_a?( Array )
189
- if association.nil? or (association.respond_to?( :empty? ) and association.empty?)
211
+ association = add_to_csv_association_methods! includes
212
+ if to_csv_association_is_blank?(association)
190
213
  [ get_class.call( includes ).columns.map{ '' } ]
191
214
  else
192
215
  [ *association.to_csv_data ]
@@ -194,50 +217,27 @@ module ActiveRecord::Extensions::FindToCSV
194
217
  when Array
195
218
  siblings = []
196
219
  includes.each do |association_name|
197
- association = self.send( association_name )
198
- association.send( :extend, ArrayInstanceMethods ) if association.is_a?( Array )
199
- if association.nil? or (association.respond_to?( :empty? ) and association.empty?)
220
+ association = add_to_csv_association_methods! association_name
221
+ if to_csv_association_is_blank?(association)
200
222
  association_data = [ get_class.call( association_name ).columns.map{ '' } ]
201
223
  else
202
224
  association_data = association.to_csv_data
203
225
  end
204
226
 
205
- if siblings.empty?
206
- siblings.push( *association_data )
207
- else
208
- temp = []
209
- association_data.each do |assoc_csv|
210
- siblings.each do |sibling|
211
- temp.push( sibling + assoc_csv )
212
- end
213
- end
214
- siblings = temp
215
- end
227
+ add_to_csv_association_data! association_data, siblings
216
228
  end
217
229
  siblings
218
230
  when Hash
219
231
  sorted_includes = includes.sort_by{ |k| k.to_s }
220
232
  siblings = []
221
233
  sorted_includes.each do |(association_name,options)|
222
- association = self.send( association_name )
223
- association.send( :extend, ArrayInstanceMethods ) if association.is_a?( Array )
224
- if association.nil? or (association.respond_to?( :empty ) and association.empty?)
234
+ association = add_to_csv_association_methods! association_name
235
+ if to_csv_association_is_blank?(association)
225
236
  association_data = [ get_class.call( association_name ).columns.map{ '' } ]
226
237
  else
227
238
  association_data = association.to_csv_data( options )
228
239
  end
229
-
230
- if siblings.empty?
231
- siblings.push( *association_data )
232
- else
233
- temp = []
234
- association_data.each do |assoc_csv|
235
- siblings.each do |sibling|
236
- temp.push( sibling + assoc_csv )
237
- end
238
- end
239
- siblings = temp
240
- end
240
+ add_to_csv_association_data! association_data, siblings
241
241
  end
242
242
  siblings
243
243
  else
@@ -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
+
@@ -0,0 +1,3 @@
1
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
2
+ include ActiveRecord::Extensions::Delete::DeleteSupport
3
+ end
@@ -315,7 +315,12 @@ module ActiveRecord::Extensions
315
315
  fieldname = caller.connection.quote_column_name( key )
316
316
  min = caller.connection.quote( val.first, caller.columns_hash[ key ] )
317
317
  max = caller.connection.quote( val.last, caller.columns_hash[ key ] )
318
- str = "#{caller.quoted_table_name}.#{fieldname} #{match_data ? 'NOT ' : '' } BETWEEN #{min} AND #{max}"
318
+ str = if val.exclude_end?
319
+ "#{match_data ? 'NOT ' : '' }(#{caller.quoted_table_name}.#{fieldname} >= #{min} AND #{caller.quoted_table_name}.#{fieldname} < #{max})"
320
+ else
321
+ "#{caller.quoted_table_name}.#{fieldname} #{match_data ? 'NOT ' : '' } BETWEEN #{min} AND #{max}"
322
+ end
323
+
319
324
  return Result.new( str, nil )
320
325
  end
321
326
  nil
@@ -0,0 +1,275 @@
1
+ # ActiveRecord::Extensions::FinderOptions provides additional functionality to the ActiveRecord
2
+ # ORM library created by DHH for Rails.
3
+ #
4
+ # == Using finder_sql_to_string
5
+ # Expose the finder sql to a string. The options are identical to those accepted by <tt>find(:all, options)</tt>
6
+ # the find method takes.
7
+ # === Example:
8
+ # sql = Contact.finder_sql_to_string(:include => :primary_email_address)
9
+ # Contact.find_by_sql(sql + 'USE_INDEX(blah)')
10
+ #
11
+ # == Enhanced Finder Options
12
+ # Add index hints, keywords, and pre and post SQL to the query without writing direct SQL
13
+ # === Parameter options:
14
+ # * <tt>:pre_sql</tt> appends SQL after the SELECT and before the selected columns
15
+ #
16
+ # sql = Contact.find :first, :pre_sql => "HIGH_PRIORITY", :select => 'contacts.name', :conditions => 'id = 5'
17
+ # SQL> SELECT HIGH_PRIORITY contacts.name FROM `contacts` WHERE id = 5
18
+ #
19
+ # * <tt>:post_sql</tt> appends additional SQL to the end of the statement
20
+ # Contact.find :first, :post_sql => 'FOR UPDATE', :select => 'contacts.name', :conditions => 'id = 5'
21
+ # SQL> SELECT contacts.name FROM `contacts` where id == 5 FOR UPDATE
22
+ #
23
+ # Book.find :all, :post_sql => 'USE_INDEX(blah)'
24
+ # SQL> SELECT books.* FROM `books` USE_INDEX(blah)
25
+ #
26
+ # * <tt>:override_select</tt> is used to override the <tt>SELECT</tt> clause of eager loaded associations
27
+ # The <tt>:select</tt> option is ignored by the vanilla ActiveRecord when using eager loading with associations (when <tt>:include</tt> is used)
28
+ # (refer to http://dev.rubyonrails.org/ticket/5371)
29
+ # The <tt>:override_select</tt> options allows us to directly specify a <tt>SELECT</tt> clause without affecting the operations of legacy code (ignore <tt>:select</tt>)
30
+ # of the current code. Several plugins are available that enable select with eager loading
31
+ # Several plugins exist to force <tt>:select</tt> to work with eager loading.
32
+ # <tt>script/plugin install git://github.com/blythedunham/eload-select.git </tt>
33
+ #
34
+ # * <tt>:having</tt> only works when <tt>:group</tt> option is specified
35
+ # Book.find(:all, :select => 'count(*) as count_all, topic_id', :group => :topic_id, :having => 'count(*) > 1')
36
+ # SQL>SELECT count(*) as count_all, topic_id FROM `books` GROUP BY topic_id HAVING count(*) > 1
37
+ #
38
+ # == Developers
39
+ # * Blythe Dunham http://blythedunham.com
40
+ #
41
+ # == Homepage
42
+ # * Project Site: http://www.continuousthinking.com/tags/arext
43
+ # * Rubyforge Project: http://rubyforge.org/projects/arext
44
+ # * Anonymous SVN: svn checkout svn://rubyforge.org/var/svn/arext
45
+ #
46
+ require 'active_record/version'
47
+ module ActiveRecord::Extensions::FinderOptions
48
+ def self.included(base)
49
+
50
+ #alias and include only if not yet defined
51
+ unless base.respond_to?(:construct_finder_sql_ext)
52
+ base.extend ClassMethods
53
+ base.extend ActiveRecord::Extensions::SqlGeneration
54
+ base.extend HavingOptionBackCompatibility
55
+ base.extend ConstructSqlCompatibility
56
+
57
+ base.class_eval do
58
+ class << self
59
+ VALID_FIND_OPTIONS.concat([:pre_sql, :post_sql, :keywords, :ignore, :rollup, :override_select, :having, :index_hint])
60
+ alias_method :construct_finder_sql, :construct_finder_sql_ext
61
+ alias_method_chain :construct_finder_sql_with_included_associations, :ext
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ module ClassMethods
68
+ # Return a string containing the SQL used with the find(:all)
69
+ # The options are the same as those with find(:all)
70
+ #
71
+ # Additional parameter of
72
+ # <tt>:force_eager_load</tt> forces eager loading even if the
73
+ # column is not referenced.
74
+ #
75
+ # sql = Contact.finder_sql_to_string(:include => :primary_email_address)
76
+ # Contact.find_by_sql(sql + 'USE_INDEX(blah)')
77
+ def finder_sql_to_string(options)
78
+ select_sql = self.send(
79
+ (use_eager_loading_sql?(options) ? :finder_sql_with_included_associations : :construct_finder_sql),
80
+ options.reject{|k,v| k == :force_eager_load}).strip
81
+ end
82
+
83
+ protected
84
+
85
+ # use eager loading sql (join associations) if inclu
86
+ def use_eager_loading_sql?(options)# :nodoc:
87
+ include_associations = merge_includes(scope(:find, :include), options[:include])
88
+ return ((include_associations.any?) &&
89
+ (options[:force_eager_load].is_a?(TrueClass) ||
90
+ references_eager_loaded_tables?(options)))
91
+ end
92
+
93
+ # construct_finder_sql is called when not using eager loading (:include option is NOT specified)
94
+ def construct_finder_sql_ext(options) # :nodoc:
95
+
96
+ #add piggy back option if plugin is installed
97
+ add_piggy_back!(options) if self.respond_to? :add_piggy_back!
98
+
99
+ scope = scope(:find)
100
+ sql = pre_sql_statements(options)
101
+ add_select_column_sql!(sql, options, scope)
102
+ add_from!(sql, options, scope)
103
+
104
+ sql << "#{options[:index_hint]} " if options[:index_hint]
105
+
106
+ add_joins!(sql, options[:joins], scope)
107
+ add_conditions!(sql, options[:conditions], scope)
108
+ add_group_with_having!(sql, options[:group], options[:having], scope)
109
+
110
+ add_order!(sql, options[:order], scope)
111
+ add_limit!(sql, options, scope)
112
+ add_lock!(sql, options, scope)
113
+
114
+ sql << post_sql_statements(options)
115
+ sql
116
+ end
117
+
118
+ #override the constructor for use with associations (:include option)
119
+ #directly use eager select if that plugin is loaded instead of this one
120
+ def construct_finder_sql_with_included_associations_with_ext(options, join_dependency)#:nodoc
121
+ scope = scope(:find)
122
+ sql = pre_sql_statements(options)
123
+
124
+ add_eager_selected_column_sql!(sql, options, scope, join_dependency)
125
+ add_from!(sql, options, scope)
126
+
127
+ sql << "#{options[:index_hint]} " if options[:index_hint]
128
+ sql << join_dependency.join_associations.collect{|join| join.association_join }.join
129
+
130
+ add_joins!(sql, options[:joins], scope)
131
+ add_conditions!(sql, options[:conditions], scope)
132
+
133
+ add_limited_ids_condition!(sql, options_with_group(options), join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
134
+
135
+ add_group_with_having!(sql, options[:group], options[:having], scope)
136
+ add_order!(sql, options[:order], scope)
137
+ add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
138
+ add_lock!(sql, options, scope)
139
+
140
+ sql << post_sql_statements(options)
141
+
142
+ return sanitize_sql(sql)
143
+ end
144
+
145
+ #generate the finder sql for use with associations (:include => :something)
146
+ def finder_sql_with_included_associations(options = {})#:nodoc
147
+ join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
148
+ sql = construct_finder_sql_with_included_associations_with_ext(options, join_dependency)
149
+ end
150
+
151
+ #first use :override_select
152
+ #next use :construct_eload_select_sql if eload-select is loaded
153
+ #finally use normal column aliases
154
+ def add_eager_selected_column_sql!(sql, options, scope, join_dependency)#:nodoc:
155
+ if options[:override_select]
156
+ sql << options[:override_select]
157
+ elsif respond_to? :construct_eload_select_sql
158
+ sql << construct_eload_select_sql((scope && scope[:select]) || options[:select], join_dependency)
159
+ else
160
+ sql << column_aliases(join_dependency)
161
+ end
162
+ end
163
+
164
+ #simple select sql
165
+ def add_select_column_sql!(sql, options, scope = :auto)#:nodoc:
166
+ sql << "#{options[:select] || options[:override_select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))}"
167
+ end
168
+
169
+ #from sql
170
+ def add_from!(sql, options, scope = :auto)#:nodoc:
171
+ sql << " FROM #{options[:from] || (scope && scope[:from]) || quoted_table_name} "
172
+ end
173
+
174
+ def options_with_group(options)#:nodoc:
175
+ options
176
+ end
177
+ end
178
+
179
+ #In Rails 2.0.0 add_joins! signature changed
180
+ # Pre Rails 2.0.0: add_joins!(sql, options, scope)
181
+ # After 2.0.0: add_joins!(sql, options[:joins], scope)
182
+ module ConstructSqlCompatibility
183
+ def self.extended(base)
184
+ if ActiveRecord::VERSION::STRING < '2.0.0'
185
+ base.extend ClassMethods
186
+ base.class_eval do
187
+ class << self
188
+ alias_method_chain :add_joins!, :compatibility
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ module ClassMethods
195
+ def add_joins_with_compatibility!(sql, options, scope = :auto)#:nodoc:
196
+ join_param = options.is_a?(Hash) ? options : { :joins => options }
197
+ add_joins_without_compatibility!(sql, join_param, scope)
198
+ end
199
+
200
+ #aliasing threw errors
201
+ def quoted_table_name#:nodoc:
202
+ self.table_name
203
+ end
204
+
205
+ #pre Rails 2.0.0 the order of the scope and options was different
206
+ def add_from!(sql, options, scope = :auto)#:nodoc:
207
+ sql << " FROM #{(scope && scope[:from]) || options[:from] || table_name} "
208
+ end
209
+
210
+ def add_select_column_sql!(sql, options, scope = :auto)#:nodoc:
211
+ sql << "#{options[:override_select] || (scope && scope[:select]) || options[:select] || '*'}"
212
+ end
213
+
214
+ end
215
+ end
216
+
217
+ # Before Version 2.3.0 there was no :having option
218
+ # Add this option to previous versions by overriding add_group!
219
+ # to accept a hash with keys :group and :having instead of just group
220
+ # this avoids having to completely rewrite dependent functions like
221
+ # construct_finder_sql_for_association_limiting
222
+
223
+ module HavingOptionBackCompatibility#:nodoc:
224
+ def self.extended(base)
225
+
226
+ #for previous versions define having
227
+ if ActiveRecord::VERSION::STRING < '2.3.0'
228
+ base.extend ClassMethods
229
+
230
+ #for 2.3.0+ alias our method to :add_group!
231
+ else
232
+ base.class_eval do
233
+ class << self
234
+ alias_method :add_group_with_having!, :add_group!
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+ module ClassMethods#:nodoc:
241
+ #add_group! in version 2.3 adds having already
242
+ #copy that implementation
243
+ def add_group_with_having!(sql, group, having, scope =:auto)#:nodoc:
244
+ if group
245
+ sql << " GROUP BY #{group}"
246
+ sql << " HAVING #{sanitize_sql(having)}" if having
247
+ else
248
+ scope = scope(:find) if :auto == scope
249
+ if scope && (scoped_group = scope[:group])
250
+ sql << " GROUP BY #{scoped_group}"
251
+ sql << " HAVING #{sanitize_sql(scope[:having])}" if scope[:having]
252
+ end
253
+ end
254
+ end
255
+
256
+ def add_group!(sql, group_options, scope = :auto)#:nodoc:
257
+ group, having = if group_options.is_a?(Hash) && group_options.has_key?(:group)
258
+ [group_options[:group] , group_options[:having]]
259
+ else
260
+ [group_options, nil]
261
+ end
262
+ add_group_with_having!(sql, group, having, scope)
263
+ end
264
+
265
+ def options_with_group(options)#:nodoc:
266
+ if options[:group]
267
+ options.merge(:group => {:group => options[:group], :having => options[:having]})
268
+ else
269
+ options
270
+ end
271
+ end
272
+ end
273
+
274
+ end
275
+ end