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,6 @@
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/finder_options/mysql.rb'
6
+ ActiveRecord::Base.send :include, ActiveRecord::Extensions::FinderOptions
@@ -1,3 +1,8 @@
1
+ require 'active_record/version'
2
+
3
+
4
+
5
+
1
6
  module ActiveRecord::ConnectionAdapters::Quoting
2
7
 
3
8
  alias :quote_before_arext :quote
@@ -10,6 +15,7 @@ module ActiveRecord::ConnectionAdapters::Quoting
10
15
  end
11
16
  end
12
17
 
18
+ unless ActiveRecord::VERSION::STRING < '2.0.2'
13
19
  class ActiveRecord::Base
14
20
 
15
21
  class << self
@@ -71,7 +77,6 @@ class ActiveRecord::Base
71
77
  else
72
78
  table_name = quoted_table_name
73
79
  end
74
-
75
80
  # ActiveRecord in 2.3.1 changed the method signature for
76
81
  # the method attribute_condition
77
82
  if ActiveRecord::VERSION::STRING < '2.3.1'
@@ -92,3 +97,4 @@ class ActiveRecord::Base
92
97
  end
93
98
 
94
99
  end
100
+ end
@@ -12,6 +12,8 @@ module ActiveRecord::Extensions::ConnectionAdapters::MysqlAdapter # :nodoc:
12
12
  sql << sql_for_on_duplicate_key_update_as_array( table_name, arg )
13
13
  elsif arg.is_a?( Hash )
14
14
  sql << sql_for_on_duplicate_key_update_as_hash( table_name, arg )
15
+ elsif arg.is_a?( String )
16
+ sql << arg
15
17
  else
16
18
  raise ArgumentError.new( "Expected Array or Hash" )
17
19
  end
@@ -34,7 +36,12 @@ module ActiveRecord::Extensions::ConnectionAdapters::MysqlAdapter # :nodoc:
34
36
  "#{table_name}.#{qc1}=VALUES( #{qc2} )"
35
37
  end
36
38
  results.join( ',')
37
- end
39
+ end
40
+
41
+ #return true if the statement is a duplicate key record error
42
+ def duplicate_key_update_error?(exception)# :nodoc:
43
+ exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
44
+ end
38
45
 
39
46
  end
40
47
 
@@ -0,0 +1,178 @@
1
+ # Insert records in bulk with a select statement
2
+ #
3
+ # == Parameters
4
+ # * +options+ - the options used for the finder sql (select)
5
+ #
6
+ # === Options
7
+ # Any valid finder options (options for <tt>ActiveRecord::Base.find(:all)</tt> )such as <tt>:joins</tt>, <tt>:conditions</tt>, <tt>:include</tt>, etc including:
8
+ # * <tt>:from</tt> - the symbol, class name or class used for the finder SQL (select)
9
+ # * <tt>:on_duplicate_key_update</tt> - an array of fields to update, or a custom string
10
+ # * <tt>:select</tt> - An array of fields to select or custom string. The SQL will be sanitized and ? replaced with values as with <tt>:conditions</tt>.
11
+ # * <tt>:ignore => true </tt> - will ignore any duplicates
12
+ # * <tt>:into</tt> - Specifies the columns for which data will be inserted. An array of fields to select or custom string.
13
+ #
14
+ # == Examples
15
+ # Create cart items for all books for shopping cart <tt>@cart+
16
+ # setting the +copies+ field to 1, the +updated_at+ field to Time.now and the +created_at+ field to the database function now()
17
+ # CartItem.insert_select(:from => :book,
18
+ # :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
19
+ # :into => [:book_id, :shopping_cart_id, :copies, :updated_at, :created_at]})
20
+ #
21
+ # GENERATED SQL example (MySQL):
22
+ # INSERT INTO `cart_items` ( `book_id`, `shopping_cart_id`, `copies`, `updated_at`, `created_at` )
23
+ # SELECT books.id, '134', 1, '2009-03-02 18:28:25', now() FROM `books`
24
+ #
25
+ # A similar example that
26
+ # * uses the class +Book+ instead of symbol <tt>:book</tt>
27
+ # * a custom string (instead of an Array) for the <tt>:select</tt> of the +insert_options+
28
+ # * Updates the +updated_at+ field of all existing cart item. This assumes there is a unique composite index on the +book_id+ and +shopping_cart_id+ fields
29
+ #
30
+ # CartItem.insert_select(:from => Book,
31
+ # :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
32
+ # :into => 'cart_items.book_id, shopping_cart_id, copies, updated_at, created_at',
33
+ # :on_duplicate_key_update => [:updated_at])
34
+ # GENERATED SQL example (MySQL):
35
+ # INSERT INTO `cart_items` ( cart_items.book_id, shopping_cart_id, copies, updated_at, created_at )
36
+ # SELECT books.id, '138', 1, '2009-03-02 18:32:34', now() FROM `books`
37
+ # ON DUPLICATE KEY UPDATE `cart_items`.`updated_at`=VALUES(`updated_at`)
38
+ #
39
+ #
40
+ # Similar example ignoring duplicates
41
+ # CartItem.insert_select(:from => :book,
42
+ # :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
43
+ # :into => [:book_id, :shopping_cart_id, :copies, :updated_at, :created_at],
44
+ # :ignore => true)
45
+ #
46
+ # == Developers
47
+ # * Blythe Dunham http://blythedunham.com
48
+ #
49
+ # == Homepage
50
+ # * Project Site: http://www.continuousthinking.com/tags/arext
51
+ # * Rubyforge Project: http://rubyforge.org/projects/arext
52
+ # * Anonymous SVN: svn checkout svn://rubyforge.org/var/svn/arext
53
+ #
54
+
55
+ module ActiveRecord::Extensions::ConnectionAdapters; end
56
+
57
+ module ActiveRecord::Extensions::InsertSelectSupport #:nodoc:
58
+ def supports_insert_select? #:nodoc:
59
+ true
60
+ end
61
+ end
62
+
63
+ class ActiveRecord::Base
64
+
65
+ include ActiveRecord::Extensions::SqlGeneration
66
+
67
+ class << self
68
+ # Insert records in bulk with a select statement
69
+ #
70
+ # == Parameters
71
+ # * +options+ - the options used for the finder sql (select)
72
+ #
73
+ # === Options
74
+ # Any valid finder options (options for <tt>ActiveRecord::Base.find(:all)</tt> )such as <tt>:joins</tt>, <tt>:conditions</tt>, <tt>:include</tt>, etc including:
75
+ # * <tt>:from</tt> - the symbol, class name or class used for the finder SQL (select)
76
+ # * <tt>:on_duplicate_key_update</tt> - an array of fields to update, or a custom string
77
+ # * <tt>:select</tt> - An array of fields to select or custom string. The SQL will be sanitized and ? replaced with values as with <tt>:conditions</tt>.
78
+ # * <tt>:ignore => true </tt> - will ignore any duplicates
79
+ # * <tt>:into</tt> - Specifies the columns for which data will be inserted. An array of fields to select or custom string.
80
+ #
81
+ # == Examples
82
+ # Create cart items for all books for shopping cart <tt>@cart+
83
+ # setting the +copies+ field to 1, the +updated_at+ field to Time.now and the +created_at+ field to the database function now()
84
+ # CartItem.insert_select(:from => :book,
85
+ # :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
86
+ # :into => [:book_id, :shopping_cart_id, :copies, :updated_at, :created_at]})
87
+ #
88
+ # GENERATED SQL example (MySQL):
89
+ # INSERT INTO `cart_items` ( `book_id`, `shopping_cart_id`, `copies`, `updated_at`, `created_at` )
90
+ # SELECT books.id, '134', 1, '2009-03-02 18:28:25', now() FROM `books`
91
+ #
92
+ # A similar example that
93
+ # * uses the class +Book+ instead of symbol <tt>:book</tt>
94
+ # * a custom string (instead of an Array) for the <tt>:select</tt> of the +insert_options+
95
+ # * Updates the +updated_at+ field of all existing cart item. This assumes there is a unique composite index on the +book_id+ and +shopping_cart_id+ fields
96
+ #
97
+ # CartItem.insert_select(:from => Book,
98
+ # :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
99
+ # :into => 'cart_items.book_id, shopping_cart_id, copies, updated_at, created_at',
100
+ # :on_duplicate_key_update => [:updated_at])
101
+ # GENERATED SQL example (MySQL):
102
+ # INSERT INTO `cart_items` ( cart_items.book_id, shopping_cart_id, copies, updated_at, created_at )
103
+ # SELECT books.id, '138', 1, '2009-03-02 18:32:34', now() FROM `books`
104
+ # ON DUPLICATE KEY UPDATE `cart_items`.`updated_at`=VALUES(`updated_at`)
105
+ #
106
+ #
107
+ # Similar example ignoring duplicates
108
+ # CartItem.insert_select(:from => :book,
109
+ # :select => ['books.id, ?, ?, ?, now()', @cart.to_param, 1, Time.now],
110
+ # :into => [:book_id, :shopping_cart_id, :copies, :updated_at, :created_at],
111
+ # :ignore => true)
112
+ def insert_select(options={})
113
+ select_obj = options.delete(:from).to_s.classify.constantize
114
+ #TODO: add batch support for high volume inserts
115
+ #return insert_select_batch(select_obj, select_options, insert_options) if insert_options[:batch]
116
+ sql = construct_insert_select_sql(select_obj, options)
117
+ connection.insert(sql, "#{name} Insert Select #{select_obj}")
118
+ end
119
+
120
+ protected
121
+
122
+ def construct_insert_select_sql(select_obj, options)#:nodoc:
123
+ construct_ar_extension_sql(gather_insert_options(options), valid_insert_select_options) do |sql, into_op|
124
+ sql << " INTO #{quoted_table_name} "
125
+ sql << "( #{into_column_sql(options.delete(:into))} ) "
126
+
127
+ #sanitize the select sql based on the select object
128
+ sql << select_obj.send(:finder_sql_to_string, sanitize_select_options(options))
129
+ sql
130
+ end
131
+ end
132
+
133
+ # return a list of the column names quoted accordingly
134
+ # nil => All columns except primary key (auto update)
135
+ # String => Exact String
136
+ # Array
137
+ # needs sanitation ["?, ?", 5, 'test'] => "5, 'test'" or [":date", {:date => Date.today}] => "12-30-2006"]
138
+ # list of strings or symbols returns quoted values [:start, :name] => `start`, `name` or ['abc'] => `start`
139
+ def select_column_sql(field_list=nil)#:nodoc:
140
+ if field_list.kind_of?(String)
141
+ field_list.dup
142
+ elsif ((field_list.kind_of?(Array) && field_list.first.is_a?(String)) &&
143
+ (field_list.last.is_a?(Hash) || field_list.first.include?('?')))
144
+ sanitize_sql(field_list)
145
+ else
146
+ field_list = field_list.blank? ? self.column_names - [self.primary_key] : [field_list].flatten
147
+ field_list.collect{|field| self.connection.quote_column_name(field.to_s) }.join(", ")
148
+ end
149
+ end
150
+
151
+ alias_method :into_column_sql, :select_column_sql
152
+
153
+ #sanitize the select options for insert select
154
+ def sanitize_select_options(options)#:nodoc:
155
+ o = options.dup
156
+ select = o.delete :select
157
+ o[:override_select] = select ? select_column_sql(select) : ' * '
158
+ o
159
+ end
160
+
161
+
162
+ def valid_insert_select_options#:nodoc:
163
+ @@valid_insert_select_options ||= [:command, :into_pre, :into_post,
164
+ :into_keywords, :ignore,
165
+ :on_duplicate_key_update]
166
+ end
167
+
168
+ #move all the insert options to a seperate map
169
+ def gather_insert_options(options)#:nodoc:
170
+ into_options = valid_insert_select_options.inject(:command => 'INSERT') do |map, o|
171
+ v = options.delete(o)
172
+ map[o] = v if v
173
+ map
174
+ end
175
+ end
176
+
177
+ end
178
+ end
@@ -0,0 +1,7 @@
1
+ #insert select functionality is dependent on finder options and import
2
+ require 'ar-extensions/finder_options/mysql'
3
+ require 'ar-extensions/import/mysql'
4
+
5
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do
6
+ include ActiveRecord::Extensions::InsertSelectSupport
7
+ end
@@ -0,0 +1,204 @@
1
+ module ActiveRecord::Extensions::Union#:nodoc:
2
+ module UnionSupport #:nodoc:
3
+ def supports_union? #:nodoc:
4
+ true
5
+ end
6
+ end
7
+ end
8
+
9
+ class ActiveRecord::Base
10
+ supports_extension :union
11
+
12
+ extend ActiveRecord::Extensions::SqlGeneration
13
+ class << self
14
+ # Find a union of two or more queries
15
+ # === Args
16
+ # Each argument is a hash map of options sent to <tt>:find :all</tt>
17
+ # including <tt>:conditions</tt>, <tt>:join</tt>, <tt>:group</tt>,
18
+ # <tt>:having</tt>, and <tt>:limit</tt>
19
+ #
20
+ # In addition the following options are accepted
21
+ # * <tt>:pre_sql</tt> inserts SQL before the SELECT statement of this protion of the +union+
22
+ # * <tt>:post_sql</tt> appends additional SQL to the end of the statement
23
+ # * <tt>:override_select</tt> is used to override the <tt>SELECT</tt> clause of eager loaded associations
24
+ #
25
+ # == Examples
26
+ # Find the union of a San Fran zipcode with a Seattle zipcode
27
+ # union_args1 = {:conditions => ['zip_id = ?', 94010], :select => :phone_number_id}
28
+ # union_args2 = {:conditions => ['zip_id = ?', 98102], :select => :phone_number_id}
29
+ # Contact.find_union(union_args1, union_args2, ...)
30
+ #
31
+ # SQL> (SELECT phone_number_id FROM contacts WHERE zip_id = 94010) UNION
32
+ # (SELECT phone_number_id FROM contacts WHERE zip_id = 98102) UNION ...
33
+ #
34
+ # == Global Options
35
+ # To specify global options that apply to the entire union, specify a hash as the
36
+ # first parameter with a key <tt>:union_options</tt>. Valid options include
37
+ # <tt>:group</tt>, <tt>:having</tt>, <tt>:order</tt>, and <tt>:limit</tt>
38
+ #
39
+ #
40
+ # Example:
41
+ # Contact.find_union(:union_options => {:limit => 10, :order => 'created_on'},
42
+ # union_args1, union_args2, ...)
43
+ #
44
+ # SQL> ((select phone_number_id from contacts ...) UNION (select phone_number_id from contacts ...)) order by created_on limit 10
45
+ #
46
+ def find_union(*args)
47
+ supports_union!
48
+ find_by_sql(find_union_sql(*args))
49
+ end
50
+
51
+ # Count across a union of two or more queries
52
+ # === Args
53
+ # * +column_name+ - The column to count. Defaults to all ('*')
54
+ # * <tt>*args</tt> - Each additional argument is a hash map of options used by <tt>:find :all</tt>
55
+ # including <tt>:conditions</tt>, <tt>:join</tt>, <tt>:group</tt>,
56
+ # <tt>:having</tt>, and <tt>:limit</tt>
57
+ #
58
+ # In addition the following options are accepted
59
+ # * <tt>:pre_sql</tt> inserts SQL before the SELECT statement of this protion of the +union+
60
+ # * <tt>:post_sql</tt> appends additional SQL to the end of the statement
61
+ # * <tt>:override_select</tt> is used to override the <tt>SELECT</tt> clause of eager loaded associations
62
+ #
63
+ # Note that distinct is implied so a record that matches more than one
64
+ # portion of the union is counted only once.
65
+ #
66
+ # == Global Options
67
+ # To specify global options that apply to the entire union, specify a hash as the
68
+ # first parameter with a key <tt>:union_options</tt>. Valid options include
69
+ # <tt>:group</tt>, <tt>:having</tt>, <tt>:order</tt>, and <tt>:limit</tt>
70
+ #
71
+ # == Examples
72
+ # Count the number of people who live in Seattle and San Francisco
73
+ # Contact.count_union(:phone_number_id,
74
+ # {:conditions => ['zip_id = ?, 94010]'},
75
+ # {:conditions => ['zip_id = ?', 98102]})
76
+ # SQL> select count(*) from ((select phone_number_id from contacts ...) UNION (select phone_number_id from contacts ...)) as counter_tbl;
77
+ def count_union(column_name, *args)
78
+ supports_union!
79
+ count_val = calculate_union(:count, column_name, *args)
80
+ (args.length == 1 && args.first[:limit] && args.first[:limit].to_i < count_val) ? args.first[:limit].to_i : count_val
81
+ end
82
+
83
+ protected
84
+
85
+ #do a union of specified calculation. Only for simple calculations
86
+ def calculate_union(operation, column_name, *args)#:nodoc:
87
+ union_options = remove_union_options(args)
88
+
89
+
90
+ if args.length == 1
91
+ column_name = '*' if column_name == :all
92
+ calculate(operation, column_name, args.first.update(union_options))
93
+
94
+ # For more than one map of options, count off the subquery of all the column_name fields unioned together
95
+ # For example, if column_name is phone_number_id the generated query is
96
+ # Contact.calculate_union(:count, :phone_number_id, args)
97
+ # SQL> select count(*) from
98
+ # ((select phone_number_id from contacts ...)
99
+ # UNION
100
+ # (select phone_number_id from contacts ...)) as counter_tbl
101
+ else
102
+ column_name = primary_key if column_name == :all
103
+ column = column_for column_name
104
+ column_name = "#{table_name}.#{column_name}" unless column_name.to_s.include?('.')
105
+
106
+ group_by = union_options.delete(:group)
107
+ having = union_options.delete(:having)
108
+ query_alias = union_options.delete(:query_alias)||"#{operation}_giraffe"
109
+
110
+
111
+ #aggregate_alias should be table_name_id
112
+ aggregate_alias = column_alias_for('', column_name)
113
+ #main alias is operation_table_name_id
114
+ main_aggregate_alias = column_alias_for(operation, column_name)
115
+
116
+ sql = "SELECT "
117
+ sql << (group_by ? "#{group_by}, #{operation}(#{aggregate_alias})" : "#{operation}(*)")
118
+ sql << " AS #{main_aggregate_alias}"
119
+ sql << " FROM ("
120
+
121
+ #by nature of the union the results will always be distinct, so remove distinct column here
122
+ sql << args.inject([]){|l, a|
123
+ calc = "(#{construct_calculation_sql_with_extension('', column_name, a)})"
124
+ #for group by we need to select the group by column also
125
+ calc.gsub!(" AS #{aggregate_alias}", " AS #{aggregate_alias}, #{group_by} ") if group_by
126
+ l << calc
127
+ }.join(" UNION ")
128
+
129
+ add_union_options!(sql, union_options)
130
+
131
+ sql << ") as #{query_alias}"
132
+
133
+ if group_by
134
+ #add groupings
135
+ sql << " GROUP BY #{group_by}"
136
+ sql << " HAVING #{having}" if having
137
+
138
+ calculated_data = connection.select_all(sql)
139
+
140
+ calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row|
141
+ key = type_cast_calculated_value(row[group_by], column_for(group_by.to_s))
142
+ value = row[main_aggregate_alias]
143
+ all << [key, type_cast_calculated_value(value, column_for(column), operation)]
144
+ end
145
+
146
+ else
147
+ count_by_sql(sql)
148
+ end
149
+ end
150
+ end
151
+
152
+
153
+ #Add Global Union options
154
+ def add_union_options!(sql, options)#:nodoc:
155
+ sql << " GROUP BY #{options[:group]} " if options[:group]
156
+
157
+ if options[:order] || options[:limit]
158
+ scope = scope(:find)
159
+ add_order!(sql, options[:order], scope)
160
+ add_limit!(sql, options, scope)
161
+ end
162
+ sql
163
+ end
164
+
165
+ #Remove the global union options
166
+ def remove_union_options(args)#:nodoc:
167
+ args.first.is_a?(Hash) && args.first.has_key?(:union_options) ? (args.shift)[:union_options] : {}
168
+ end
169
+
170
+ def construct_calculation_sql_with_extension(operation, column_name, options)
171
+ construct_ar_extension_sql(options.merge(:command => '', :keywords => nil, :distinct => nil)) {|sql, o|
172
+ calc_sql = construct_calculation_sql(operation, column_name, options)
173
+
174
+ #this is really gross but prevents us from rewriting construct_calculation_sql
175
+ calc_sql.gsub!(/^SELECT\s/, "SELECT #{options[:keywords]} ") if options[:keywords]
176
+
177
+ sql << calc_sql
178
+ }
179
+ end
180
+
181
+ # Return the sql for union of the query options specified on the command line
182
+ # If the first parameter is a map containing :union_options, use these
183
+ def find_union_sql(*args)#:nodoc:
184
+ options = remove_union_options(args)
185
+
186
+ if args.length == 1
187
+ return finder_sql_to_string(args.first.update(options))
188
+ end
189
+
190
+ sql = args.inject([]) do |sql_list, union_args|
191
+ part = union_args.merge(:force_eager_load => true,
192
+ :override_select => union_args[:select]||"#{quoted_table_name}.*",
193
+ :select => nil)
194
+ sql_list << "(#{finder_sql_to_string(part)})"
195
+ sql_list
196
+ end.join(" UNION ")
197
+
198
+
199
+ add_union_options!(sql, options)
200
+ sql
201
+ end
202
+ end
203
+ end
204
+