diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 3fe8d7ec2ee94..0c8c8c006e2a4 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -15,7 +15,7 @@ *Martin Schuerrer* -* Add suport for concurrent indexing in PostgreSQL adapter via the +* Add support for concurrent indexing in PostgreSQL adapter via the `algorithm: :concurrently` option add_index(:people, :last_name, algorithm: :concurrently) @@ -27,6 +27,14 @@ *Dan McClain* +* Add support for fulltext and spatial indexes on MySQL tables with MyISAM database + engine via the `type: 'FULLTEXT'` / `type: 'SPATIAL'` option + + add_index(:people, :last_name, type: 'FULLTEXT') + add_index(:people, :last_name, type: 'SPATIAL') + + *Ken Mazaika* + * Add an `add_index` override in Postgresql adapter and MySQL adapter to allow custom index type support. Fixes #6101. 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 2f17e47b7c32d..eb974e4a6e6ea 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -8,7 +8,7 @@ module ConnectionAdapters #:nodoc: # Abstract representation of an index definition on a table. Instances of # this type are typically created and returned by methods in database # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :using) #:nodoc: + class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using) #:nodoc: end # Abstract representation of a column definition. Instances of this type 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 e95e97e4a8c24..aebcc2d87472d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -498,13 +498,25 @@ def rename_column(table_name, column_name, new_column_name) # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active # # ====== Creating an index with a specific method + # # add_index(:developers, :name, :using => 'btree') + # # generates + # # CREATE INDEX index_developers_on_name ON developers USING btree (name) -- PostgreSQL # CREATE INDEX index_developers_on_name USING btree ON developers (name) -- MySQL # # Note: only supported by PostgreSQL and MySQL # + # ====== Creating an index with a specific type + # + # add_index(:developers, :name, :type => :fulltext) + # + # generates + # + # CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL + # + # Note: only supported by MySQL. Supported: :fulltext and :spatial on MyISAM tables. def add_index(table_name, column_name, options = {}) index_name, index_type, index_columns, index_options = 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})#{index_options}" @@ -755,9 +767,10 @@ def add_index_options(table_name, column_name, options = {}) index_name = index_name(table_name, column: column_names) if Hash === options # legacy support, since this param was a string - options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm) + 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 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 cc5e6ac44dfde..b0b160f9b4f45 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -151,6 +151,9 @@ def missing_default_forged_as_empty_string?(default) :boolean => { :name => "tinyint", :limit => 1 } } + INDEX_TYPES = [:fulltext, :spatial] + INDEX_USINGS = [:btree, :hash] + class BindSubstitution < Arel::Visitors::MySQL # :nodoc: include Arel::Visitors::BindVisitor end @@ -435,8 +438,11 @@ def indexes(table_name, name = nil) #:nodoc: if current_index != row[:Key_name] next if row[:Key_name] == 'PRIMARY' # skip the primary key current_index = row[:Key_name] - indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], []) - indexes.last.using = row[:Index_type].downcase.to_sym + + mysql_index_type = row[:Index_type].downcase.to_sym + index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil + index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil + indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using) end indexes.last.columns << row[:Column_name] diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 0e1afbae8d4ed..0168c36abc29b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -160,9 +160,9 @@ def indexes(table_name, name = nil) desc_order_columns = inddef.scan(/(\w+) DESC/).flatten orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {} where = inddef.scan(/WHERE (.+)$/).flatten[0] - type = inddef.scan(/USING (.+?) /).flatten[0].to_sym + using = inddef.scan(/USING (.+?) /).flatten[0].to_sym - column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, type) + column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using) end.compact end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 47cbdbef1a246..10c6d272cda94 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -187,6 +187,8 @@ def indexes(table, stream) statement_parts << ('using: ' + index.using.inspect) if index.using + statement_parts << ('type: ' + index.type.inspect) if index.type + ' ' + statement_parts.join(', ') end diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 9050ae3fe3445..e6d0183b119ef 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -36,9 +36,14 @@ def test_add_index expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10)) " assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10}) - %w(btree hash).each do |type| - expected = "CREATE INDEX `index_people_on_last_name` USING #{type} ON `people` (`last_name`) " - assert_equal expected, add_index(:people, :last_name, :using => type) + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) " + assert_equal expected, add_index(:people, :last_name, :type => type) + end + + %w(btree hash).each do |using| + expected = "CREATE INDEX `index_people_on_last_name` USING #{using} ON `people` (`last_name`) " + assert_equal expected, add_index(:people, :last_name, :using => using) end expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) " diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index e6e54bf20a302..807a7a155e3c6 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -37,22 +37,25 @@ def test_table_exists_wrong_schema end def test_dump_indexes - index_a_name = 'index_post_title' - index_b_name = 'index_post_body' + index_a_name = 'index_key_tests_on_snack' + index_b_name = 'index_key_tests_on_pizza' + index_c_name = 'index_key_tests_on_awesome' - table = Post.table_name - - @connection.execute "CREATE INDEX `#{index_a_name}` ON `#{table}` (`title`);" - @connection.execute "CREATE INDEX `#{index_b_name}` USING btree ON `#{table}` (`body`(10));" + table = 'key_tests' indexes = @connection.indexes(table).sort_by {|i| i.name} - assert_equal 2,indexes.size - - assert_equal :btree, indexes.select{|i| i.name == index_a_name}[0].using - assert_equal :btree, indexes.select{|i| i.name == index_b_name}[0].using - - @connection.execute "DROP INDEX `#{index_a_name}` ON `#{table}`;" - @connection.execute "DROP INDEX `#{index_b_name}` ON `#{table}`;" + assert_equal 3,indexes.size + + index_a = indexes.select{|i| i.name == index_a_name}[0] + index_b = indexes.select{|i| i.name == index_b_name}[0] + index_c = indexes.select{|i| i.name == index_c_name}[0] + assert_equal :btree, index_a.using + assert_nil index_a.type + assert_equal :btree, index_b.using + assert_nil index_b.type + + assert_nil index_c.using + assert_equal :fulltext, index_c.type end end end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 48d63aeef5105..8a2a7ef2698a8 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -36,9 +36,14 @@ def test_add_index expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10)) " assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10}) - %w(btree hash).each do |type| - expected = "CREATE INDEX `index_people_on_last_name` USING #{type} ON `people` (`last_name`) " - assert_equal expected, add_index(:people, :last_name, :using => type) + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) " + assert_equal expected, add_index(:people, :last_name, :type => type) + end + + %w(btree hash).each do |using| + expected = "CREATE INDEX `index_people_on_last_name` USING #{using} ON `people` (`last_name`) " + assert_equal expected, add_index(:people, :last_name, :using => using) end expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) " diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 78f754d2ce520..5db60ff8a0b91 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -45,22 +45,25 @@ def test_tables_quoting end def test_dump_indexes - index_a_name = 'index_post_title' - index_b_name = 'index_post_body' + index_a_name = 'index_key_tests_on_snack' + index_b_name = 'index_key_tests_on_pizza' + index_c_name = 'index_key_tests_on_awesome' - table = Post.table_name - - @connection.execute "CREATE INDEX `#{index_a_name}` ON `#{table}` (`title`);" - @connection.execute "CREATE INDEX `#{index_b_name}` USING btree ON `#{table}` (`body`(10));" + table = 'key_tests' indexes = @connection.indexes(table).sort_by {|i| i.name} - assert_equal 2,indexes.size + assert_equal 3,indexes.size - assert_equal :btree, indexes.select{|i| i.name == index_a_name}[0].using - assert_equal :btree, indexes.select{|i| i.name == index_b_name}[0].using + index_a = indexes.select{|i| i.name == index_a_name}[0] + index_b = indexes.select{|i| i.name == index_b_name}[0] + index_c = indexes.select{|i| i.name == index_c_name}[0] + assert_equal :btree, index_a.using + assert_nil index_a.type + assert_equal :btree, index_b.using + assert_nil index_b.type - @connection.execute "DROP INDEX `#{index_a_name}` ON `#{table}`;" - @connection.execute "DROP INDEX `#{index_b_name}` ON `#{table}`;" + assert_nil index_c.using + assert_equal :fulltext, index_c.type end end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index a50a138fb0f59..6e03a2bd677ab 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -225,6 +225,12 @@ def test_schema_dump_includes_length_for_mysql_blob_and_text_fields assert_match %r{t.text\s+"medium_text",\s+limit: 16777215$}, output assert_match %r{t.text\s+"long_text",\s+limit: 2147483647$}, output end + + def test_schema_dumps_index_type + output = standard_dump + assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output + assert_match %r{add_index "key_tests", \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output + end end def test_schema_dump_includes_decimal_options diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index f25f72c48157b..1b1457ab9c03e 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -14,6 +14,16 @@ add_index :binary_fields, :var_binary + create_table :key_tests, force: true, :options => 'ENGINE=MyISAM' do |t| + t.string :awesome + t.string :pizza + t.string :snacks + end + + add_index :key_tests, :awesome, :type => :fulltext, :name => 'index_key_tests_on_awesome' + add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' + add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; SQL diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb index 5401c12ed5b99..ecdce1519b1da 100644 --- a/activerecord/test/schema/mysql_specific_schema.rb +++ b/activerecord/test/schema/mysql_specific_schema.rb @@ -14,6 +14,16 @@ add_index :binary_fields, :var_binary + create_table :key_tests, force: true, :options => 'ENGINE=MyISAM' do |t| + t.string :awesome + t.string :pizza + t.string :snacks + end + + add_index :key_tests, :awesome, :type => :fulltext, :name => 'index_key_tests_on_awesome' + add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' + add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; SQL