Skip to content

Commit

Permalink
Merge branch 'rm-create-with-index'
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelfranca committed Mar 26, 2014
2 parents 9ed0cf5 + cbe1bc2 commit 110f2ed
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 66 deletions.
17 changes: 17 additions & 0 deletions activerecord/CHANGELOG.md
@@ -1,3 +1,20 @@
* Create indexes inline in CREATE TABLE for MySQL.

This is important, because adding an index on a temporary table after it has been created
would commit the transaction.

It also allows creating and dropping indexed tables with fewer queries and fewer permissions
required.

Example:

create_table :temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query" do |t|
t.index :zip
end
# => CREATE TEMPORARY TABLE temp (INDEX (zip)) AS SELECT id, name, zip FROM a_really_complicated_query

*Cody Cutrer*, *Steve Rice*, *Rafael Mendonça Franca*

* Save `has_one` association even if the record doesn't changed. * Save `has_one` association even if the record doesn't changed.


Fixes #14407. Fixes #14407.
Expand Down
Expand Up @@ -186,24 +186,23 @@ def column_exists?(table_name, column_name, type = nil, options = {})
def create_table(table_name, options = {}) def create_table(table_name, options = {})
td = create_table_definition table_name, options[:temporary], options[:options], options[:as] td = create_table_definition table_name, options[:temporary], options[:options], options[:as]


if !options[:as] if options[:id] != false && !options[:as]
unless options[:id] == false pk = options.fetch(:primary_key) do
pk = options.fetch(:primary_key) { Base.get_primary_key table_name.to_s.singularize
Base.get_primary_key table_name.to_s.singularize
}

td.primary_key pk, options.fetch(:id, :primary_key), options
end end


yield td if block_given? td.primary_key pk, options.fetch(:id, :primary_key), options
end end


yield td if block_given?

if options[:force] && table_exists?(table_name) if options[:force] && table_exists?(table_name)
drop_table(table_name, options) drop_table(table_name, options)
end end


execute schema_creation.accept td result = execute schema_creation.accept td
td.indexes.each_pair { |c,o| add_index table_name, c, o } td.indexes.each_pair { |c, o| add_index(table_name, c, o) } unless supports_indexes_in_create?
result
end end


# Creates a new join table with the name created using the lexical order of the first two # Creates a new join table with the name created using the lexical order of the first two
Expand Down Expand Up @@ -740,6 +739,40 @@ def update_table_definition(table_name, base) #:nodoc:
Table.new(table_name, base) Table.new(table_name, base)
end end


def add_index_options(table_name, column_name, options = {}) #:nodoc:
column_names = Array(column_name)
index_name = index_name(table_name, column: column_names)

options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)

index_type = options[:unique] ? "UNIQUE" : ""
index_type = options[:type].to_s if options.key?(:type)
index_name = options[:name].to_s if options.key?(:name)
max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length

if options.key?(:algorithm)
algorithm = index_algorithms.fetch(options[:algorithm]) {
raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
}
end

using = "USING #{options[:using]}" if options[:using].present?

if supports_partial_index?
index_options = options[:where] ? " WHERE #{options[:where]}" : ""
end

if index_name.length > max_index_length
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
end
if table_exists?(table_name) && index_name_exists?(table_name, index_name, false)
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
end
index_columns = quoted_columns_for_index(column_names, options).join(", ")

[index_name, index_type, index_columns, index_options, algorithm, using]
end

protected protected
def add_index_sort_order(option_strings, column_names, options = {}) def add_index_sort_order(option_strings, column_names, options = {})
if options.is_a?(Hash) && order = options[:order] if options.is_a?(Hash) && order = options[:order]
Expand Down Expand Up @@ -770,40 +803,6 @@ def options_include_default?(options)
options.include?(:default) && !(options[:null] == false && options[:default].nil?) options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end end


def add_index_options(table_name, column_name, options = {})
column_names = Array(column_name)
index_name = index_name(table_name, column: column_names)

options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)

index_type = options[:unique] ? "UNIQUE" : ""
index_type = options[:type].to_s if options.key?(:type)
index_name = options[:name].to_s if options.key?(:name)
max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length

if options.key?(:algorithm)
algorithm = index_algorithms.fetch(options[:algorithm]) {
raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
}
end

using = "USING #{options[:using]}" if options[:using].present?

if supports_partial_index?
index_options = options[:where] ? " WHERE #{options[:where]}" : ""
end

if index_name.length > max_index_length
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
end
if index_name_exists?(table_name, index_name, false)
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
end
index_columns = quoted_columns_for_index(column_names, options).join(", ")

[index_name, index_type, index_columns, index_options, algorithm, using]
end

def index_name_for_remove(table_name, options = {}) def index_name_for_remove(table_name, options = {})
index_name = index_name(table_name, options) index_name = index_name(table_name, options)


Expand Down
Expand Up @@ -146,28 +146,24 @@ def adapter_name
'Abstract' 'Abstract'
end end


# Does this adapter support migrations? Backend specific, as the # Does this adapter support migrations?
# abstract adapter always returns +false+.
def supports_migrations? def supports_migrations?
false false
end end


# Can this adapter determine the primary key for tables not attached # Can this adapter determine the primary key for tables not attached
# to an Active Record class, such as join tables? Backend specific, as # to an Active Record class, such as join tables?
# the abstract adapter always returns +false+.
def supports_primary_key? def supports_primary_key?
false false
end end


# Does this adapter support using DISTINCT within COUNT? This is +true+ # Does this adapter support using DISTINCT within COUNT?
# for all adapters except sqlite.
def supports_count_distinct? def supports_count_distinct?
true true
end end


# Does this adapter support DDL rollbacks in transactions? That is, would # Does this adapter support DDL rollbacks in transactions? That is, would
# CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL, # CREATE TABLE or ALTER TABLE get rolled back by a transaction?
# SQL Server, and others support this. MySQL and others do not.
def supports_ddl_transactions? def supports_ddl_transactions?
false false
end end
Expand All @@ -176,16 +172,14 @@ def supports_bulk_alter?
false false
end end


# Does this adapter support savepoints? PostgreSQL and MySQL do, # Does this adapter support savepoints?
# SQLite < 3.6.8 does not.
def supports_savepoints? def supports_savepoints?
false false
end end


# Should primary key values be selected from their corresponding # Should primary key values be selected from their corresponding
# sequence before the insert statement? If true, next_sequence_value # sequence before the insert statement? If true, next_sequence_value
# is called before each insert to set the record's primary key. # is called before each insert to set the record's primary key.
# This is false for all adapters but Firebird.
def prefetch_primary_key?(table_name = nil) def prefetch_primary_key?(table_name = nil)
false false
end end
Expand All @@ -200,8 +194,7 @@ def supports_partial_index?
false false
end end


# Does this adapter support explain? As of this writing sqlite3, # Does this adapter support explain?
# mysql2, and postgresql are the only ones that do.
def supports_explain? def supports_explain?
false false
end end
Expand All @@ -211,12 +204,17 @@ def supports_transaction_isolation?
false false
end end


# Does this adapter support database extensions? As of this writing only # Does this adapter support database extensions?
# postgresql does.
def supports_extensions? def supports_extensions?
false false
end end


# Does this adapter support creating indexes in the same statement as
# creating the table?
def supports_indexes_in_create?
false
end

# This is meant to be implemented by the adapters that support extensions # This is meant to be implemented by the adapters that support extensions
def disable_extension(name) def disable_extension(name)
end end
Expand All @@ -225,14 +223,12 @@ def disable_extension(name)
def enable_extension(name) def enable_extension(name)
end end


# A list of extensions, to be filled in by adapters that support them. At # A list of extensions, to be filled in by adapters that support them.
# the moment only postgresql does.
def extensions def extensions
[] []
end end


# A list of index algorithms, to be filled by adapters that support them. # A list of index algorithms, to be filled by adapters that support them.
# MySQL and PostgreSQL have support for them right now.
def index_algorithms def index_algorithms
{} {}
end end
Expand Down Expand Up @@ -293,7 +289,6 @@ def clear_cache!
end end


# Returns true if its required to reload the connection between requests for development mode. # Returns true if its required to reload the connection between requests for development mode.
# This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
def requires_reloading? def requires_reloading?
false false
end end
Expand Down
Expand Up @@ -6,12 +6,25 @@ class AbstractMysqlAdapter < AbstractAdapter
include Savepoints include Savepoints


class SchemaCreation < AbstractAdapter::SchemaCreation class SchemaCreation < AbstractAdapter::SchemaCreation

def visit_AddColumn(o) def visit_AddColumn(o)
add_column_position!(super, column_options(o)) add_column_position!(super, column_options(o))
end end


private private

def visit_TableDefinition(o)
name = o.name
create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} "

statements = o.columns.map { |c| accept c }
statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) })

create_sql << "(#{statements.join(', ')}) " if statements.present?
create_sql << "#{o.options}"
create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
create_sql
end

def visit_ChangeColumnDefinition(o) def visit_ChangeColumnDefinition(o)
column = o.column column = o.column
options = o.options options = o.options
Expand All @@ -29,6 +42,11 @@ def add_column_position!(sql, options)
end end
sql sql
end end

def index_in_create(table_name, column_name, options)
index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options)
"#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}"
end
end end


def schema_creation def schema_creation
Expand Down Expand Up @@ -225,6 +243,10 @@ def supports_transaction_isolation?
version[0] >= 5 version[0] >= 5
end end


def supports_indexes_in_create?
true
end

def native_database_types def native_database_types
NATIVE_DATABASE_TYPES NATIVE_DATABASE_TYPES
end end
Expand Down
15 changes: 14 additions & 1 deletion activerecord/test/cases/adapters/mysql/active_schema_test.rb
Expand Up @@ -17,7 +17,8 @@ def execute(sql, name = nil) return sql end
end end


def test_add_index def test_add_index
# add_index calls index_name_exists? which can't work since execute is stubbed # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed
def (ActiveRecord::Base.connection).table_exists?(*); true; end
def (ActiveRecord::Base.connection).index_name_exists?(*); false; end def (ActiveRecord::Base.connection).index_name_exists?(*); false; end


expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
Expand Down Expand Up @@ -116,6 +117,18 @@ def test_remove_timestamps
end end
end end


def test_indexes_in_create
ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false)
ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)

expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query"
actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
t.index :zip
end

assert_equal expected, actual
end

private private
def with_real_execute def with_real_execute
ActiveRecord::Base.connection.singleton_class.class_eval do ActiveRecord::Base.connection.singleton_class.class_eval do
Expand Down
15 changes: 14 additions & 1 deletion activerecord/test/cases/adapters/mysql2/active_schema_test.rb
Expand Up @@ -17,7 +17,8 @@ def execute(sql, name = nil) return sql end
end end


def test_add_index def test_add_index
# add_index calls index_name_exists? which can't work since execute is stubbed # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed
def (ActiveRecord::Base.connection).table_exists?(*); true; end
def (ActiveRecord::Base.connection).index_name_exists?(*); false; end def (ActiveRecord::Base.connection).index_name_exists?(*); false; end


expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
Expand Down Expand Up @@ -116,6 +117,18 @@ def test_remove_timestamps
end end
end end


def test_indexes_in_create
ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false)
ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)

expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query"
actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
t.index :zip
end

assert_equal expected, actual
end

private private
def with_real_execute def with_real_execute
ActiveRecord::Base.connection.singleton_class.class_eval do ActiveRecord::Base.connection.singleton_class.class_eval do
Expand Down

0 comments on commit 110f2ed

Please sign in to comment.