aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md45
-rw-r--r--activerecord/README.rdoc4
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb16
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb2
-rw-r--r--activerecord/lib/active_record/base.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb57
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb123
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb63
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb23
-rw-r--r--activerecord/lib/active_record/migration.rb9
-rw-r--r--activerecord/lib/active_record/relation.rb18
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb2
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb14
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb30
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb3
-rw-r--r--activerecord/test/cases/adapters/mysql2/explain_test.rb23
-rw-r--r--activerecord/test/cases/adapters/postgresql/explain_test.rb25
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb20
-rw-r--r--activerecord/test/cases/adapters/sqlite3/explain_test.rb23
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb18
-rw-r--r--activerecord/test/cases/base_test.rb6
-rw-r--r--activerecord/test/cases/finder_test.rb9
-rw-r--r--activerecord/test/cases/migration_test.rb83
-rw-r--r--activerecord/test/cases/relation_test.rb2
-rw-r--r--activerecord/test/cases/relations_test.rb16
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb5
30 files changed, 591 insertions, 79 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 2b46a6a869..65578c1dc9 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,5 +1,25 @@
## Rails 3.2.0 (unreleased) ##
+* Implemented ActiveRecord::Relation#explain. *fxn*
+
+* Add ActiveRecord::Relation#uniq for generating unique queries.
+
+ Before:
+
+ Client.select('DISTINCT name')
+
+ After:
+
+ Client.select(:name).uniq
+
+ This also allows you to revert the unqueness in a relation:
+
+ Client.select(:name).uniq.uniq(false)
+
+ *Jon Leighton*
+
+* Support index sort order in sqlite, mysql and postgres adapters. *Vlad Jebelev*
+
* Allow the :class_name option for associations to take a symbol (:Client) in addition to
a string ('Client').
@@ -47,6 +67,27 @@
## Rails 3.1.2 (unreleased) ##
+* Fix bug with PostgreSQLAdapter#indexes. When the search path has multiple schemas, spaces
+ were not being stripped from the schema names after the first.
+
+ *Sean Kirby*
+
+* Preserve SELECT columns on the COUNT for finder_sql when possible. *GH 3503*
+
+ *Justin Mazzi*
+
+* Reset prepared statement cache when schema changes impact statement results. *GH 3335*
+
+ *Aaron Patterson*
+
+* Postgres: Do not attempt to deallocate a statement if the connection is no longer active.
+
+ *Ian Leitch*
+
+* Prevent QueryCache leaking database connections. *GH 3243*
+
+ *Mark J. Titorenko*
+
* Fix bug where building the conditions of a nested through association could potentially
modify the conditions of the through and/or source association. If you have experienced
bugs with conditions appearing in the wrong queries when using nested through associations,
@@ -67,6 +108,10 @@
*Jon Leighton*
+* MySQL: use the information_schema than the describe command when we look for a primary key. *GH #3440*
+
+ *Kenny J*
+
## Rails 3.1.1 (October 7, 2011) ##
* Add deprecation for the preload_associations method. Fixes #3022.
diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc
index b5db57569c..70922ef864 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -150,7 +150,7 @@ A short rundown of some of the major features:
* Database agnostic schema management with Migrations.
class AddSystemSettings < ActiveRecord::Migration
- def self.up
+ def up
create_table :system_settings do |t|
t.string :name
t.string :label
@@ -162,7 +162,7 @@ A short rundown of some of the major features:
SystemSetting.create :name => "notice", :label => "Use notice?", :value => 1
end
- def self.down
+ def down
drop_table :system_settings
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 6cc401e6cc..6f8b76abda 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -20,31 +20,19 @@ module ActiveRecord
# It's okay to just apply all these like this. The options will only be present if the
# association supports that option; this is enforced by the association builder.
scope = scope.apply_finder_options(options.slice(
- :readonly, :include, :order, :limit, :joins, :group, :having, :offset))
+ :readonly, :include, :order, :limit, :joins, :group, :having, :offset, :select))
if options[:through] && !options[:include]
scope = scope.includes(source_options[:include])
end
- if select = select_value
- scope = scope.select(select)
- end
+ scope = scope.uniq if options[:uniq]
add_constraints(scope)
end
private
- def select_value
- select_value = options[:select]
-
- if reflection.collection?
- select_value ||= options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*"
- end
-
- select_value
- end
-
def add_constraints(scope)
tables = construct_tables
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 4174e4da09..4a5afcd585 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -77,7 +77,7 @@ module ActiveRecord
#
# The second, slower, branch is necessary to support instances where the database
# returns columns with extra stuff in (like 'my_column(omg)').
- if method_name =~ ActiveModel::AttributeMethods::COMPILABLE_REGEXP
+ if method_name =~ ActiveModel::AttributeMethods::NAME_COMPILABLE_REGEXP
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__
def _#{method_name}
#{access_code}
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index e9cdb130db..eb585ee906 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -10,7 +10,7 @@ module ActiveRecord
module ClassMethods
protected
def define_method_attribute=(attr_name)
- if attr_name =~ ActiveModel::AttributeMethods::COMPILABLE_REGEXP
+ if attr_name =~ ActiveModel::AttributeMethods::NAME_COMPILABLE_REGEXP
generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
else
generated_attribute_methods.send(:define_method, "#{attr_name}=") do |new_value|
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 360e494af1..3558ae3545 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -445,7 +445,9 @@ module ActiveRecord #:nodoc:
delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped
delegate :find_each, :find_in_batches, :to => :scoped
- delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped
+ delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
+ :where, :preload, :eager_load, :includes, :from, :lock, :readonly,
+ :having, :create_with, :uniq, :to => :scoped
delegate :count, :average, :minimum, :maximum, :sum, :calculate, :to => :scoped
# Executes a custom SQL query against your database and returns all the results. The results will
@@ -758,7 +760,7 @@ module ActiveRecord #:nodoc:
# values, eg:
#
# class CreateJobLevels < ActiveRecord::Migration
- # def self.up
+ # def up
# create_table :job_levels do |t|
# t.integer :id
# t.string :name
@@ -772,7 +774,7 @@ module ActiveRecord #:nodoc:
# end
# end
#
- # def self.down
+ # def down
# drop_table :job_levels
# end
# end
@@ -1769,7 +1771,8 @@ MSG
# Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
# nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
def attribute_present?(attribute)
- !_read_attribute(attribute).blank?
+ value = _read_attribute(attribute)
+ !value.nil? || (value.respond_to?(:empty?) && !value.empty?)
end
# Returns the column object for the named attribute.
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index 77a5fe1efb..0ec0576795 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -123,14 +123,14 @@ module ActiveRecord
# A cached lookup for table existence.
def table_exists?(name)
- return true if @tables.key? name
+ return @tables[name] if @tables.key? name
with_connection do |conn|
conn.tables.each { |table| @tables[table] = true }
- @tables[name] = true if !@tables.key?(name) && conn.table_exists?(name)
+ @tables[name] = conn.table_exists?(name) if !@tables.key?(name)
end
- @tables.key? name
+ @tables[name]
end
# Clears out internal caches:
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 989a4fcbca..6f135b56b5 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -6,7 +6,7 @@ require 'bigdecimal/util'
module ActiveRecord
module ConnectionAdapters #:nodoc:
- class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc:
+ class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders) #:nodoc:
end
# Abstract representation of a column definition. Instances of this type
@@ -46,13 +46,13 @@ module ActiveRecord
# +change_table+ is actually of this type:
#
# class SomeMigration < ActiveRecord::Migration
- # def self.up
+ # def up
# create_table :foo do |t|
# puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition"
# end
# end
#
- # def self.down
+ # def down
# ...
# end
# end
@@ -479,4 +479,3 @@ module ActiveRecord
end
end
-
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 7226069ebf..11da84e245 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -158,7 +158,13 @@ module ActiveRecord
td = table_definition
td.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
- td.instance_eval(&blk) if blk
+ if block_given?
+ if blk.arity == 1
+ yield td
+ else
+ td.instance_eval(&blk)
+ end
+ end
if options[:force] && table_exists?(table_name)
drop_table(table_name)
@@ -235,14 +241,19 @@ module ActiveRecord
#
# See also Table for details on
# all of the various column transformation
- def change_table(table_name, options = {})
- if supports_bulk_alter? && options[:bulk]
- recorder = ActiveRecord::Migration::CommandRecorder.new(self)
- yield Table.new(table_name, recorder)
- bulk_change_table(table_name, recorder.commands)
- else
- yield Table.new(table_name, self)
+ def change_table(table_name, options = {}, &blk)
+ bulk_change = supports_bulk_alter? && options[:bulk]
+ recorder = bulk_change ? ActiveRecord::Migration::CommandRecorder.new(self) : self
+ table = Table.new(table_name, recorder)
+
+ if block_given?
+ if blk.arity == 1
+ yield table
+ else
+ table.instance_eval(&blk)
+ end
end
+ bulk_change_table(table_name, recorder.commands) if bulk_change
end
# Renames a table.
@@ -339,6 +350,14 @@ module ActiveRecord
# CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
#
# Note: SQLite doesn't support index length
+ #
+ # ====== Creating an index with a sort order (desc or asc, asc is the default)
+ # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :part_id => :asc})
+ # generates
+ # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)
+ #
+ # Note: mysql doesn't yet support index order (it accepts the syntax but ignores it)
+ #
def add_index(table_name, column_name, options = {})
index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})"
@@ -520,9 +539,29 @@ module ActiveRecord
end
protected
+ def add_index_sort_order(option_strings, column_names, options = {})
+ if options.is_a?(Hash) && order = options[:order]
+ case order
+ when Hash
+ column_names.each {|name| option_strings[name] += " #{order[name].to_s.upcase}" if order.has_key?(name)}
+ when String
+ column_names.each {|name| option_strings[name] += " #{order.upcase}"}
+ end
+ end
+
+ return option_strings
+ end
+
# Overridden by the mysql adapter for supporting index lengths
def quoted_columns_for_index(column_names, options = {})
- column_names.map {|name| quote_column_name(name) }
+ option_strings = Hash[column_names.map {|name| [name, '']}]
+
+ # add index sort order if supported
+ if supports_index_sort_order?
+ option_strings = add_index_sort_order(option_strings, column_names, options)
+ end
+
+ column_names.map {|name| quote_column_name(name) + option_strings[name]}
end
def options_include_default?(options)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 4c3a8f7233..c47bcfc406 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -130,6 +130,11 @@ module ActiveRecord
false
end
+ # Does this adapter support index sort order?
+ def supports_index_sort_order?
+ false
+ end
+
# QUOTING ==================================================
# Override to return the quoted table name. Defaults to column quoting.
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index dd573ba569..baf4c043c4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -155,6 +155,12 @@ module ActiveRecord
true
end
+ # Technically MySQL allows to create indexes with the sort order syntax
+ # but at the moment (5.5) it doesn't yet implement them
+ def supports_index_sort_order?
+ true
+ end
+
def native_database_types
NATIVE_DATABASE_TYPES
end
@@ -222,6 +228,80 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
+ def explain(arel)
+ sql = "EXPLAIN #{to_sql(arel)}"
+ start = Time.now
+ result = exec_query(sql, 'EXPLAIN')
+ elapsed = Time.now - start
+
+ ExplainPrettyPrinter.new.pp(result, elapsed)
+ end
+
+ class ExplainPrettyPrinter # :nodoc:
+ # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
+ # MySQL shell:
+ #
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+ # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
+ # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+ # 2 rows in set (0.00 sec)
+ #
+ # This is an exercise in Ruby hyperrealism :).
+ def pp(result, elapsed)
+ widths = compute_column_widths(result)
+ separator = build_separator(widths)
+
+ pp = []
+
+ pp << separator
+ pp << build_cells(result.columns, widths)
+ pp << separator
+
+ result.rows.each do |row|
+ pp << build_cells(row, widths)
+ end
+
+ pp << separator
+ pp << build_footer(result.rows.length, elapsed)
+
+ pp.join("\n") + "\n"
+ end
+
+ private
+
+ def compute_column_widths(result)
+ [].tap do |widths|
+ result.columns.each_with_index do |column, i|
+ cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s}
+ widths << cells_in_column.map(&:length).max
+ end
+ end
+ end
+
+ def build_separator(widths)
+ padding = 1
+ '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+'
+ end
+
+ def build_cells(items, widths)
+ cells = []
+ items.each_with_index do |item, i|
+ item = 'NULL' if item.nil?
+ justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust'
+ cells << item.to_s.send(justifier, widths[i])
+ end
+ '| ' + cells.join(' | ') + ' |'
+ end
+
+ def build_footer(nrows, elapsed)
+ rows_label = nrows == 1 ? 'row' : 'rows'
+ "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed
+ end
+ end
+
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
if name == :skip_logging
@@ -496,8 +576,17 @@ module ActiveRecord
# Returns a table's primary key and belonging sequence.
def pk_and_sequence_for(table)
- execute_and_free("DESCRIBE #{quote_table_name(table)}", 'SCHEMA') do |result|
- keys = each_hash(result).select { |row| row[:Key] == 'PRI' }.map { |row| row[:Field] }
+ sql = <<-SQL
+ SELECT t.constraint_type, k.column_name
+ FROM information_schema.table_constraints t
+ JOIN information_schema.key_column_usage k
+ USING (constraint_name, table_schema, table_name)
+ WHERE t.table_schema = DATABASE()
+ AND t.table_name = '#{table}'
+ SQL
+
+ execute_and_free(sql, 'SCHEMA') do |result|
+ keys = each_hash(result).select { |row| row[:constraint_type] == 'PRIMARY KEY' }.map { |row| row[:column_name] }
keys.length == 1 ? [keys.first, nil] : nil
end
end
@@ -526,17 +615,29 @@ module ActiveRecord
protected
+ def add_index_length(option_strings, column_names, options = {})
+ if options.is_a?(Hash) && length = options[:length]
+ case length
+ when Hash
+ column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name)}
+ when Fixnum
+ column_names.each {|name| option_strings[name] += "(#{length})"}
+ end
+ end
+
+ return option_strings
+ end
+
def quoted_columns_for_index(column_names, options = {})
- length = options[:length] if options.is_a?(Hash)
+ option_strings = Hash[column_names.map {|name| [name, '']}]
- case length
- when Hash
- column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
- when Fixnum
- column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
- else
- column_names.map {|name| quote_column_name(name) }
- end
+ # add index length
+ option_strings = add_index_length(option_strings, column_names, options)
+
+ # add index sort order
+ option_strings = add_index_sort_order(option_strings, column_names, options)
+
+ column_names.map {|name| quote_column_name(name) + option_strings[name]}
end
def translate_exception(exception, message)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index e8a43e7bce..b7918c7f07 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -247,6 +247,10 @@ module ActiveRecord
true
end
+ def supports_index_sort_order?
+ true
+ end
+
class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max)
super
@@ -513,6 +517,48 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
+ def explain(arel)
+ sql = "EXPLAIN #{to_sql(arel)}"
+ ExplainPrettyPrinter.new.pp(exec_query(sql))
+ end
+
+ class ExplainPrettyPrinter # :nodoc:
+ # Pretty prints the result of a EXPLAIN in a way that resembles the output of the
+ # PostgreSQL shell:
+ #
+ # QUERY PLAN
+ # ------------------------------------------------------------------------------
+ # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
+ # Join Filter: (posts.user_id = users.id)
+ # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
+ # Index Cond: (id = 1)
+ # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
+ # Filter: (posts.user_id = 1)
+ # (6 rows)
+ #
+ def pp(result)
+ header = result.columns.first
+ lines = result.rows.map(&:first)
+
+ # We add 2 because there's one char of padding at both sides, note
+ # the extra hyphens in the example above.
+ width = [header, *lines].map(&:length).max + 2
+
+ pp = []
+
+ pp << header.center(width).rstrip
+ pp << '-' * width
+
+ pp += lines.map {|line| " #{line}"}
+
+ nrows = result.rows.length
+ rows_label = nrows == 1 ? 'row' : 'rows'
+ pp << "(#{nrows} #{rows_label})"
+
+ pp.join("\n") + "\n"
+ end
+ end
+
# Executes a SELECT query and returns an array of rows. Each row is an
# array of field values.
def select_rows(sql, name = nil)
@@ -754,16 +800,15 @@ module ActiveRecord
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil)
- schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
result = query(<<-SQL, name)
- SELECT distinct i.relname, d.indisunique, d.indkey, t.oid
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
WHERE i.relkind = 'i'
AND d.indisprimary = 'f'
AND t.relname = '#{table_name}'
- AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname IN (#{schemas}) )
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
ORDER BY i.relname
SQL
@@ -772,7 +817,8 @@ module ActiveRecord
index_name = row[0]
unique = row[1] == 't'
indkey = row[2].split(" ")
- oid = row[3]
+ inddef = row[3]
+ oid = row[4]
columns = Hash[query(<<-SQL, "Columns for index #{row[0]} on #{table_name}")]
SELECT a.attnum, a.attname
@@ -782,7 +828,12 @@ module ActiveRecord
SQL
column_names = columns.values_at(*indkey).compact
- column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names)
+
+ # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
+ desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
+ orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
+
+ column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders)
end.compact
end
@@ -826,7 +877,7 @@ module ActiveRecord
# Returns the active schema search path.
def schema_search_path
- @schema_search_path ||= query('SHOW search_path')[0][0]
+ @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0]
end
# Returns the current client message level.
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
index f74f3e6ec8..35df0a1542 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -157,6 +157,10 @@ module ActiveRecord
sqlite_version >= '3.1.0'
end
+ def supports_index_sort_order?
+ sqlite_version >= '3.3.0'
+ end
+
def native_database_types #:nodoc:
{
:primary_key => default_primary_key_type,
@@ -218,6 +222,25 @@ module ActiveRecord
# DATABASE STATEMENTS ======================================
+ def explain(arel)
+ sql = "EXPLAIN QUERY PLAN #{to_sql(arel)}"
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN'))
+ end
+
+ class ExplainPrettyPrinter
+ # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles
+ # the output of the SQLite shell:
+ #
+ # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
+ # 0|1|1|SCAN TABLE posts (~100000 rows)
+ #
+ def pp(result) # :nodoc:
+ result.rows.map do |row|
+ row.join('|')
+ end.join("\n") + "\n"
+ end
+ end
+
def exec_query(sql, name = nil, binds = [])
log(sql, name, binds) do
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 7166f1b82a..d70c7d1d34 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -112,12 +112,13 @@ module ActiveRecord
# a column but keeps the type and content.
# * <tt>change_column(table_name, column_name, type, options)</tt>: Changes
# the column to a different type using the same parameters as add_column.
- # * <tt>remove_column(table_name, column_name)</tt>: Removes the column named
- # +column_name+ from the table called +table_name+.
+ # * <tt>remove_column(table_name, column_names)</tt>: Removes the column listed in
+ # +column_names+ from the table called +table_name+.
# * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index
# with the name of the column. Other options include
- # <tt>:name</tt> and <tt>:unique</tt> (e.g.
- # <tt>{ :name => "users_name_index", :unique => true }</tt>).
+ # <tt>:name</tt>, <tt>:unique</tt> (e.g.
+ # <tt>{ :name => "users_name_index", :unique => true }</tt>) and <tt>:order</tt>
+ # (e.g. { :order => {:name => :desc} }</tt>).
# * <tt>remove_index(table_name, :column => column_name)</tt>: Removes the index
# specified by +column_name+.
# * <tt>remove_index(table_name, :name => index_name)</tt>: Removes the index
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index ecefaa633c..f0891440a6 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -7,7 +7,7 @@ module ActiveRecord
JoinOperation = Struct.new(:relation, :join_class, :on)
ASSOCIATION_METHODS = [:includes, :eager_load, :preload]
MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind]
- SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order]
+ SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order, :uniq]
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches
@@ -143,6 +143,22 @@ module ActiveRecord
super
end
+ def explain
+ queries = []
+ callback = lambda do |*args|
+ payload = args.last
+ queries << payload[:sql] unless payload[:exception] || %w(SCHEMA EXPLAIN).include?(payload[:name])
+ end
+
+ ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
+ to_a
+ end
+
+ queries.map do |sql|
+ @klass.connection.explain(sql)
+ end.join
+ end
+
def to_a
return @records if loaded?
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 7eeb3dde70..3c8e0f2052 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -191,7 +191,7 @@ module ActiveRecord
join_dependency = construct_join_dependency_for_association_find
relation = construct_relation_for_association_find(join_dependency)
- relation = relation.except(:select).select("1").limit(1)
+ relation = relation.except(:select, :order).select("1").limit(1)
case id
when Array, Hash
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 7e8ddd1b5d..af167dc59b 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -22,21 +22,23 @@ module ActiveRecord
value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
attribute.in(value.arel.ast)
when Array, ActiveRecord::Associations::CollectionProxy
- values = value.to_a.map { |x|
- x.is_a?(ActiveRecord::Base) ? x.id : x
- }
+ values = value.to_a.map {|x| x.is_a?(ActiveRecord::Base) ? x.id : x}
+ ranges, values = values.partition {|value| value.is_a?(Range) || value.is_a?(Arel::Relation)}
+
+ array_predicates = ranges.map {|range| attribute.in(range)}
if values.include?(nil)
values = values.compact
if values.empty?
- attribute.eq nil
+ array_predicates << attribute.eq(nil)
else
- attribute.in(values.compact).or attribute.eq(nil)
+ array_predicates << attribute.in(values.compact).or(attribute.eq(nil))
end
else
- attribute.in(values)
+ array_predicates << attribute.in(values)
end
+ array_predicates.inject {|composite, predicate| composite.or(predicate)}
when Range, Arel::Relation
attribute.in(value)
when ActiveRecord::Base
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 670ba0987d..c281bead0d 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -9,7 +9,8 @@ module ActiveRecord
:select_values, :group_values, :order_values, :joins_values,
:where_values, :having_values, :bind_values,
:limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value,
- :from_value, :reorder_value, :reverse_order_value
+ :from_value, :reorder_value, :reverse_order_value,
+ :uniq_value
def includes(*args)
args.reject! {|a| a.blank? }
@@ -38,7 +39,7 @@ module ActiveRecord
end
# Works in two unique ways.
- #
+ #
# First: takes a block so it can be used just like Array#select.
#
# Model.scoped.select { |m| m.field == value }
@@ -176,9 +177,25 @@ module ActiveRecord
relation
end
+ # Specifies whether the records should be unique or not. For example:
+ #
+ # User.select(:name)
+ # # => Might return two records with the same name
+ #
+ # User.select(:name).uniq
+ # # => Returns 1 record per unique name
+ #
+ # User.select(:name).uniq.uniq(false)
+ # # => You can also remove the uniqueness
+ def uniq(value = true)
+ relation = clone
+ relation.uniq_value = value
+ relation
+ end
+
# Used to extend a scope with additional methods, either through
- # a module or through a block provided.
- #
+ # a module or through a block provided.
+ #
# The object returned is a relation, which can be further extended.
#
# === Using a module
@@ -200,7 +217,7 @@ module ActiveRecord
#
# scope = Model.scoped.extending do
# def page(number)
- # # pagination code goes here
+ # # pagination code goes here
# end
# end
# scope.page(params[:page])
@@ -209,7 +226,7 @@ module ActiveRecord
#
# scope = Model.scoped.extending(Pagination) do
# def per_page(number)
- # # pagination code goes here
+ # # pagination code goes here
# end
# end
def extending(*modules)
@@ -252,6 +269,7 @@ module ActiveRecord
build_select(arel, @select_values.uniq)
+ arel.distinct(@uniq_value)
arel.from(@from_value) if @from_value
arel.lock(@lock_value) if @lock_value
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 6fe305f843..cdde5cf3b9 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -190,6 +190,9 @@ HEADER
index_lengths = (index.lengths || []).compact
statement_parts << (':length => ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty?
+ index_orders = (index.orders || {})
+ statement_parts << (':order => ' + index.orders.inspect) unless index_orders.empty?
+
' ' + statement_parts.join(', ')
end
diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb
new file mode 100644
index 0000000000..8ea777b72b
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb
@@ -0,0 +1,23 @@
+require "cases/helper"
+require 'models/developer'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class Mysql2Adapter
+ class ExplainTest < ActiveRecord::TestCase
+ fixtures :developers
+
+ def test_explain_for_one_query
+ explain = Developer.where(:id => 1).explain
+ assert_match %(developers | const), explain
+ end
+
+ def test_explain_with_eager_loading
+ explain = Developer.where(:id => 1).includes(:audit_logs).explain
+ assert_match %(developers | const), explain
+ assert_match %(audit_logs | ALL), explain
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb
new file mode 100644
index 0000000000..0d599ed37f
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb
@@ -0,0 +1,25 @@
+require "cases/helper"
+require 'models/developer'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLAdapter
+ class ExplainTest < ActiveRecord::TestCase
+ fixtures :developers
+
+ def test_explain_for_one_query
+ explain = Developer.where(:id => 1).explain
+ assert_match %(QUERY PLAN), explain
+ assert_match %(Index Scan using developers_pkey on developers), explain
+ end
+
+ def test_explain_with_eager_loading
+ explain = Developer.where(:id => 1).includes(:audit_logs).explain
+ assert_match %(QUERY PLAN), explain
+ assert_match %(Index Scan using developers_pkey on developers), explain
+ assert_match %(Seq Scan on audit_logs), explain
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index c8f8714f66..467e5d7b86 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -10,14 +10,17 @@ class SchemaTest < ActiveRecord::TestCase
INDEX_A_NAME = 'a_index_things_on_name'
INDEX_B_NAME = 'b_index_things_on_different_columns_in_each_schema'
INDEX_C_NAME = 'c_index_full_text_search'
+ INDEX_D_NAME = 'd_index_things_on_description_desc'
INDEX_A_COLUMN = 'name'
INDEX_B_COLUMN_S1 = 'email'
INDEX_B_COLUMN_S2 = 'moment'
INDEX_C_COLUMN = %q{(to_tsvector('english', coalesce(things.name, '')))}
+ INDEX_D_COLUMN = 'description'
COLUMNS = [
'id integer',
'name character varying(50)',
'email character varying(50)',
+ 'description character varying(100)',
'moment timestamp without time zone default now()'
]
PK_TABLE_NAME = 'table_with_pk'
@@ -54,6 +57,8 @@ class SchemaTest < ActiveRecord::TestCase
@connection.execute "CREATE INDEX #{INDEX_B_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_B_COLUMN_S2});"
@connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});"
@connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});"
+ @connection.execute "CREATE INDEX #{INDEX_D_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_D_COLUMN} DESC);"
+ @connection.execute "CREATE INDEX #{INDEX_D_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_D_COLUMN} DESC);"
@connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{PK_TABLE_NAME} (id serial primary key)"
end
@@ -184,11 +189,15 @@ class SchemaTest < ActiveRecord::TestCase
end
def test_dump_indexes_for_schema_one
- do_dump_index_tests_for_schema(SCHEMA_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S1)
+ do_dump_index_tests_for_schema(SCHEMA_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN)
end
def test_dump_indexes_for_schema_two
- do_dump_index_tests_for_schema(SCHEMA2_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S2)
+ do_dump_index_tests_for_schema(SCHEMA2_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S2, INDEX_D_COLUMN)
+ end
+
+ def test_dump_indexes_for_schema_multiple_schemas_in_search_path
+ do_dump_index_tests_for_schema("public, #{SCHEMA_NAME}", INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN)
end
def test_with_uppercase_index_name
@@ -288,13 +297,16 @@ class SchemaTest < ActiveRecord::TestCase
@connection.schema_search_path = "'$user', public"
end
- def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name)
+ def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name)
with_schema_search_path(this_schema_name) do
indexes = @connection.indexes(TABLE_NAME).sort_by {|i| i.name}
- assert_equal 2,indexes.size
+ assert_equal 3,indexes.size
do_dump_index_assertions_for_one_index(indexes[0], INDEX_A_NAME, first_index_column_name)
do_dump_index_assertions_for_one_index(indexes[1], INDEX_B_NAME, second_index_column_name)
+ do_dump_index_assertions_for_one_index(indexes[2], INDEX_D_NAME, third_index_column_name)
+
+ assert_equal :desc, indexes.select{|i| i.name == INDEX_D_NAME}[0].orders[INDEX_D_COLUMN]
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
new file mode 100644
index 0000000000..e18892821d
--- /dev/null
+++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
@@ -0,0 +1,23 @@
+require "cases/helper"
+require 'models/developer'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class SQLite3Adapter
+ class ExplainTest < ActiveRecord::TestCase
+ fixtures :developers
+
+ def test_explain_for_one_query
+ explain = Developer.where(:id => 1).explain
+ assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
+ end
+
+ def test_explain_with_eager_loading
+ explain = Developer.where(:id => 1).includes(:audit_logs).explain
+ assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
+ assert_match(/(SCAN )?TABLE audit_logs/, explain)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index b1b41fed0d..9300c57819 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -35,6 +35,24 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert !t.attribute_present?("content")
end
+ def test_attribute_present_with_booleans
+ b1 = Boolean.new
+ b1.value = false
+ assert b1.attribute_present?(:value)
+
+ b2 = Boolean.new
+ b2.value = true
+ assert b2.attribute_present?(:value)
+
+ b3 = Boolean.new
+ assert !b3.attribute_present?(:value)
+
+ b4 = Boolean.new
+ b4.value = false
+ b4.save!
+ assert Boolean.find(b4.id).attribute_present?(:value)
+ end
+
def test_attribute_keys_on_new_instance
t = Topic.new
assert_equal nil, t.title, "The topics table has a title column, so it should be nil"
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 12c1cfb30e..fdb656fe13 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1935,4 +1935,10 @@ class BasicsTest < ActiveRecord::TestCase
dev.update_attribute(:updated_at, nil)
assert_match(/\/#{dev.id}$/, dev.cache_key)
end
+
+ def test_uniq_delegates_to_scoped
+ scope = stub
+ Bird.stubs(:scoped).returns(mock(:uniq => scope))
+ assert_equal scope, Bird.uniq
+ end
end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 3088ab012f..05c4b15407 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -57,6 +57,11 @@ class FinderTest < ActiveRecord::TestCase
assert Topic.first.replies.exists?
end
+ # ensures +exists?+ runs valid SQL by excluding order value
+ def test_exists_with_order
+ assert Topic.order(:id).uniq.exists?
+ end
+
def test_does_not_exist_with_empty_table_and_no_args_given
Topic.delete_all
assert !Topic.exists?
@@ -369,6 +374,10 @@ class FinderTest < ActiveRecord::TestCase
assert_equal [1], Comment.find(:all, :conditions => { :id => 1..1, :post_id => 1..10 }).map(&:id).sort
end
+ def test_find_on_hash_conditions_with_array_of_integers_and_ranges
+ assert_equal [1,2,3,5,6,7,8,9], Comment.find(:all, :conditions => {:id => [1..2, 3, 5, 6..8, 9]}).map(&:id).sort
+ end
+
def test_find_on_multiple_hash_conditions
assert Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false })
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1, :conditions => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }) }
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 5c47a8ad33..75eb9c2bce 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -121,6 +121,18 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_nothing_raised { Person.connection.add_index("people", %w(last_name first_name administrator), :name => "named_admin") }
assert_nothing_raised { Person.connection.remove_index("people", :name => "named_admin") }
end
+
+ # Selected adapters support index sort order
+ if current_adapter?(:SQLite3Adapter, :MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
+ assert_nothing_raised { Person.connection.add_index("people", ["last_name"], :order => {:last_name => :desc}) }
+ assert_nothing_raised { Person.connection.remove_index("people", ["last_name"]) }
+ assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :order => {:last_name => :desc}) }
+ assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) }
+ assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :order => {:last_name => :desc, :first_name => :asc}) }
+ assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) }
+ assert_nothing_raised { Person.connection.add_index("people", ["last_name", "first_name"], :order => :desc) }
+ assert_nothing_raised { Person.connection.remove_index("people", ["last_name", "first_name"]) }
+ end
end
def test_index_symbol_names
@@ -471,7 +483,7 @@ if ActiveRecord::Base.connection.supports_migrations?
# Do a manual insertion
if current_adapter?(:OracleAdapter)
- Person.connection.execute "insert into people (id, wealth, created_at, updated_at) values (people_seq.nextval, 12345678901234567890.0123456789, 0, 0)"
+ Person.connection.execute "insert into people (id, wealth, created_at, updated_at) values (people_seq.nextval, 12345678901234567890.0123456789, sysdate, sysdate)"
elsif current_adapter?(:OpenBaseAdapter) || (current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003) #before mysql 5.0.3 decimals stored as strings
Person.connection.execute "insert into people (wealth, created_at, updated_at) values ('12345678901234567890.0123456789', 0, 0)"
elsif current_adapter?(:PostgreSQLAdapter)
@@ -1730,6 +1742,75 @@ if ActiveRecord::Base.connection.supports_migrations?
ensure
Person.connection.drop_table :testings rescue nil
end
+
+ def test_create_table_should_not_have_mixed_syntax
+ assert_raise(NoMethodError) do
+ Person.connection.create_table :testings, :force => true do |t|
+ t.string :foo
+ integer :bar
+ end
+ end
+ assert_raise(NameError) do
+ Person.connection.create_table :testings, :force => true do
+ t.string :foo
+ integer :bar
+ end
+ end
+ end
+
+ def test_change_table_without_block_parameter_no_bulk
+ Person.connection.create_table :testings, :force => true do
+ string :foo
+ end
+ assert Person.connection.column_exists?(:testings, :foo, :string)
+
+ Person.connection.change_table :testings do
+ remove :foo
+ integer :bar
+ end
+
+ assert_equal %w(bar id), Person.connection.columns(:testings).map { |c| c.name }.sort
+ ensure
+ Person.connection.drop_table :testings rescue nil
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_change_table_without_block_parameter_with_bulk
+ Person.connection.create_table :testings, :force => true do
+ string :foo
+ end
+ assert Person.connection.column_exists?(:testings, :foo, :string)
+
+ assert_queries(1) do
+ Person.connection.change_table(:testings, :bulk => true) do
+ integer :bar
+ string :foo_bar
+ end
+ end
+
+ assert_equal %w(bar foo foo_bar id), Person.connection.columns(:testings).map { |c| c.name }.sort
+ ensure
+ Person.connection.drop_table :testings rescue nil
+ end
+ end
+
+ def test_change_table_should_not_have_mixed_syntax
+ Person.connection.create_table :testings, :force => true do
+ string :foo
+ end
+ assert_raise(NoMethodError) do
+ Person.connection.change_table :testings do |t|
+ t.remove :foo
+ integer :bar
+ end
+ end
+ assert_raise(NameError) do
+ Person.connection.change_table :testings do
+ t.remove :foo
+ integer :bar
+ end
+ end
+ end
end # SexierMigrationsTest
class MigrationLoggerTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index b23ead6feb..715a378431 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -20,7 +20,7 @@ module ActiveRecord
end
def test_single_values
- assert_equal [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order].map(&:to_s).sort,
+ assert_equal [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order, :uniq].map(&:to_s).sort,
Relation::SINGLE_VALUE_METHODS.map(&:to_s).sort
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 95408a5f29..bf1eb6386a 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -1148,4 +1148,20 @@ class RelationTest < ActiveRecord::TestCase
assert_equal posts(:thinking), comments(:more_greetings).post
assert_equal posts(:welcome), comments(:greetings).post
end
+
+ def test_uniq
+ tag1 = Tag.create(:name => 'Foo')
+ tag2 = Tag.create(:name => 'Foo')
+
+ query = Tag.select(:name).where(:id => [tag1.id, tag2.id])
+
+ assert_equal ['Foo', 'Foo'], query.map(&:name)
+ assert_sql(/DISTINCT/) do
+ assert_equal ['Foo'], query.uniq.map(&:name)
+ end
+ assert_sql(/DISTINCT/) do
+ assert_equal ['Foo'], query.uniq(true).map(&:name)
+ end
+ assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name)
+ end
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 71ff727b7f..5c3a78688e 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -238,4 +238,9 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r(:id => false), match[1], "no table id not preserved"
assert_match %r{t.string[[:space:]]+"id",[[:space:]]+:null => false$}, match[2], "non-primary key id column not preserved"
end
+
+ def test_schema_dump_keeps_id_false_when_id_is_false_and_unique_not_null_column_added
+ output = standard_dump
+ assert_match %r{create_table "subscribers", :id => false}, output
+ end
end