jorahood-ar-extensions 0.9.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/ChangeLog +145 -0
  2. data/README +167 -0
  3. data/Rakefile +79 -0
  4. data/config/database.yml +7 -0
  5. data/config/database.yml.template +7 -0
  6. data/config/mysql.schema +72 -0
  7. data/config/postgresql.schema +39 -0
  8. data/db/migrate/generic_schema.rb +96 -0
  9. data/db/migrate/mysql_schema.rb +31 -0
  10. data/db/migrate/oracle_schema.rb +5 -0
  11. data/db/migrate/version.rb +4 -0
  12. data/init.rb +31 -0
  13. data/lib/ar-extensions/create_and_update.rb +509 -0
  14. data/lib/ar-extensions/csv.rb +309 -0
  15. data/lib/ar-extensions/delete.rb +143 -0
  16. data/lib/ar-extensions/extensions.rb +506 -0
  17. data/lib/ar-extensions/finder_options.rb +275 -0
  18. data/lib/ar-extensions/finders.rb +94 -0
  19. data/lib/ar-extensions/foreign_keys.rb +70 -0
  20. data/lib/ar-extensions/fulltext.rb +62 -0
  21. data/lib/ar-extensions/import.rb +352 -0
  22. data/lib/ar-extensions/insert_select.rb +178 -0
  23. data/lib/ar-extensions/synchronize.rb +30 -0
  24. data/lib/ar-extensions/temporary_table.rb +124 -0
  25. data/lib/ar-extensions/union.rb +204 -0
  26. data/lib/ar-extensions/version.rb +9 -0
  27. data/tests/connections/native_mysql/connection.rb +16 -0
  28. data/tests/connections/native_oracle/connection.rb +16 -0
  29. data/tests/connections/native_postgresql/connection.rb +19 -0
  30. data/tests/connections/native_sqlite/connection.rb +14 -0
  31. data/tests/connections/native_sqlite3/connection.rb +14 -0
  32. data/tests/fixtures/addresses.yml +25 -0
  33. data/tests/fixtures/books.yml +46 -0
  34. data/tests/fixtures/developers.yml +20 -0
  35. data/tests/fixtures/unit/active_record_base_finders/addresses.yml +25 -0
  36. data/tests/fixtures/unit/active_record_base_finders/books.yml +64 -0
  37. data/tests/fixtures/unit/active_record_base_finders/developers.yml +20 -0
  38. data/tests/fixtures/unit/synchronize/books.yml +16 -0
  39. data/tests/fixtures/unit/to_csv_headers/addresses.yml +8 -0
  40. data/tests/fixtures/unit/to_csv_headers/developers.yml +6 -0
  41. data/tests/fixtures/unit/to_csv_with_common_options/addresses.yml +40 -0
  42. data/tests/fixtures/unit/to_csv_with_common_options/developers.yml +13 -0
  43. data/tests/fixtures/unit/to_csv_with_common_options/languages.yml +29 -0
  44. data/tests/fixtures/unit/to_csv_with_common_options/teams.yml +3 -0
  45. data/tests/fixtures/unit/to_csv_with_default_options/developers.yml +7 -0
  46. data/tests/models/address.rb +4 -0
  47. data/tests/models/animal.rb +2 -0
  48. data/tests/models/book.rb +3 -0
  49. data/tests/models/cart_item.rb +4 -0
  50. data/tests/models/developer.rb +8 -0
  51. data/tests/models/group.rb +3 -0
  52. data/tests/models/language.rb +5 -0
  53. data/tests/models/mysql/book.rb +3 -0
  54. data/tests/models/mysql/test_innodb.rb +3 -0
  55. data/tests/models/mysql/test_memory.rb +3 -0
  56. data/tests/models/mysql/test_myisam.rb +3 -0
  57. data/tests/models/project.rb +2 -0
  58. data/tests/models/shopping_cart.rb +4 -0
  59. data/tests/models/team.rb +4 -0
  60. data/tests/models/topic.rb +13 -0
  61. data/tests/mysql/test_create_and_update.rb +290 -0
  62. data/tests/mysql/test_delete.rb +142 -0
  63. data/tests/mysql/test_finder_options.rb +121 -0
  64. data/tests/mysql/test_finders.rb +29 -0
  65. data/tests/mysql/test_import.rb +354 -0
  66. data/tests/mysql/test_insert_select.rb +173 -0
  67. data/tests/mysql/test_mysql_adapter.rb +45 -0
  68. data/tests/mysql/test_union.rb +81 -0
  69. data/tests/oracle/test_adapter.rb +14 -0
  70. data/tests/postgresql/test_adapter.rb +14 -0
  71. metadata +147 -0
@@ -0,0 +1,30 @@
1
+ module ActiveRecord # :nodoc:
2
+ class Base # :nodoc:
3
+
4
+ # Synchronizes the passed in ActiveRecord instances with data
5
+ # from the database. This is like calling reload
6
+ # on an individual ActiveRecord instance but it is intended for use on
7
+ # multiple instances.
8
+ #
9
+ # This uses one query for all instance updates and then updates existing
10
+ # instances rather sending one query for each instance
11
+ def self.synchronize(instances, key=self.primary_key)
12
+ return if instances.empty?
13
+
14
+ keys = instances.map(&"#{key}".to_sym)
15
+ klass = instances.first.class
16
+ fresh_instances = klass.find( :all, :conditions=>{ key=>keys }, :order=>"#{key} ASC" )
17
+
18
+ instances.each_with_index do |instance, index|
19
+ instance.clear_aggregation_cache
20
+ instance.clear_association_cache
21
+ instance.instance_variable_set '@attributes', fresh_instances[index].attributes
22
+ end
23
+ end
24
+
25
+ # See ActiveRecord::ConnectionAdapters::AbstractAdapter.synchronize
26
+ def synchronize(instances, key=ActiveRecord::Base.primary_key)
27
+ self.class.synchronize(instances, key)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,124 @@
1
+
2
+ module ActiveRecord::Extensions::TemporaryTableSupport # :nodoc:
3
+ def supports_temporary_tables? #:nodoc:
4
+ true
5
+ end
6
+ end
7
+
8
+
9
+ class ActiveRecord::Base
10
+ @@temporary_table_hsh ||= {}
11
+
12
+ # Returns true if the underlying database connection supports temporary tables
13
+ def self.supports_temporary_tables?
14
+ connection.supports_temporary_tables?
15
+ rescue NoMethodError
16
+ false
17
+ end
18
+
19
+ ######################################################################
20
+ # Creates a temporary table given the passed in options hash. The
21
+ # temporary table is created based off from another table the
22
+ # current model class. This method returns the constant for the new
23
+ # new model. This can also be used with block form (see below).
24
+ #
25
+ # == Parameters
26
+ # * options - the options hash used to define the temporary table.
27
+ #
28
+ # ==== Options
29
+ # * :table_name - the desired name of the temporary table. If not supplied \
30
+ # then a name of "temp_" + the current table_name of the current model \
31
+ # will be used.
32
+ # * :like - the table model you want to base the temporary tables \
33
+ # structure off from. If this is not supplied then the table_name of the \
34
+ # current model will be used.
35
+ # * :model_name - the name of the model you want to use for the temporary \
36
+ # table. This must be compliant with Ruby's naming conventions for \
37
+ # constants. If this is not supplied a rails-generated table name will \
38
+ # be created which is based off from the table_name of the temporary table. \
39
+ # IE: Account.create_temporary_table creates the TempAccount model class
40
+ #
41
+ # ==== Example 1, using defaults
42
+ # class Project < ActiveRecord::Base ; end
43
+ #
44
+ # Project.create_temporary_table
45
+ #
46
+ # This creates a temporary table named 'temp_projects' and creates a constant
47
+ # name TempProject. The table structure is copied from the _projects_ table.
48
+ #
49
+ # ==== Example 2, using :table_name and :model options
50
+ # Project.create_temporary_table :table_name=>'my_projects', :model=>'MyProject'
51
+ #
52
+ # This creates a temporary table named 'my_projects' and creates a constant named
53
+ # MyProject. The table structure is copied from the _projects_ table.
54
+ #
55
+ # ==== Example 3, using :like
56
+ # ActiveRecord::Base.create_temporary_table :like=>Project
57
+ #
58
+ # This is the same as calling Project.create_temporary_table.
59
+ #
60
+ # ==== Example 4, using block form
61
+ # Project.create_temporary_table do |t|
62
+ # # ...
63
+ # end
64
+ #
65
+ # Using the block form will automatically drop the temporary table
66
+ # when the block exits. _t_ which is passed into the block is the temporary
67
+ # table class. In the above example _t_ equals TempProject. The block form
68
+ # can be used with all of the available options.
69
+ #
70
+ # === See
71
+ # * drop
72
+ ######################################################################
73
+ def self.create_temporary_table( options={} )
74
+ options[:table_name] = "temp_#{self.table_name}" unless options[:table_name]
75
+ options[:like] = self unless options[:like]
76
+ options[:temporary] = true if not options[:permanent] and not options.has_key?( :temporary )
77
+ table_name = options[:table_name]
78
+ model_name = options[:model_name] || ActiveSupport::Inflector.classify( table_name )
79
+ raise Exception.new( "Model #{model_name} already exists! \n" ) if Object.const_defined? model_name
80
+
81
+ like_table_name = options[:like].table_name || self.table_name
82
+ sql = "CREATE #{options[:temporary] ? 'TEMPORARY' : ''} TABLE #{table_name} LIKE #{like_table_name}"
83
+ connection.execute( sql )
84
+
85
+ eval "class ::#{model_name} < #{ActiveRecord::TemporaryTable.name}
86
+ set_table_name :#{table_name}
87
+ end"
88
+
89
+ @@temporary_table_hsh[ model = Object.const_get( model_name ) ] = true
90
+ if block_given?
91
+ yield model
92
+ model.drop
93
+ nil
94
+ else
95
+ model
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+ class ActiveRecord::TemporaryTable < ActiveRecord::Base
102
+
103
+ # Drops a temporary table from the database and removes
104
+ # the temporary table constant.
105
+ #
106
+ # ==== Example
107
+ # Project.create_temporary_table
108
+ # Object.const_defined?( :TempProject ) # => true
109
+ # TempProject.drop
110
+ # Object.const_defined?( :TempProject ) # => false
111
+ #
112
+ def self.drop
113
+ if @@temporary_table_hsh[ self ]
114
+ sql = 'DROP TABLE ' + self.table_name + ';'
115
+ connection.execute( sql )
116
+ Object.send( :remove_const, self.name.to_sym )
117
+ @@temporary_table_hsh.delete( self )
118
+ else
119
+ raise StandardError.new( "Trying to drop nonexistance temporary table: #{self.name}" )
120
+ end
121
+ end
122
+
123
+ end
124
+
@@ -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
+
@@ -0,0 +1,9 @@
1
+
2
+ module ActiveRecord # :nodoc:
3
+ module Extensions # :nodoc:
4
+ module VERSION
5
+ MAJOR, MINOR, REVISION = %W( 0 9 2 )
6
+ STRING = [ MAJOR, MINOR, REVISION ].join( '.' )
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ print "Using native MySQL\n"
2
+ #require_dependency 'fixtures/course'
3
+ #require 'logger'
4
+
5
+ ActiveRecord::Base.logger = Logger.new("debug.log")
6
+
7
+ db1 = 'aroptests'
8
+
9
+ config = ActiveRecord::Base.configurations['test'] = { :adapter => "mysql",
10
+ :username => "zdennis",
11
+ :encoding => "utf8",
12
+ :host => '127.0.0.1',
13
+ :database => db1 }
14
+
15
+ ActiveRecord::Base.establish_connection( config )
16
+
@@ -0,0 +1,16 @@
1
+ print "Using native Oracle\n"
2
+ #require_dependency 'fixtures/course'
3
+ #require 'logger'
4
+
5
+ ActiveRecord::Base.logger = Logger.new("debug.log")
6
+
7
+ config = ActiveRecord::Base.configurations['test'] = { :adapter => "oracle",
8
+ :username => "arext_development",
9
+ :password => "arext",
10
+ :database => "activerecord_unittest",
11
+ :min_messages => "debug" }
12
+
13
+ ActiveRecord::Base.establish_connection( config )
14
+
15
+
16
+
@@ -0,0 +1,19 @@
1
+ print "Using native PostgreSQL\n"
2
+ #require_dependency 'fixtures/course'
3
+ #require 'logger'
4
+
5
+ ActiveRecord::Base.logger = Logger.new("debug.log")
6
+
7
+ db1 = 'aroptests'
8
+
9
+ config = ActiveRecord::Base.configurations['test'] = { :adapter => "postgresql",
10
+ :username => "postgres",
11
+ :password => "password",
12
+ :host => 'localhost',
13
+ :database => db1,
14
+ :min_messages => "warning" }
15
+
16
+ ActiveRecord::Base.establish_connection( config )
17
+
18
+
19
+
@@ -0,0 +1,14 @@
1
+ print "Using native Sqlite\n"
2
+ #require_dependency 'fixtures/course'
3
+ #require 'logger'
4
+
5
+ ActiveRecord::Base.logger = Logger.new("debug.log")
6
+
7
+ db1 = 'aroptests'
8
+ dbfile = File.join( File.dirname( __FILE__ ), 'test.db' )
9
+
10
+ config = ActiveRecord::Base.configurations['test'] = { :adapter => "sqlite",
11
+ :dbfile => dbfile }
12
+
13
+
14
+ ActiveRecord::Base.establish_connection( config )
@@ -0,0 +1,14 @@
1
+ print "Using native Sqlite3\n"
2
+ #require_dependency 'fixtures/course'
3
+ #require 'logger'
4
+
5
+ ActiveRecord::Base.logger = Logger.new("debug.log")
6
+
7
+ db1 = 'aroptests'
8
+ dbfile = File.join( File.dirname( __FILE__ ), 'test.db' )
9
+
10
+ config = ActiveRecord::Base.configurations['test'] = { :adapter => "sqlite3",
11
+ :dbfile => dbfile }
12
+
13
+ ActiveRecord::Base.establish_connection( config )
14
+