ar-extensions 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,76 @@
1
+ module ActiveRecord::ConnectionAdapters::Quoting
2
+
3
+ alias :quote_orig :quote
4
+ def quote( value, column=nil ) # :nodoc:
5
+ if value.is_a?( Regexp )
6
+ "'#{value.inspect[1...-1]}'"
7
+ else
8
+ quote_orig( value, column )
9
+ end
10
+ end
11
+ end
12
+
13
+
14
+ class ActiveRecord::Base
15
+
16
+ class << self
17
+
18
+ private
19
+
20
+ alias :sanitize_sql_orig :sanitize_sql
21
+ def sanitize_sql( arg ) # :nodoc:
22
+ return sanitize_sql_orig( arg ) if arg.nil?
23
+ if arg.respond_to?( :to_sql )
24
+ arg = sanitize_sql_by_way_of_duck_typing( arg ) #if arg.respond_to?( :to_sql )
25
+ elsif arg.is_a?( Hash )
26
+ arg = sanitize_sql_from_hash( arg ) #if arg.is_a?( Hash )
27
+ elsif arg.size == 2 and arg.first.is_a?( String ) and arg.last.is_a?( Hash )
28
+ arg = sanitize_sql_from_string_and_hash( arg ) # if arg.size == 2 and arg.first.is_a?( String ) and arg.last.is_a?( Hash )
29
+ end
30
+ sanitize_sql_orig( arg )
31
+ end
32
+
33
+ def sanitize_sql_by_way_of_duck_typing( arg ) #: nodoc:
34
+ arg.to_sql( caller )
35
+ end
36
+
37
+ def sanitize_sql_from_string_and_hash( arr ) # :nodoc:
38
+ return arr if arr.first =~ /\:[\w]+/
39
+ arr2 = sanitize_sql_from_hash( arr.last )
40
+ if arr2.empty?
41
+ conditions = arr.first
42
+ else
43
+ conditions = [ arr.first << " AND (#{arr2.first})" ]
44
+ conditions.push( *arr2[1..-1] )
45
+ end
46
+ conditions
47
+ end
48
+
49
+ def sanitize_sql_from_hash( hsh ) #:nodoc:
50
+ conditions, values = [], []
51
+
52
+ hsh.each_pair do |key,val|
53
+ if val.respond_to?( :to_sql )
54
+ conditions << sanitize_sql_by_way_of_duck_typing( val )
55
+ next
56
+ else
57
+ sql = nil
58
+ result = ActiveRecord::Extensions.process( key, val, self )
59
+ if result
60
+ conditions << result.sql if result.sql
61
+ values.push( result.value ) if result.value
62
+ else
63
+ conditions << "#{table_name}.#{connection.quote_column_name(key.to_s)} #{attribute_condition( val )} "
64
+ values << val
65
+ end
66
+ end
67
+ end
68
+
69
+ conditions = conditions.join( ' AND ' )
70
+ return [] if conditions.size == 1 and conditions.first.empty?
71
+ [ conditions, *values ]
72
+ end
73
+
74
+ end
75
+
76
+ end
@@ -0,0 +1,70 @@
1
+ # Enables support for enabling and disabling foreign keys
2
+ # for the underlyig database connection for ActiveRecord.
3
+ #
4
+ # This can be used with or without block form. This also
5
+ # uses the connection attached to the model.
6
+ #
7
+ # ==== Example 1, without block form
8
+ # Project.foreign_keys.disable
9
+ # Project.foreign_keys.enable
10
+ #
11
+ # If you use this form you have to manually re-enable the foreign
12
+ # keys.
13
+ #
14
+ # ==== Example 2, with block form
15
+ # Project.foreign_keys.disable do
16
+ # # ...
17
+ # end
18
+ #
19
+ # Project.foreign_keys.enable do
20
+ # # ...
21
+ # end
22
+ #
23
+ # If you use the block form the foreign keys are automatically
24
+ # enabled or disabled when the block exits. This currently
25
+ # does not restore the state of foreign keys to the state before
26
+ # the block was entered.
27
+ #
28
+ # Note: If you use the disable block foreign keys
29
+ # will be enabled after the block exits. If you use the enable block foreign keys
30
+ # will be disabled after the block exits.
31
+ #
32
+ # TODO: check the external state and restore that state when using block form.
33
+ module ActiveRecord::Extensions::ForeignKeys
34
+
35
+ class ForeignKeyController # :nodoc:
36
+ attr_reader :clazz
37
+
38
+ def initialize( clazz )
39
+ @clazz = clazz
40
+ end
41
+
42
+ def disable # :nodoc:
43
+ if block_given?
44
+ disable
45
+ yield
46
+ enable
47
+ else
48
+ clazz.connection.execute "set foreign_key_checks = 0"
49
+ end
50
+ end
51
+
52
+ def enable #:nodoc:
53
+ if block_given?
54
+ enable
55
+ yield
56
+ disable
57
+ else
58
+ clazz.connection.execute "set foreign_key_checks = 1"
59
+ end
60
+ end
61
+
62
+ end #end ForeignKeyController
63
+
64
+ def foreign_keys # :nodoc:
65
+ ForeignKeyController.new( self )
66
+ end
67
+
68
+ end
69
+
70
+ ActiveRecord::Base.extend( ActiveRecord::Extensions::ForeignKeys )
@@ -0,0 +1,63 @@
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
+
55
+ end
56
+
57
+ end
58
+
59
+
60
+
61
+
62
+
63
+
@@ -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
+ class ActiveRecord::ConnectionAdapters::MysqlAdapter # :nodoc:
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,254 @@
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
+ # Returns true if the current database connection adapter
23
+ # supports import functionality, otherwise returns false.
24
+ def supports_import?
25
+ connection.supports_import?
26
+ rescue NoMethodError
27
+ false
28
+ end
29
+
30
+ # Returns true if the current database connection adapter
31
+ # supports on duplicate key update functionality, otherwise
32
+ # returns false.
33
+ def supports_on_duplicate_key_update?
34
+ connection.supports_on_duplicate_key_update?
35
+ rescue NoMethodError
36
+ false
37
+ end
38
+
39
+ # Imports a collection of values to the database.
40
+ #
41
+ # This is more efficient than using ActiveRecord::Base#create or
42
+ # ActiveRecord::Base#save multiple times. This method works well if
43
+ # you want to create more than one record at a time and do not care
44
+ # about having ActiveRecord objects returned for each record
45
+ # inserted.
46
+ #
47
+ # This can be used with or without validations. It does not utilize
48
+ # the ActiveRecord::Callbacks during creation/modification while
49
+ # performing the import.
50
+ #
51
+ # == Usage
52
+ # Model.import array_of_models
53
+ # Model.import column_names, array_of_values
54
+ # Model.import column_names, array_of_values, options
55
+ #
56
+ # ==== Model.import array_of_models
57
+ #
58
+ # With this form you can call _import_ passing in an array of model
59
+ # objects that you want updated.
60
+ #
61
+ # ==== Model.import column_names, array_of_values
62
+ #
63
+ # The first parameter +column_names+ is an array of symbols or
64
+ # strings which specify the columns that you want to update.
65
+ #
66
+ # The second parameter, +array_of_values+, is an array of
67
+ # arrays. Each subarray is a single set of values for a new
68
+ # record. The order of values in each subarray should match up to
69
+ # the order of the +column_names+.
70
+ #
71
+ # ==== Model.import column_names, array_of_values, options
72
+ #
73
+ # The first two parameters are the same as the above form. The third
74
+ # parameter, +options+, is a hash. This is optional. Please see
75
+ # below for what +options+ are available.
76
+ #
77
+ # == Options
78
+ # * +validate+ - true|false, tells import whether or not to use \
79
+ # ActiveRecord validations. Validations are enforced by default.
80
+ # * +on_duplicate_key_update+ - an Array or Hash, tells import to \
81
+ # use MySQL's ON DUPLICATE KEY UPDATE ability. See On Duplicate\
82
+ # Key Update below.
83
+ #
84
+ # == Examples
85
+ # class BlogPost < ActiveRecord::Base ; end
86
+ #
87
+ # # Example using array of model objects
88
+ # posts = [ BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT',
89
+ # BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT2',
90
+ # BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT3' ]
91
+ # BlogPost.import posts
92
+ #
93
+ # # Example using column_names and array_of_values
94
+ # columns = [ :author_name, :title ]
95
+ # values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
96
+ # BlogPost.import columns, values
97
+ #
98
+ # # Example using column_names, array_of_value and options
99
+ # columns = [ :author_name, :title ]
100
+ # values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
101
+ # BlogPost.import( columns, values, :validate => false )
102
+ #
103
+ # == On Duplicate Key Update (MySQL only)
104
+ #
105
+ # The :on_duplicate_key_update option can be either an Array or a Hash.
106
+ #
107
+ # ==== Using an Array
108
+ #
109
+ # The :on_duplicate_key_update option can be an array of column
110
+ # names. The column names are the only fields that are updated if
111
+ # a duplicate record is found. Below is an example:
112
+ #
113
+ # BlogPost.import columns, values, :on_duplicate_key_update=>[ :date_modified, :content, :author ]
114
+ #
115
+ # ==== Using A Hash
116
+ #
117
+ # The :on_duplicate_key_update option can be a hash of column name
118
+ # to model attribute name mappings. This gives you finer grained
119
+ # control over what fields are updated with what attributes on your
120
+ # model. Below is an example:
121
+ #
122
+ # BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :title => :title }
123
+ #
124
+ def import( *args )
125
+ options = { :validate=>true }
126
+ options.merge!( args.pop ) if args.last.is_a? Hash
127
+
128
+ # assume array of model objects
129
+ if args.last.is_a?( Array ) and args.last.first.is_a? ActiveRecord::Base
130
+ if args.length == 2
131
+ models = args.last
132
+ column_names = args.first
133
+ else
134
+ models = args.first
135
+ column_names = self.column_names.dup
136
+ column_names.delete( self.primary_key ) unless options[ :on_duplicate_key_update ]
137
+ end
138
+
139
+ array_of_attributes = models.inject( [] ) do |arr,model|
140
+ attributes = []
141
+ column_names.each do |name|
142
+ attributes << model.send( "#{name}_before_type_cast" )
143
+ end
144
+ arr << attributes
145
+ end
146
+ # supports 2-element array and array
147
+ elsif args.size == 2 and args.first.is_a?( Array ) and args.last.is_a?( Array )
148
+ column_names, array_of_attributes = args
149
+ else
150
+ raise ArgumentError.new( "Invalid arguments!" )
151
+ end
152
+
153
+ is_validating = options.delete( :validate )
154
+
155
+ # dup the passed in array so we don't modify it unintentionally
156
+ array_of_attributes = array_of_attributes.dup
157
+ if is_validating
158
+ import_with_validations( column_names, array_of_attributes, options )
159
+ else
160
+ import_without_validations_or_callbacks( column_names, array_of_attributes, options )
161
+ end
162
+ end
163
+
164
+ # TODO import_from_table needs to be implemented.
165
+ def import_from_table( options ) # :nodoc:
166
+ end
167
+
168
+ # Imports the passed in +column_names+ and +array_of_attributes+
169
+ # given the passed in +options+ Hash with validations. Returns an
170
+ # array of instances that failed validations. See
171
+ # ActiveRecord::Base.import for more information on
172
+ # +column_names+, +array_of_attributes+ and +options+.
173
+ def import_with_validations( column_names, array_of_attributes, options={} )
174
+ failed_instances = []
175
+
176
+ # create instances for each of our column/value sets
177
+ arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
178
+
179
+ # keep track of the instance and the position it is currently at. if this fails
180
+ # validation we'll use the index to remove it from the array_of_attributes
181
+ arr.each_with_index do |hsh,i|
182
+ instance = new( hsh )
183
+ if not instance.valid?
184
+ array_of_attributes[ i ] = nil
185
+ failed_instances << instance
186
+ end
187
+ end
188
+ array_of_attributes.compact!
189
+
190
+ if not array_of_attributes.empty?
191
+ import_without_validations_or_callbacks( column_names, array_of_attributes, options )
192
+ end
193
+ failed_instances
194
+ end
195
+
196
+ # Imports the passed in +column_names+ and +array_of_attributes+
197
+ # given the passed in +options+ Hash. This will return the number
198
+ # of insert operations it took to create these records without
199
+ # validations or callbacks. See ActiveRecord::Base.import for more
200
+ # information on +column_names+, +array_of_attributes_ and
201
+ # +options+.
202
+ def import_without_validations_or_callbacks( column_names, array_of_attributes, options={} )
203
+ escaped_column_names = quote_column_names( column_names )
204
+ columns = []
205
+ array_of_attributes.first.each_with_index { |arr,i| columns << columns_hash[ column_names[i] ] }
206
+
207
+ if not supports_import?
208
+ columns_sql = "(" + escaped_column_names.join( ',' ) + ")"
209
+ insert_statements, values = [], []
210
+ array_of_attributes.each do |arr|
211
+ my_values = []
212
+ arr.each_with_index do |val,j|
213
+ my_values << connection.quote( val, columns[j] )
214
+ end
215
+ insert_statements << "INSERT INTO #{self.table_name} #{columns_sql} VALUES(" + my_values.join( ',' ) + ")"
216
+ connection.execute( insert_statements.last )
217
+ end
218
+ return
219
+ else
220
+
221
+ # generate the sql
222
+ insert_sql = connection.multiple_value_sets_insert_sql( table_name, escaped_column_names, options )
223
+ values_sql = connection.values_sql_for_column_names_and_attributes( columns, array_of_attributes )
224
+ post_sql_statements = connection.post_sql_statements( table_name, options )
225
+
226
+ # perform the inserts
227
+ number_of_inserts = connection.insert_many(
228
+ [ insert_sql, post_sql_statements ].flatten,
229
+ values_sql,
230
+ "#{self.class.name} Create Many Without Validations Or Callbacks" )
231
+ end
232
+ end
233
+
234
+ # Returns an array of quoted column names
235
+ def quote_column_names( names )
236
+ names.map{ |name| connection.quote_column_name( name ) }
237
+ end
238
+
239
+
240
+ private
241
+
242
+ # Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
243
+ def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
244
+ arr = []
245
+ array_of_attributes.each do |attributes|
246
+ c = 0
247
+ hsh = attributes.inject( {} ){|hsh,attr| hsh[ column_names[c] ] = attr ; c+=1 ; hsh }
248
+ arr << hsh
249
+ end
250
+ arr
251
+ end
252
+
253
+ end
254
+ end