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