From 9600e0b02973ce5dd36642511d542a6b62983a5e Mon Sep 17 00:00:00 2001
From: Ken Mazaika <kenmazaika@gmail.com>
Date: Wed, 27 Mar 2013 00:30:11 -0400
Subject: Add support for FULLTEXT and SPATIAL indexes using the :type flag for
 MySQL.

---
 activerecord/CHANGELOG.md                          | 10 +++++++-
 .../abstract/schema_definitions.rb                 |  2 +-
 .../abstract/schema_statements.rb                  | 15 ++++++++++-
 .../connection_adapters/abstract_mysql_adapter.rb  | 10 ++++++--
 .../postgresql/schema_statements.rb                |  4 +--
 activerecord/lib/active_record/schema_dumper.rb    |  2 ++
 .../cases/adapters/mysql/active_schema_test.rb     | 11 +++++---
 .../test/cases/adapters/mysql/schema_test.rb       | 29 ++++++++++++----------
 .../cases/adapters/mysql2/active_schema_test.rb    | 11 +++++---
 .../test/cases/adapters/mysql2/schema_test.rb      | 25 +++++++++++--------
 activerecord/test/cases/schema_dumper_test.rb      |  6 +++++
 activerecord/test/schema/mysql2_specific_schema.rb | 10 ++++++++
 activerecord/test/schema/mysql_specific_schema.rb  | 10 ++++++++
 13 files changed, 108 insertions(+), 37 deletions(-)

diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 3fe8d7ec2e..0c8c8c006e 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 2f17e47b7c..eb974e4a6e 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 ActiveRecord
     # 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 e95e97e4a8..aebcc2d874 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 @@ module ActiveRecord
       #   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: <tt>:fulltext</tt> and <tt>:spatial</tt> 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 @@ module ActiveRecord
           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 cc5e6ac44d..b0b160f9b4 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 @@ module ActiveRecord
         :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 @@ module ActiveRecord
             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 0e1afbae8d..0168c36abc 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 @@ module ActiveRecord
             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 47cbdbef1a..10c6d272cd 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -187,6 +187,8 @@ HEADER
 
             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 9050ae3fe3..e6d0183b11 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 @@ class ActiveSchemaTest < ActiveRecord::TestCase
     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 e6e54bf20a..807a7a155e 100644
--- a/activerecord/test/cases/adapters/mysql/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/schema_test.rb
@@ -37,22 +37,25 @@ module ActiveRecord
       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 48d63aeef5..8a2a7ef269 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 @@ class ActiveSchemaTest < ActiveRecord::TestCase
     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 78f754d2ce..5db60ff8a0 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb
@@ -45,22 +45,25 @@ module ActiveRecord
       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 a50a138fb0..6e03a2bd67 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -225,6 +225,12 @@ class SchemaDumperTest < ActiveRecord::TestCase
       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 f25f72c481..1b1457ab9c 100644
--- a/activerecord/test/schema/mysql2_specific_schema.rb
+++ b/activerecord/test/schema/mysql2_specific_schema.rb
@@ -14,6 +14,16 @@ ActiveRecord::Schema.define do
 
   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 5401c12ed5..ecdce1519b 100644
--- a/activerecord/test/schema/mysql_specific_schema.rb
+++ b/activerecord/test/schema/mysql_specific_schema.rb
@@ -14,6 +14,16 @@ ActiveRecord::Schema.define do
 
   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
-- 
cgit v1.2.3