ar-extensions 0.5.1

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