ar-extensions 0.8.2 → 0.9.0

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