├── .gitignore ├── .rspec ├── .travis.yml ├── Appraisals ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── arel-mysql-index-hint.gemspec ├── docker-compose.yml ├── gemfiles ├── activerecord_4.0.gemfile ├── activerecord_4.1.gemfile ├── activerecord_4.2.gemfile └── activerecord_5.0.gemfile ├── lib ├── arel-mysql-index-hint.rb └── arel-mysql-index-hint │ ├── active_record-hint_methods.rb │ ├── arel-table.rb │ ├── arel-visitors-mysql.rb │ └── version.rb └── spec ├── arel-mysql-index-hint ├── eager_load_spec.rb ├── includes_spec.rb ├── joins_spec.rb ├── preload_spec.rb ├── unnested_hint_spec.rb └── without_join_spec.rb ├── init.sql ├── models.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | test.rb 16 | /gemfiles/*.lock 17 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | language: ruby 4 | rvm: 5 | - 2.2.4 6 | - 2.3.0 7 | before_install: 8 | - gem install bundler 9 | before_script: 10 | - docker-compose up -d 11 | - function mysql_ping { mysqladmin -u root -h 127.0.0.1 -ppassword ping > /dev/null 2> /dev/null; } 12 | - for i in {1..60}; do mysql_ping && break; sleep 1; done 13 | script: 14 | - bundle install 15 | - bundle exec rake 16 | gemfile: 17 | - gemfiles/activerecord_4.0.gemfile 18 | - gemfiles/activerecord_4.1.gemfile 19 | - gemfiles/activerecord_4.2.gemfile 20 | - gemfiles/activerecord_5.0.gemfile 21 | services: 22 | - docker 23 | addons: 24 | apt: 25 | packages: 26 | - mysql-client-core-5.6 27 | - mysql-client-5.6 28 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "activerecord-4.0" do 2 | gem "activerecord", "~> 4.0.0" 3 | gem "mysql2", "~> 0.3.10" 4 | end 5 | 6 | appraise "activerecord-4.1" do 7 | gem "activerecord", "~> 4.1.0" 8 | gem "mysql2", "~> 0.3.10" 9 | end 10 | 11 | appraise "activerecord-4.2" do 12 | gem "activerecord", "~> 4.2.0" 13 | end 14 | 15 | appraise "activerecord-5.0" do 16 | gem "activerecord", "~> 5.0.0.beta4" 17 | end 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in arel-mysql-index-hint.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Genki Sugawara 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # arel-mysql-index-hint 2 | 3 | Add index hint to MySQL query in Arel. 4 | 5 | [![Gem Version](https://badge.fury.io/rb/arel-mysql-index-hint.svg)](http://badge.fury.io/rb/arel-mysql-index-hint) 6 | [![Build Status](https://travis-ci.org/winebarrel/arel-mysql-index-hint.svg?branch=master)](https://travis-ci.org/winebarrel/arel-mysql-index-hint) 7 | [![Coverage Status](https://img.shields.io/coveralls/winebarrel/arel-mysql-index-hint.svg)](https://coveralls.io/r/winebarrel/arel-mysql-index-hint?branch=master) 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem 'arel-mysql-index-hint' 15 | ``` 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install arel-mysql-index-hint 24 | 25 | ## Usage 26 | 27 | ```ruby 28 | Article.hint(force: :idx_article) 29 | #=> "SELECT `articles`.* FROM `articles` FORCE INDEX (`idx_article`)" 30 | 31 | Article.hint(force: [:idx_article, :idx_article2]) 32 | #=> "SELECT `articles`.* FROM `articles` FORCE INDEX (`idx_article`, `idx_article`)" 33 | 34 | Article.hint(use: :idx_article, ignore: :idx_article2) 35 | #=> "SELECT `articles`.* FROM `articles` USE INDEX (`idx_article`) IGNORE INDEX (`idx_article`)" 36 | 37 | Article.joins(:comments).hint(articles: {use: :idx_article}) 38 | #=> "SELECT `articles`.* FROM `articles` USE INDEX (`idx_article`) INNER JOIN `comments` ON `comments` 39 | 40 | Article.joins(:comments).hint(comments: {force: :idx_comment}) 41 | #=> "SELECT `articles`.* FROM `articles` INNER JOIN `comments` FORCE INDEX (`idx_comment`) ON `comments" 42 | ``` 43 | 44 | ## Running tests 45 | 46 | ```sh 47 | docker-compose up -d 48 | bundle install 49 | bundle exec appraisal install 50 | bundle exec appraisal activerecord-4.0 rake 51 | bundle exec appraisal activerecord-4.1 rake 52 | bundle exec appraisal activerecord-4.2 rake 53 | bundle exec appraisal activerecord-5.0 rake 54 | ``` 55 | 56 | **Notice:** mysql-client is required. 57 | 58 | ### on OS X (docker-machine & VirtualBox) 59 | 60 | Port forwarding is required. 61 | 62 | ```sh 63 | VBoxManage controlvm default natpf1 "mysql,tcp,127.0.0.1,3306,,3306" 64 | ``` 65 | 66 | ## Related Links 67 | 68 | * [MySQL::MySQL 5.6 Reference Manual::13.2.9.3 Index Hint Syntax](https://dev.mysql.com/doc/refman/5.6/en/index-hints.html) 69 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new("spec") 5 | task :default => :spec 6 | -------------------------------------------------------------------------------- /arel-mysql-index-hint.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'arel-mysql-index-hint/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "arel-mysql-index-hint" 8 | spec.version = ArelMysqlIndexHint::VERSION 9 | spec.authors = ["Genki Sugawara"] 10 | spec.email = ["sgwr_dts@yahoo.co.jp"] 11 | spec.summary = %q{Add index hint to MySQL query in Arel.} 12 | spec.description = %q{Add index hint to MySQL query in Arel.} 13 | spec.homepage = "https://github.com/winebarrel/arel-mysql-index-hint" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "activerecord", ">= 4.0.0", "< 5" 22 | spec.add_dependency "arel", ">= 4.0.0" 23 | 24 | spec.add_development_dependency "bundler" 25 | spec.add_development_dependency "rake" 26 | spec.add_development_dependency "mysql2" 27 | spec.add_development_dependency "rspec", ">= 3.0.0" 28 | spec.add_development_dependency "factory_girl" 29 | spec.add_development_dependency "coveralls" 30 | spec.add_development_dependency "appraisal" 31 | end 32 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | mysql: 2 | image: "mysql:5.6" 3 | ports: 4 | - "3306:3306" 5 | environment: 6 | MYSQL_ROOT_PASSWORD: password 7 | -------------------------------------------------------------------------------- /gemfiles/activerecord_4.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 4.0.0" 6 | gem "mysql2", "~> 0.3.10" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /gemfiles/activerecord_4.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 4.1.0" 6 | gem "mysql2", "~> 0.3.10" 7 | 8 | gemspec :path => "../" 9 | -------------------------------------------------------------------------------- /gemfiles/activerecord_4.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 4.2.0" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /gemfiles/activerecord_5.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 5.0.0.beta4" 6 | 7 | gemspec :path => "../" 8 | -------------------------------------------------------------------------------- /lib/arel-mysql-index-hint.rb: -------------------------------------------------------------------------------- 1 | require "active_support" 2 | require "arel-mysql-index-hint/version" 3 | 4 | ActiveSupport.on_load :active_record do 5 | require "arel-mysql-index-hint/active_record-hint_methods" 6 | require "arel-mysql-index-hint/arel-table" 7 | require "arel-mysql-index-hint/arel-visitors-mysql" 8 | 9 | ActiveRecord::Relation.class_eval do 10 | prepend ArelMysqlIndexHint::ActiveRecordHintMethods 11 | end 12 | 13 | ActiveRecord::Querying.class_eval do 14 | delegate :hint, :to => :all 15 | end 16 | 17 | Arel::Table.class_eval do 18 | prepend ArelMysqlIndexHint::ArelTable 19 | end 20 | 21 | Arel::Visitors::MySQL.class_eval do 22 | prepend ArelMysqlIndexHint::ArelVisitorsMySQL 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/arel-mysql-index-hint/active_record-hint_methods.rb: -------------------------------------------------------------------------------- 1 | module ArelMysqlIndexHint 2 | module ActiveRecordHintMethods 3 | def mysql_index_hint_value=(value) 4 | @values[:mysql_index_hint] = value 5 | end 6 | 7 | def mysql_index_hint_value 8 | @values[:mysql_index_hint] 9 | end 10 | 11 | def hint(index_hint_by_table) 12 | self.mysql_index_hint_value ||= {}.with_indifferent_access 13 | mysql_index_hint_value.deep_merge!(index_hint_by_table) 14 | self 15 | end 16 | 17 | def build_arel 18 | arel = super 19 | 20 | if mysql_index_hint_value.present? 21 | if mysql_index_hint_value.values.any? {|i| i.is_a?(Hash) } 22 | index_hint_by_table = mysql_index_hint_value 23 | else 24 | index_hint_by_table = Hash.new(mysql_index_hint_value) 25 | end 26 | 27 | arel.ast.select {|i| i.is_a?(Arel::Table) }.each do |node| 28 | node.index_hint = index_hint_by_table[node.name] 29 | end 30 | end 31 | 32 | arel 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/arel-mysql-index-hint/arel-table.rb: -------------------------------------------------------------------------------- 1 | module ArelMysqlIndexHint 2 | module ArelTable 3 | attr_accessor :index_hint 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/arel-mysql-index-hint/arel-visitors-mysql.rb: -------------------------------------------------------------------------------- 1 | module ArelMysqlIndexHint 2 | module ArelVisitorsMySQL 3 | def visit_Arel_Table(o, a) 4 | sql = super 5 | 6 | if o.index_hint 7 | append_index_hint(sql, o.index_hint) 8 | else 9 | sql 10 | end 11 | end 12 | 13 | private 14 | 15 | def append_index_hint(sql, index_hint) 16 | index_hint_sql = index_hint.map {|hint_type, index| 17 | index = Array(index).map {|i| quote_table_name(i) } 18 | hint_type = hint_type.to_s.upcase 19 | "#{hint_type} INDEX (#{index.join(', ')})" 20 | }.join(" ") 21 | 22 | if sql.is_a?(String) 23 | sql + " " + index_hint_sql 24 | else 25 | sql << " " + index_hint_sql 26 | sql 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/arel-mysql-index-hint/version.rb: -------------------------------------------------------------------------------- 1 | module ArelMysqlIndexHint 2 | VERSION = "0.2.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/arel-mysql-index-hint/eager_load_spec.rb: -------------------------------------------------------------------------------- 1 | if ActiveRecord::VERSION::STRING >= "4.1" 2 | describe "arel-mysql-index-hint" do 3 | describe "#eager_load" do 4 | context "when single index" do 5 | subject do 6 | User. 7 | eager_load(:microposts). 8 | where(microposts: {id: 1}). 9 | hint(microposts: {hint_type => :index_microposts_on_user_id_and_created_at}). 10 | to_sql.gsub(/\s+/, " ") 11 | end 12 | 13 | let(:sql) do 14 | "SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`email` AS t0_r2, `users`.`created_at` AS t0_r3, `users`.`updated_at` AS t0_r4, `users`.`password_digest` AS t0_r5, `users`.`remember_token` AS t0_r6, `users`.`admin` AS t0_r7, `microposts`.`id` AS t1_r0, `microposts`.`content` AS t1_r1, `microposts`.`user_id` AS t1_r2, `microposts`.`created_at` AS t1_r3, `microposts`.`updated_at` AS t1_r4 " + 15 | "FROM `users` " + 16 | "LEFT OUTER JOIN `microposts` " + 17 | "#{hint_type.to_s.upcase} INDEX (`index_microposts_on_user_id_and_created_at`) " + 18 | "ON `microposts`.`user_id` = `users`.`id` " + 19 | "WHERE `microposts`.`id` = 1" 20 | end 21 | 22 | let(:hint_type) { :force } 23 | 24 | it { is_expected.to eq sql } 25 | end 26 | 27 | context "when multiple indexes" do 28 | subject do 29 | User. 30 | eager_load(:microposts). 31 | where(microposts: {id: 1}). 32 | hint( 33 | users: {hint_type => :index_users_on_email}, 34 | microposts: {hint_type => :index_microposts_on_user_id_and_created_at}, 35 | ). 36 | to_sql.gsub(/\s+/, " ") 37 | end 38 | 39 | let(:sql) do 40 | "SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`email` AS t0_r2, `users`.`created_at` AS t0_r3, `users`.`updated_at` AS t0_r4, `users`.`password_digest` AS t0_r5, `users`.`remember_token` AS t0_r6, `users`.`admin` AS t0_r7, `microposts`.`id` AS t1_r0, `microposts`.`content` AS t1_r1, `microposts`.`user_id` AS t1_r2, `microposts`.`created_at` AS t1_r3, `microposts`.`updated_at` AS t1_r4 " + 41 | "FROM `users` " + 42 | "#{hint_type.to_s.upcase} INDEX (`index_users_on_email`) " + 43 | "LEFT OUTER JOIN `microposts` " + 44 | "#{hint_type.to_s.upcase} INDEX (`index_microposts_on_user_id_and_created_at`) " + 45 | "ON `microposts`.`user_id` = `users`.`id` " + 46 | "WHERE `microposts`.`id` = 1" 47 | end 48 | 49 | let(:hint_type) { :force } 50 | 51 | it { is_expected.to eq sql } 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/arel-mysql-index-hint/includes_spec.rb: -------------------------------------------------------------------------------- 1 | if ActiveRecord::VERSION::STRING >= "4.1" 2 | describe "arel-mysql-index-hint" do 3 | describe "#includes" do 4 | context "when single index" do 5 | subject do 6 | User. 7 | includes(:microposts). 8 | where(microposts: {id: 1}). 9 | hint(microposts: {hint_type => :index_microposts_on_user_id_and_created_at}). 10 | to_sql.gsub(/\s+/, " ") 11 | end 12 | 13 | let(:sql) do 14 | "SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`email` AS t0_r2, `users`.`created_at` AS t0_r3, `users`.`updated_at` AS t0_r4, `users`.`password_digest` AS t0_r5, `users`.`remember_token` AS t0_r6, `users`.`admin` AS t0_r7, `microposts`.`id` AS t1_r0, `microposts`.`content` AS t1_r1, `microposts`.`user_id` AS t1_r2, `microposts`.`created_at` AS t1_r3, `microposts`.`updated_at` AS t1_r4 " + 15 | "FROM `users` " + 16 | "LEFT OUTER JOIN `microposts` " + 17 | "#{hint_type.to_s.upcase} INDEX (`index_microposts_on_user_id_and_created_at`) " + 18 | "ON `microposts`.`user_id` = `users`.`id` " + 19 | "WHERE `microposts`.`id` = 1" 20 | end 21 | 22 | let(:hint_type) { :force } 23 | 24 | it { is_expected.to eq sql } 25 | end 26 | 27 | context "when multiple indexes" do 28 | subject do 29 | User. 30 | includes(:microposts). 31 | where(microposts: {id: 1}). 32 | hint( 33 | users: {hint_type => :index_users_on_email}, 34 | microposts: {hint_type => :index_microposts_on_user_id_and_created_at}, 35 | ). 36 | to_sql.gsub(/\s+/, " ") 37 | end 38 | 39 | let(:sql) do 40 | "SELECT `users`.`id` AS t0_r0, `users`.`name` AS t0_r1, `users`.`email` AS t0_r2, `users`.`created_at` AS t0_r3, `users`.`updated_at` AS t0_r4, `users`.`password_digest` AS t0_r5, `users`.`remember_token` AS t0_r6, `users`.`admin` AS t0_r7, `microposts`.`id` AS t1_r0, `microposts`.`content` AS t1_r1, `microposts`.`user_id` AS t1_r2, `microposts`.`created_at` AS t1_r3, `microposts`.`updated_at` AS t1_r4 " + 41 | "FROM `users` " + 42 | "#{hint_type.to_s.upcase} INDEX (`index_users_on_email`) " + 43 | "LEFT OUTER JOIN `microposts` " + 44 | "#{hint_type.to_s.upcase} INDEX (`index_microposts_on_user_id_and_created_at`) " + 45 | "ON `microposts`.`user_id` = `users`.`id` " + 46 | "WHERE `microposts`.`id` = 1" 47 | end 48 | 49 | let(:hint_type) { :force } 50 | 51 | it { is_expected.to eq sql } 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/arel-mysql-index-hint/joins_spec.rb: -------------------------------------------------------------------------------- 1 | describe "arel-mysql-index-hint" do 2 | describe "#joins" do 3 | context "when single index" do 4 | subject do 5 | User. 6 | joins(:microposts). 7 | hint(microposts: {hint_type => :index_microposts_on_user_id_and_created_at}). 8 | to_sql.gsub(/\s+/, " ") 9 | end 10 | 11 | let(:sql) do 12 | "SELECT `users`.* FROM `users` " + 13 | "INNER JOIN `microposts` " + 14 | "#{hint_type.to_s.upcase} INDEX (`index_microposts_on_user_id_and_created_at`) " + 15 | "ON `microposts`.`user_id` = `users`.`id`" 16 | end 17 | 18 | context "force index" do 19 | let(:hint_type) { :force } 20 | it { is_expected.to eq sql } 21 | end 22 | 23 | context "use index" do 24 | let(:hint_type) { :user } 25 | it { is_expected.to eq sql } 26 | end 27 | 28 | context "ignore index" do 29 | let(:hint_type) { :ignore } 30 | it { is_expected.to eq sql } 31 | end 32 | end 33 | 34 | context "when multiple indexes" do 35 | subject do 36 | User. 37 | joins(:microposts). 38 | hint( 39 | users: {hint_type => :index_users_on_email}, 40 | microposts: {hint_type => :index_microposts_on_user_id_and_created_at}, 41 | ). 42 | to_sql.gsub(/\s+/, " ") 43 | end 44 | 45 | let(:sql) do 46 | "SELECT `users`.* FROM `users` " + 47 | "#{hint_type.to_s.upcase} INDEX (`index_users_on_email`) " + 48 | "INNER JOIN `microposts` " + 49 | "#{hint_type.to_s.upcase} INDEX (`index_microposts_on_user_id_and_created_at`) " + 50 | "ON `microposts`.`user_id` = `users`.`id`" 51 | end 52 | 53 | context "force index" do 54 | let(:hint_type) { :force } 55 | it { is_expected.to eq sql } 56 | end 57 | 58 | context "use index" do 59 | let(:hint_type) { :user } 60 | it { is_expected.to eq sql } 61 | end 62 | 63 | context "ignore index" do 64 | let(:hint_type) { :ignore } 65 | it { is_expected.to eq sql } 66 | end 67 | end 68 | 69 | context "when add hint before joins" do 70 | subject do 71 | User. 72 | hint(microposts: {hint_type => :index_microposts_on_user_id_and_created_at}). 73 | joins(:microposts). 74 | to_sql.gsub(/\s+/, " ") 75 | end 76 | 77 | let(:sql) do 78 | "SELECT `users`.* FROM `users` " + 79 | "INNER JOIN `microposts` " + 80 | "#{hint_type.to_s.upcase} INDEX (`index_microposts_on_user_id_and_created_at`) " + 81 | "ON `microposts`.`user_id` = `users`.`id`" 82 | end 83 | 84 | let(:hint_type) { :force } 85 | 86 | it { is_expected.to eq sql } 87 | end 88 | 89 | context "when without index" do 90 | subject do 91 | User. 92 | joins(:microposts). 93 | to_sql.gsub(/\s+/, " ") 94 | end 95 | 96 | let(:sql) do 97 | "SELECT `users`.* FROM `users` " + 98 | "INNER JOIN `microposts` " + 99 | "ON `microposts`.`user_id` = `users`.`id`" 100 | end 101 | 102 | it { is_expected.to eq sql } 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /spec/arel-mysql-index-hint/preload_spec.rb: -------------------------------------------------------------------------------- 1 | describe "arel-mysql-index-hint" do 2 | describe "#preload" do 3 | subject do 4 | User. 5 | where(id: 1). 6 | preload(:microposts). 7 | hint(users: {hint_type => :index_users_on_email}). 8 | to_sql.gsub(/\s+/, " ") 9 | end 10 | 11 | let(:sql) do 12 | "SELECT `users`.* " + 13 | "FROM `users` " + 14 | "#{hint_type.to_s.upcase} INDEX (`index_users_on_email`) " + 15 | "WHERE `users`.`id` = 1" 16 | end 17 | 18 | let(:hint_type) { :force } 19 | 20 | it { is_expected.to eq sql } 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/arel-mysql-index-hint/unnested_hint_spec.rb: -------------------------------------------------------------------------------- 1 | describe "arel-mysql-index-hint" do 2 | context "unnested hint" do 3 | subject do 4 | User. 5 | hint(hint_type => :index_users_on_email). 6 | to_sql.gsub(/\s+/, " ") 7 | end 8 | 9 | let(:sql) do 10 | "SELECT `users`.* " + 11 | "FROM `users` " + 12 | "#{hint_type.to_s.upcase} INDEX (`index_users_on_email`)" 13 | end 14 | 15 | let(:hint_type) { :force } 16 | 17 | it { is_expected.to eq sql } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/arel-mysql-index-hint/without_join_spec.rb: -------------------------------------------------------------------------------- 1 | describe "arel-mysql-index-hint" do 2 | describe "#all" do 3 | subject do 4 | User. 5 | all. 6 | hint(users: {hint_type => :index_users_on_email}). 7 | to_sql.gsub(/\s+/, " ") 8 | end 9 | 10 | let(:sql) do 11 | "SELECT `users`.* " + 12 | "FROM `users` " + 13 | "#{hint_type.to_s.upcase} INDEX (`index_users_on_email`)" 14 | end 15 | 16 | let(:hint_type) { :force } 17 | 18 | it { is_expected.to eq sql } 19 | end 20 | 21 | describe "call nothing" do 22 | subject do 23 | User. 24 | hint(users: {hint_type => :index_users_on_email}). 25 | to_sql.gsub(/\s+/, " ") 26 | end 27 | 28 | let(:sql) do 29 | "SELECT `users`.* " + 30 | "FROM `users` " + 31 | "#{hint_type.to_s.upcase} INDEX (`index_users_on_email`)" 32 | end 33 | 34 | let(:hint_type) { :force } 35 | 36 | it { is_expected.to eq sql } 37 | end 38 | 39 | describe "#limit" do 40 | subject do 41 | User. 42 | limit(1). 43 | hint(users: {hint_type => :index_users_on_email}). 44 | to_sql.gsub(/\s+/, " ") 45 | end 46 | 47 | let(:sql) do 48 | "SELECT `users`.* " + 49 | "FROM `users` " + 50 | "#{hint_type.to_s.upcase} INDEX (`index_users_on_email`) " + 51 | "LIMIT 1" 52 | end 53 | 54 | let(:hint_type) { :force } 55 | 56 | it { is_expected.to eq sql } 57 | end 58 | 59 | describe "#first" do 60 | subject do 61 | User. 62 | hint(users: {hint_type => :index_users_on_email}). 63 | first 64 | end 65 | 66 | let(:sql) do 67 | "SELECT `users`.* FROM `users` " + 68 | "#{hint_type.to_s.upcase} INDEX (`index_users_on_email`) " + 69 | "ORDER BY `users`.`id` ASC " + 70 | "LIMIT 1" 71 | end 72 | 73 | let(:hint_type) { :force } 74 | 75 | it do 76 | subject 77 | expect(sql_log.first).to eq sql 78 | end 79 | end 80 | 81 | describe "#take" do 82 | subject do 83 | User. 84 | hint(users: {hint_type => :index_users_on_email}). 85 | take 86 | end 87 | 88 | let(:sql) do 89 | "SELECT `users`.* FROM `users` " + 90 | "#{hint_type.to_s.upcase} INDEX (`index_users_on_email`) " + 91 | "LIMIT 1" 92 | end 93 | 94 | let(:hint_type) { :force } 95 | 96 | it do 97 | subject 98 | expect(sql_log.first).to eq sql 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /spec/init.sql: -------------------------------------------------------------------------------- 1 | # original: https://github.com/railstutorial/sample_app_rails_4 2 | 3 | DROP DATABASE IF EXISTS `arel_mysql_index_hint_test`; 4 | CREATE DATABASE `arel_mysql_index_hint_test` DEFAULT CHARSET=utf8; 5 | 6 | USE `arel_mysql_index_hint_test`; 7 | 8 | DROP TABLE IF EXISTS `microposts`; 9 | CREATE TABLE `microposts` ( 10 | `id` int(11) NOT NULL AUTO_INCREMENT, 11 | `content` varchar(255) DEFAULT NULL, 12 | `user_id` int(11) DEFAULT NULL, 13 | `created_at` datetime DEFAULT NULL, 14 | `updated_at` datetime DEFAULT NULL, 15 | PRIMARY KEY (`id`), 16 | KEY `index_microposts_on_user_id_and_created_at` (`user_id`,`created_at`) 17 | ) DEFAULT CHARSET=utf8; 18 | 19 | DROP TABLE IF EXISTS `relationships`; 20 | CREATE TABLE `relationships` ( 21 | `id` int(11) NOT NULL AUTO_INCREMENT, 22 | `follower_id` int(11) DEFAULT NULL, 23 | `followed_id` int(11) DEFAULT NULL, 24 | `created_at` datetime DEFAULT NULL, 25 | `updated_at` datetime DEFAULT NULL, 26 | PRIMARY KEY (`id`), 27 | UNIQUE KEY `index_relationships_on_follower_id_and_followed_id` (`follower_id`,`followed_id`), 28 | KEY `index_relationships_on_followed_id` (`followed_id`), 29 | KEY `index_relationships_on_follower_id` (`follower_id`) 30 | ) DEFAULT CHARSET=utf8; 31 | 32 | DROP TABLE IF EXISTS `users`; 33 | CREATE TABLE `users` ( 34 | `id` int(11) NOT NULL AUTO_INCREMENT, 35 | `name` varchar(255) DEFAULT NULL, 36 | `email` varchar(255) DEFAULT NULL, 37 | `created_at` datetime DEFAULT NULL, 38 | `updated_at` datetime DEFAULT NULL, 39 | `password_digest` varchar(255) DEFAULT NULL, 40 | `remember_token` varchar(255) DEFAULT NULL, 41 | `admin` tinyint(1) DEFAULT NULL, 42 | PRIMARY KEY (`id`), 43 | UNIQUE KEY `index_users_on_email` (`email`), 44 | KEY `index_users_on_remember_token` (`remember_token`) 45 | ) DEFAULT CHARSET=utf8; 46 | -------------------------------------------------------------------------------- /spec/models.rb: -------------------------------------------------------------------------------- 1 | # original: https://github.com/railstutorial/sample_app_rails_4 2 | require "factory_girl" 3 | 4 | class Micropost < ActiveRecord::Base 5 | belongs_to :user 6 | end 7 | 8 | class Relationship < ActiveRecord::Base 9 | belongs_to :follower, class_name: "User" 10 | belongs_to :followed, class_name: "User" 11 | end 12 | 13 | class User < ActiveRecord::Base 14 | has_many :microposts, dependent: :destroy 15 | has_many :relationships, foreign_key: "follower_id", dependent: :destroy 16 | has_many :followed_users, through: :relationships, source: :followed 17 | has_many :reverse_relationships, foreign_key: "followed_id", 18 | class_name: "Relationship", 19 | dependent: :destroy 20 | has_many :followers, through: :reverse_relationships, source: :follower 21 | end 22 | 23 | FactoryGirl.define do 24 | factory :user do 25 | sequence(:name) { |n| "Person #{n}" } 26 | sequence(:email) { |n| "person_#{n}@example.com"} 27 | 28 | factory :admin do 29 | admin true 30 | end 31 | end 32 | 33 | factory :micropost do 34 | content "Lorem ipsum" 35 | user 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['TRAVIS'] 2 | require 'simplecov' 3 | require 'coveralls' 4 | 5 | SimpleCov.formatter = Coveralls::SimpleCov::Formatter 6 | 7 | SimpleCov.start do 8 | add_filter "spec/" 9 | end 10 | end 11 | 12 | require "active_record" 13 | require "arel-mysql-index-hint" 14 | require "models" 15 | 16 | $__arel_mysql_index_hint_sql_log__ = [] 17 | 18 | TEST_MYSQL_HOST = '127.0.0.1' 19 | TEST_MYSQL_PORT = 3306 20 | TEST_MYSQL_USER = 'root' 21 | TEST_MYSQL_PASS = 'password' 22 | MYSQL_CLI = "MYSQL_PWD=#{TEST_MYSQL_PASS} mysql -h #{TEST_MYSQL_HOST} -P #{TEST_MYSQL_PORT} -u #{TEST_MYSQL_USER}" 23 | 24 | ActiveSupport::Notifications.subscribe('sql.active_record') do |name, start, finish, id, payload| 25 | sql = payload[:sql] 26 | $__arel_mysql_index_hint_sql_log__ << sql.gsub(/\s+/, " ") if sql 27 | end 28 | 29 | RSpec.configure do |config| 30 | config.before(:all) do 31 | init_database 32 | 33 | ActiveRecord::Base.establish_connection( 34 | adapter: 'mysql2', 35 | database: 'arel_mysql_index_hint_test', 36 | host: TEST_MYSQL_HOST, 37 | port: TEST_MYSQL_PORT, 38 | username: TEST_MYSQL_USER, 39 | password: TEST_MYSQL_PASS, 40 | ) 41 | end 42 | 43 | config.before(:each) do 44 | $__arel_mysql_index_hint_sql_log__.clear 45 | end 46 | end 47 | 48 | def init_database 49 | sql_file = File.expand_path('../init.sql', __FILE__) 50 | system("#{MYSQL_CLI} < #{sql_file}") 51 | end 52 | 53 | def sql_log 54 | $__arel_mysql_index_hint_sql_log__.dup 55 | end 56 | --------------------------------------------------------------------------------