Changeset 5426
- Timestamp:
- 11/05/06 02:01:31 (2 years ago)
- Files:
-
- trunk/activerecord/CHANGELOG (modified) (1 diff)
- trunk/activerecord/lib/active_record/calculations.rb (modified) (6 diffs)
- trunk/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb (modified) (18 diffs)
- trunk/activerecord/Rakefile (modified) (1 diff)
- trunk/activerecord/test/base_test.rb (modified) (1 diff)
- trunk/activerecord/test/connections/native_sqlite/connection.rb (modified) (2 diffs)
- trunk/activerecord/test/connections/native_sqlite3/connection.rb (modified) (2 diffs)
- trunk/activerecord/test/migration_test.rb (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/activerecord/CHANGELOG
r5416 r5426 1 1 *SVN* 2 3 * SQLite: fix calculations workaround, remove count(distinct) query rewrite, cleanup test connection scripts. [Jeremy Kemper] 4 5 * SQLite: count(distinct) queries supported in >= 3.2.6. #6544 [Bob Silva] 2 6 3 7 * Dynamically generate reader methods for serialized attributes. #6362 [Stefan Kaes] trunk/activerecord/lib/active_record/calculations.rb
r5204 r5426 7 7 8 8 module ClassMethods 9 # Count operates using three different approaches. 9 # Count operates using three different approaches. 10 10 # 11 11 # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model. … … 37 37 # Person.count(:conditions => "age > 26") 38 38 # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN. 39 # Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins. 39 # Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins. 40 40 # Person.count('id', :conditions => "age > 26") # Performs a COUNT(id) 41 41 # Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*') … … 75 75 76 76 # This calculates aggregate values in the given column: Methods for count, sum, average, minimum, and maximum have been added as shortcuts. 77 # Options such as :conditions, :order, :group, :having, and :joins can be passed to customize the query. 77 # Options such as :conditions, :order, :group, :having, and :joins can be passed to customize the query. 78 78 # 79 79 # There are two basic forms of output: 80 80 # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float for AVG, and the given column's type for everything else. 81 # * Grouped values: This returns an ordered hash of the values and groups them by the :group option. It takes either a column name, or the name 81 # * Grouped values: This returns an ordered hash of the values and groups them by the :group option. It takes either a column name, or the name 82 82 # of a belongs_to association. 83 83 # … … 158 158 159 159 def construct_calculation_sql(operation, column_name, options) #:nodoc: 160 operation = operation.to_s.downcase 161 options = options.symbolize_keys 162 160 163 scope = scope(:find) 161 164 merged_includes = merge_includes(scope ? scope[:include] : [], options[:include]) 162 165 aggregate_alias = column_alias_for(operation, column_name) 163 use_workaround = !Base.connection.supports_count_distinct? && options[:distinct] && operation.to_s.downcase == 'count' 164 join_dependency = nil 165 166 if merged_includes.any? && operation.to_s.downcase == 'count' 167 options[:distinct] = true 168 column_name = options[:select] || [table_name, primary_key] * '.' 169 end 170 171 sql = "SELECT #{operation}(#{'DISTINCT ' if options[:distinct]}#{column_name}) AS #{aggregate_alias}" 166 167 if operation == 'count' 168 if merged_includes.any? 169 options[:distinct] = true 170 column_name = options[:select] || [table_name, primary_key] * '.' 171 end 172 173 if options[:distinct] 174 use_workaround = !connection.supports_count_distinct? 175 end 176 end 177 178 sql = "SELECT #{operation}(#{'DISTINCT ' if options[:distinct]}#{column_name}) AS #{aggregate_alias}" 172 179 173 180 # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT. 174 181 sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround 175 182 176 183 sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group] 177 184 sql << " FROM (SELECT DISTINCT #{column_name}" if use_workaround … … 186 193 187 194 if options[:group] 188 group_key = Base.connection.adapter_name == 'FrontBase' ? :group_alias : :group_field195 group_key = connection.adapter_name == 'FrontBase' ? :group_alias : :group_field 189 196 sql << " GROUP BY #{options[group_key]} " 190 197 end … … 192 199 if options[:group] && options[:having] 193 200 # FrontBase requires identifiers in the HAVING clause and chokes on function calls 194 if Base.connection.adapter_name == 'FrontBase'201 if connection.adapter_name == 'FrontBase' 195 202 options[:having].downcase! 196 203 options[:having].gsub!(/#{operation}\s*\(\s*#{column_name}\s*\)/, aggregate_alias) 197 204 end 198 205 199 206 sql << " HAVING #{options[:having]} " 200 207 end trunk/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
r5353 r5426 74 74 when "%" then "%25" 75 75 end 76 end 77 end 78 76 end 77 end 78 79 79 def binary_to_string(value) 80 80 value.gsub(/%00|%25/) do |b| … … 83 83 when "%25" then "%" 84 84 end 85 end 85 end 86 86 end 87 87 end … … 102 102 true 103 103 end 104 104 105 105 def supports_count_distinct? #:nodoc: 106 false106 sqlite_version >= '3.2.6' 107 107 end 108 108 … … 179 179 catch_schema_changes { @connection.transaction } 180 180 end 181 181 182 182 def commit_db_transaction #:nodoc: 183 183 catch_schema_changes { @connection.commit } … … 226 226 execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}" 227 227 end 228 228 229 229 def rename_table(name, new_name) 230 230 execute "ALTER TABLE #{name} RENAME TO #{new_name}" … … 236 236 execute "VACUUM" 237 237 end 238 238 239 239 def remove_column(table_name, column_name) #:nodoc: 240 240 alter_table(table_name) do |definition| … … 242 242 end 243 243 end 244 244 245 245 def change_column_default(table_name, column_name, default) #:nodoc: 246 246 alter_table(table_name) do |definition| … … 262 262 alter_table(table_name, :rename => {column_name => new_column_name}) 263 263 end 264 264 265 265 266 266 protected … … 270 270 end 271 271 end 272 272 273 273 def alter_table(table_name, options = {}) #:nodoc: 274 274 altered_table_name = "altered_#{table_name}" … … 276 276 277 277 transaction do 278 move_table(table_name, altered_table_name, 278 move_table(table_name, altered_table_name, 279 279 options.merge(:temporary => true)) 280 280 move_table(altered_table_name, table_name, &caller) 281 281 end 282 282 end 283 283 284 284 def move_table(from, to, options = {}, &block) #:nodoc: 285 285 copy_table(from, to, options, &block) 286 286 drop_table(from) 287 287 end 288 288 289 289 def copy_table(from, to, options = {}) #:nodoc: 290 290 create_table(to, options) do |@definition| … … 295 295 column.name) : column.name 296 296 297 @definition.column(column_name, column.type, 297 @definition.column(column_name, column.type, 298 298 :limit => column.limit, :default => column.default, 299 299 :null => column.null) … … 302 302 yield @definition if block_given? 303 303 end 304 304 305 305 copy_table_indexes(from, to) 306 copy_table_contents(from, to, 307 @definition.columns.map {|column| column.name}, 306 copy_table_contents(from, to, 307 @definition.columns.map {|column| column.name}, 308 308 options[:rename] || {}) 309 309 end 310 310 311 311 def copy_table_indexes(from, to) #:nodoc: 312 312 indexes(from).each do |index| … … 317 317 name = name[5..-1] 318 318 end 319 319 320 320 # index name can't be the same 321 321 opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") } … … 324 324 end 325 325 end 326 326 327 327 def copy_table_contents(from, to, columns, rename = {}) #:nodoc: 328 328 column_mappings = Hash[*columns.map {|name| [name, name]}.flatten] … … 331 331 columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} 332 332 @connection.execute "SELECT * FROM #{from}" do |row| 333 sql = "INSERT INTO #{to} ("+columns*','+") VALUES (" 333 sql = "INSERT INTO #{to} ("+columns*','+") VALUES (" 334 334 sql << columns.map {|col| quote row[column_mappings[col]]} * ', ' 335 335 sql << ')' … … 337 337 end 338 338 end 339 339 340 340 def catch_schema_changes 341 341 return yield … … 348 348 end 349 349 end 350 end 351 350 351 def sqlite_version 352 @sqlite_version ||= select_value('select sqlite_version(*)') 353 end 354 end 355 352 356 class SQLite2Adapter < SQLiteAdapter # :nodoc: 353 # SQLite 2 does not support COUNT(DISTINCT) queries: 354 # 355 # select COUNT(DISTINCT ArtistID) from CDs; 356 # 357 # In order to get the number of artists we execute the following statement 358 # 359 # SELECT COUNT(ArtistID) FROM (SELECT DISTINCT ArtistID FROM CDs); 360 def execute(sql, name = nil) #:nodoc: 361 super(rewrite_count_distinct_queries(sql), name) 362 end 363 364 def rewrite_count_distinct_queries(sql) 365 if sql =~ /count\(distinct ([^\)]+)\)( AS \w+)? (.*)/i 366 distinct_column = $1 367 distinct_query = $3 368 column_name = distinct_column.split('.').last 369 "SELECT COUNT(#{column_name}) FROM (SELECT DISTINCT #{distinct_column} #{distinct_query})" 370 else 371 sql 372 end 373 end 374 357 def supports_count_distinct? #:nodoc: 358 false 359 end 360 375 361 def rename_table(name, new_name) 376 362 move_table(name, new_name) 377 363 end 378 364 379 365 def add_column(table_name, column_name, type, options = {}) #:nodoc: 380 366 alter_table(table_name) do |definition| … … 382 368 end 383 369 end 384 385 370 end 386 371 trunk/activerecord/Rakefile
r5181 r5426 24 24 25 25 desc "Default Task" 26 task :default => [ :test_mysql, :test_sqlite, :test_ postgresql ]26 task :default => [ :test_mysql, :test_sqlite, :test_sqlite3, :test_postgresql ] 27 27 28 28 # Run the unit tests trunk/activerecord/test/base_test.rb
r5416 r5426 1220 1220 assert_equal res4, res5 1221 1221 1222 res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" 1223 res7 = nil 1224 assert_nothing_raised do 1225 res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id", 1226 :joins => "p, comments co", 1227 :select => "p.id", 1228 :distinct => true) 1229 end 1230 assert_equal res6, res7 1222 unless current_adapter?(:SQLite2Adapter, :DeprecatedSQLiteAdapter) 1223 res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id" 1224 res7 = nil 1225 assert_nothing_raised do 1226 res7 = Post.count(:conditions => "p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id", 1227 :joins => "p, comments co", 1228 :select => "p.id", 1229 :distinct => true) 1230 end 1231 assert_equal res6, res7 1232 end 1231 1233 end 1232 1234 trunk/activerecord/test/connections/native_sqlite/connection.rb
r4602 r5426 11 11 sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite" 12 12 13 def make_connection(clazz, db_file , db_definitions_file)13 def make_connection(clazz, db_file) 14 14 ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'sqlite', :database => db_file } } 15 15 unless File.exist?(db_file) … … 18 18 puts "Executing '#{sqlite_command}'" 19 19 raise SqliteError.new("Seems that there is no sqlite executable available") unless system(sqlite_command) 20 clazz.establish_connection(clazz.name)21 script = File.read("#{BASE_DIR}/db_definitions/#{db_definitions_file}")22 # SQLite-Ruby has problems with semi-colon separated commands, so split and execute one at a time23 script.split(';').each do24 |command|25 clazz.connection.execute(command) unless command.strip.empty?26 end27 else28 clazz.establish_connection(clazz.name)29 20 end 21 clazz.establish_connection(clazz.name) 30 22 end 31 23 32 make_connection(ActiveRecord::Base, sqlite_test_db, 'sqlite.sql') 33 make_connection(Course, sqlite_test_db2, 'sqlite2.sql') 34 load(File.join(BASE_DIR, 'db_definitions', 'schema.rb')) 24 make_connection(ActiveRecord::Base, sqlite_test_db) 25 make_connection(Course, sqlite_test_db2) trunk/activerecord/test/connections/native_sqlite3/connection.rb
r5258 r5426 11 11 sqlite_test_db2 = "#{BASE_DIR}/fixture_database_2.sqlite3" 12 12 13 def make_connection(clazz, db_file , db_definitions_file)13 def make_connection(clazz, db_file) 14 14 ActiveRecord::Base.configurations = { clazz.name => { :adapter => 'sqlite3', :database => db_file, :timeout => 5000 } } 15 15 unless File.exist?(db_file) … … 18 18 puts "Executing '#{sqlite_command}'" 19 19 raise SqliteError.new("Seems that there is no sqlite3 executable available") unless system(sqlite_command) 20 clazz.establish_connection(clazz.name)21 script = File.read("#{BASE_DIR}/db_definitions/#{db_definitions_file}")22 # SQLite-Ruby has problems with semi-colon separated commands, so split and execute one at a time23 script.split(';').each do24 |command|25 clazz.connection.execute(command) unless command.strip.empty?26 end27 else28 clazz.establish_connection(clazz.name)29 20 end 21 clazz.establish_connection(clazz.name) 30 22 end 31 23 32 make_connection(ActiveRecord::Base, sqlite_test_db, 'sqlite.sql') 33 make_connection(Course, sqlite_test_db2, 'sqlite2.sql') 34 load(File.join(BASE_DIR, 'db_definitions', 'schema.rb')) 24 make_connection(ActiveRecord::Base, sqlite_test_db) 25 make_connection(Course, sqlite_test_db2) trunk/activerecord/test/migration_test.rb
r5260 r5426 166 166 end 167 167 168 # SQL Server and Sybase will not allow you to add a NOT NULL column 169 # to a table without specifying a default value, so the 170 # following test must be skipped 171 unless current_adapter?(:SQLServerAdapter, :SybaseAdapter) 168 # SQL Server, Sybase, and SQLite3 will not allow you to add a NOT NULL 169 # column to a table without a default value. 170 unless current_adapter?(:SQLServerAdapter, :SybaseAdapter, :SQLiteAdapter) 172 171 def test_add_column_not_null_without_default 173 172 Person.connection.create_table :testings do |t|