Skip to content

Commit 110f2ed

Browse files
committed
Merge branch 'rm-create-with-index'
2 parents 9ed0cf5 + cbe1bc2 commit 110f2ed

File tree

6 files changed

+125
-66
lines changed

6 files changed

+125
-66
lines changed

activerecord/CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
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+
6+
It also allows creating and dropping indexed tables with fewer queries and fewer permissions
7+
required.
8+
9+
Example:
10+
11+
create_table :temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query" do |t|
12+
t.index :zip
13+
end
14+
# => CREATE TEMPORARY TABLE temp (INDEX (zip)) AS SELECT id, name, zip FROM a_really_complicated_query
15+
16+
*Cody Cutrer*, *Steve Rice*, *Rafael Mendonça Franca*
17+
118
* Save `has_one` association even if the record doesn't changed.
219

320
Fixes #14407.

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

Lines changed: 43 additions & 44 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
189+
if options[:id] != false && !options[:as]
190+
pk = options.fetch(:primary_key) do
191+
Base.get_primary_key table_name.to_s.singularize
196192
end
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
@@ -740,6 +739,40 @@ def update_table_definition(table_name, base) #:nodoc:
740739
Table.new(table_name, base)
741740
end
742741

742+
def add_index_options(table_name, column_name, options = {}) #:nodoc:
743+
column_names = Array(column_name)
744+
index_name = index_name(table_name, column: column_names)
745+
746+
options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
747+
748+
index_type = options[:unique] ? "UNIQUE" : ""
749+
index_type = options[:type].to_s if options.key?(:type)
750+
index_name = options[:name].to_s if options.key?(:name)
751+
max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
752+
753+
if options.key?(:algorithm)
754+
algorithm = index_algorithms.fetch(options[:algorithm]) {
755+
raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
756+
}
757+
end
758+
759+
using = "USING #{options[:using]}" if options[:using].present?
760+
761+
if supports_partial_index?
762+
index_options = options[:where] ? " WHERE #{options[:where]}" : ""
763+
end
764+
765+
if index_name.length > max_index_length
766+
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
767+
end
768+
if table_exists?(table_name) && index_name_exists?(table_name, index_name, false)
769+
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
770+
end
771+
index_columns = quoted_columns_for_index(column_names, options).join(", ")
772+
773+
[index_name, index_type, index_columns, index_options, algorithm, using]
774+
end
775+
743776
protected
744777
def add_index_sort_order(option_strings, column_names, options = {})
745778
if options.is_a?(Hash) && order = options[:order]
@@ -770,40 +803,6 @@ def options_include_default?(options)
770803
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
771804
end
772805

773-
def add_index_options(table_name, column_name, options = {})
774-
column_names = Array(column_name)
775-
index_name = index_name(table_name, column: column_names)
776-
777-
options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
778-
779-
index_type = options[:unique] ? "UNIQUE" : ""
780-
index_type = options[:type].to_s if options.key?(:type)
781-
index_name = options[:name].to_s if options.key?(:name)
782-
max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
783-
784-
if options.key?(:algorithm)
785-
algorithm = index_algorithms.fetch(options[:algorithm]) {
786-
raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
787-
}
788-
end
789-
790-
using = "USING #{options[:using]}" if options[:using].present?
791-
792-
if supports_partial_index?
793-
index_options = options[:where] ? " WHERE #{options[:where]}" : ""
794-
end
795-
796-
if index_name.length > max_index_length
797-
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
798-
end
799-
if index_name_exists?(table_name, index_name, false)
800-
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
801-
end
802-
index_columns = quoted_columns_for_index(column_names, options).join(", ")
803-
804-
[index_name, index_type, index_columns, index_options, algorithm, using]
805-
end
806-
807806
def index_name_for_remove(table_name, options = {})
808807
index_name = index_name(table_name, options)
809808

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -146,28 +146,24 @@ def adapter_name
146146
'Abstract'
147147
end
148148

149-
# Does this adapter support migrations? Backend specific, as the
150-
# abstract adapter always returns +false+.
149+
# Does this adapter support migrations?
151150
def supports_migrations?
152151
false
153152
end
154153

155154
# Can this adapter determine the primary key for tables not attached
156-
# to an Active Record class, such as join tables? Backend specific, as
157-
# the abstract adapter always returns +false+.
155+
# to an Active Record class, such as join tables?
158156
def supports_primary_key?
159157
false
160158
end
161159

162-
# Does this adapter support using DISTINCT within COUNT? This is +true+
163-
# for all adapters except sqlite.
160+
# Does this adapter support using DISTINCT within COUNT?
164161
def supports_count_distinct?
165162
true
166163
end
167164

168165
# Does this adapter support DDL rollbacks in transactions? That is, would
169-
# CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL,
170-
# SQL Server, and others support this. MySQL and others do not.
166+
# CREATE TABLE or ALTER TABLE get rolled back by a transaction?
171167
def supports_ddl_transactions?
172168
false
173169
end
@@ -176,16 +172,14 @@ def supports_bulk_alter?
176172
false
177173
end
178174

179-
# Does this adapter support savepoints? PostgreSQL and MySQL do,
180-
# SQLite < 3.6.8 does not.
175+
# Does this adapter support savepoints?
181176
def supports_savepoints?
182177
false
183178
end
184179

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

203-
# Does this adapter support explain? As of this writing sqlite3,
204-
# mysql2, and postgresql are the only ones that do.
197+
# Does this adapter support explain?
205198
def supports_explain?
206199
false
207200
end
@@ -211,12 +204,17 @@ def supports_transaction_isolation?
211204
false
212205
end
213206

214-
# Does this adapter support database extensions? As of this writing only
215-
# postgresql does.
207+
# Does this adapter support database extensions?
216208
def supports_extensions?
217209
false
218210
end
219211

212+
# Does this adapter support creating indexes in the same statement as
213+
# creating the table?
214+
def supports_indexes_in_create?
215+
false
216+
end
217+
220218
# This is meant to be implemented by the adapters that support extensions
221219
def disable_extension(name)
222220
end
@@ -225,14 +223,12 @@ def disable_extension(name)
225223
def enable_extension(name)
226224
end
227225

228-
# A list of extensions, to be filled in by adapters that support them. At
229-
# the moment only postgresql does.
226+
# A list of extensions, to be filled in by adapters that support them.
230227
def extensions
231228
[]
232229
end
233230

234231
# A list of index algorithms, to be filled by adapters that support them.
235-
# MySQL and PostgreSQL have support for them right now.
236232
def index_algorithms
237233
{}
238234
end
@@ -293,7 +289,6 @@ def clear_cache!
293289
end
294290

295291
# Returns true if its required to reload the connection between requests for development mode.
296-
# This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
297292
def requires_reloading?
298293
false
299294
end

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,25 @@ class AbstractMysqlAdapter < AbstractAdapter
66
include Savepoints
77

88
class SchemaCreation < AbstractAdapter::SchemaCreation
9-
109
def visit_AddColumn(o)
1110
add_column_position!(super, column_options(o))
1211
end
1312

1413
private
14+
15+
def visit_TableDefinition(o)
16+
name = o.name
17+
create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} "
18+
19+
statements = o.columns.map { |c| accept c }
20+
statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) })
21+
22+
create_sql << "(#{statements.join(', ')}) " if statements.present?
23+
create_sql << "#{o.options}"
24+
create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
25+
create_sql
26+
end
27+
1528
def visit_ChangeColumnDefinition(o)
1629
column = o.column
1730
options = o.options
@@ -29,6 +42,11 @@ def add_column_position!(sql, options)
2942
end
3043
sql
3144
end
45+
46+
def index_in_create(table_name, column_name, options)
47+
index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options)
48+
"#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}"
49+
end
3250
end
3351

3452
def schema_creation
@@ -225,6 +243,10 @@ def supports_transaction_isolation?
225243
version[0] >= 5
226244
end
227245

246+
def supports_indexes_in_create?
247+
true
248+
end
249+
228250
def native_database_types
229251
NATIVE_DATABASE_TYPES
230252
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+
ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false)
122+
ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
123+
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+
129+
assert_equal expected, actual
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+
ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false)
122+
ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
123+
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+
129+
assert_equal expected, actual
130+
end
131+
119132
private
120133
def with_real_execute
121134
ActiveRecord::Base.connection.singleton_class.class_eval do

0 commit comments

Comments
 (0)