Skip to content

Commit fb522a2

Browse files
committed
Merge pull request rails#14480 from steverice/mysql-indexes-in-create-table
Create indexes inline in CREATE TABLE for MySQL
2 parents 9ed0cf5 + 63c94ef commit fb522a2

File tree

6 files changed

+80
-14
lines changed

6 files changed

+80
-14
lines changed

activerecord/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
* Create indexes inline in CREATE TABLE for MySQL
2+
3+
This is important, because adding an index on a temporary table after it has been created
4+
would commit the transaction.
5+
It also allows creating and dropping indexed tables with fewer queries and fewer permissions required.
6+
7+
Example:
8+
9+
create_table :temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query" do |t|
10+
t.index :zip
11+
end
12+
# => CREATE TEMPORARY TABLE temp (INDEX (zip)) AS SELECT id, name, zip FROM a_really_complicated_query
13+
14+
*Cody Cutrer*, *Steve Rice*
15+
116
* Save `has_one` association even if the record doesn't changed.
217

318
Fixes #14407.

activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -186,24 +186,23 @@ def column_exists?(table_name, column_name, type = nil, options = {})
186186
def create_table(table_name, options = {})
187187
td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
188188

189-
if !options[:as]
190-
unless options[:id] == false
191-
pk = options.fetch(:primary_key) {
192-
Base.get_primary_key table_name.to_s.singularize
193-
}
194-
195-
td.primary_key pk, options.fetch(:id, :primary_key), options
196-
end
189+
unless options[:id] == false || options[:as]
190+
pk = options.fetch(:primary_key) {
191+
Base.get_primary_key table_name.to_s.singularize
192+
}
197193

198-
yield td if block_given?
194+
td.primary_key pk, options.fetch(:id, :primary_key), options
199195
end
200196

197+
yield td if block_given?
198+
201199
if options[:force] && table_exists?(table_name)
202200
drop_table(table_name, options)
203201
end
204202

205-
execute schema_creation.accept td
206-
td.indexes.each_pair { |c,o| add_index table_name, c, o }
203+
result = execute schema_creation.accept td
204+
td.indexes.each_pair { |c,o| add_index table_name, c, o } unless supports_indexes_in_create?
205+
result
207206
end
208207

209208
# Creates a new join table with the name created using the lexical order of the first two
@@ -796,7 +795,7 @@ def add_index_options(table_name, column_name, options = {})
796795
if index_name.length > max_index_length
797796
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
798797
end
799-
if index_name_exists?(table_name, index_name, false)
798+
if table_exists?(table_name) && index_name_exists?(table_name, index_name, false)
800799
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
801800
end
802801
index_columns = quoted_columns_for_index(column_names, options).join(", ")

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,12 @@ def supports_extensions?
217217
false
218218
end
219219

220+
# Does this adapter support creating indexes in the same statement as
221+
# creating the table? As of this writing, only mysql does.
222+
def supports_indexes_in_create?
223+
false
224+
end
225+
220226
# This is meant to be implemented by the adapters that support extensions
221227
def disable_extension(name)
222228
end

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ class AbstractMysqlAdapter < AbstractAdapter
66
include Savepoints
77

88
class SchemaCreation < AbstractAdapter::SchemaCreation
9+
def visit_TableDefinition(o)
10+
create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "
11+
create_sql << "#{quote_table_name(o.name)} "
12+
statements = []
13+
statements.concat(o.columns.map { |c| accept c })
14+
statements.concat(o.indexes.map { |(column_name, options)| index_in_create(o.name, column_name, options) })
15+
create_sql << "(#{statements.join(', ')}) " if statements.present?
16+
create_sql << "#{o.options}"
17+
create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
18+
create_sql
19+
end
920

1021
def visit_AddColumn(o)
1122
add_column_position!(super, column_options(o))
@@ -29,6 +40,11 @@ def add_column_position!(sql, options)
2940
end
3041
sql
3142
end
43+
44+
def index_in_create(table_name, column_name, options)
45+
index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.send(:add_index_options, table_name, column_name, options)
46+
"#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}".gsub(' ', ' ').strip
47+
end
3248
end
3349

3450
def schema_creation
@@ -225,6 +241,10 @@ def supports_transaction_isolation?
225241
version[0] >= 5
226242
end
227243

244+
def supports_indexes_in_create?
245+
true
246+
end
247+
228248
def native_database_types
229249
NATIVE_DATABASE_TYPES
230250
end

activerecord/test/cases/adapters/mysql/active_schema_test.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ def execute(sql, name = nil) return sql end
1717
end
1818

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

2324
expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
@@ -116,6 +117,18 @@ def test_remove_timestamps
116117
end
117118
end
118119

120+
def test_indexes_in_create
121+
begin
122+
ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false)
123+
ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
124+
expected = "CREATE TEMPORARY TABLE `temp` (INDEX `index_temp_on_zip` (`zip`)) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query"
125+
actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
126+
t.index :zip
127+
end
128+
assert_equal expected, actual
129+
end
130+
end
131+
119132
private
120133
def with_real_execute
121134
ActiveRecord::Base.connection.singleton_class.class_eval do

activerecord/test/cases/adapters/mysql2/active_schema_test.rb

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ def execute(sql, name = nil) return sql end
1717
end
1818

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

2324
expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
@@ -116,6 +117,18 @@ def test_remove_timestamps
116117
end
117118
end
118119

120+
def test_indexes_in_create
121+
begin
122+
ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false)
123+
ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
124+
expected = "CREATE TEMPORARY TABLE `temp` (INDEX `index_temp_on_zip` (`zip`)) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query"
125+
actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
126+
t.index :zip
127+
end
128+
assert_equal expected, actual
129+
end
130+
end
131+
119132
private
120133
def with_real_execute
121134
ActiveRecord::Base.connection.singleton_class.class_eval do

0 commit comments

Comments
 (0)