Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
#### Added

- [#855](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/855) Add helpers to create/change/drop a schema.
- [#857](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/857) Included WAITFOR as read query type.
- [#857](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/857) Included WAITFOR as read query type.
- [#865](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/865) Implemented optimizer hints.

## v6.0.1

Expand Down
4 changes: 4 additions & 0 deletions lib/active_record/connection_adapters/sqlserver_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ def supports_savepoints?
true
end

def supports_optimizer_hints?
true
end

def supports_lazy_transactions?
true
end
Expand Down
18 changes: 18 additions & 0 deletions lib/arel/visitors/sqlserver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ def visit_Arel_Nodes_SelectStatement o, collector
@select_statement = nil
end

def visit_Arel_Nodes_SelectCore(o, collector)
collector = super
maybe_visit o.optimizer_hints, collector
end

def visit_Arel_Nodes_OptimizerHints(o, collector)
hints = o.expr.map { |v| sanitize_as_option_clause(v) }.join(", ")
collector << "OPTION (#{hints})"
end

def visit_Arel_Table o, collector
# Apparently, o.engine.connection can actually be a different adapter
# than sqlserver. Can be removed if fixed in ActiveRecord. See:
Expand Down Expand Up @@ -144,6 +154,10 @@ def collect_in_clause(left, right, collector)
super
end

def collect_optimizer_hints(o, collector)
collector
end

# SQLServer ToSql/Visitor (Additions)

def visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, options = {}
Expand Down Expand Up @@ -247,6 +261,10 @@ def remove_invalid_ordering_from_select_statement(node)

node.orders = [] unless node.offset || node.limit
end

def sanitize_as_option_clause(value)
value.gsub(%r{OPTION \s* \( (.+) \)}xi, "\\1")
end
end
end
end
15 changes: 15 additions & 0 deletions test/cases/coerced_tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,21 @@ def test_reverse_arel_assoc_order_with_function_coerced
end
end

module ActiveRecord
class RelationTest < ActiveRecord::TestCase
# Skipping this test. SQL Server doesn't support optimizer hint as comments
coerce_tests! :test_relation_with_optimizer_hints_filters_sql_comment_delimiters

coerce_tests! :test_does_not_duplicate_optimizer_hints_on_merge
def test_does_not_duplicate_optimizer_hints_on_merge_coerced
escaped_table = Post.connection.quote_table_name("posts")
expected = "SELECT #{escaped_table}.* FROM #{escaped_table} OPTION (OMGHINT)"
query = Post.optimizer_hints("OMGHINT").merge(Post.optimizer_hints("OMGHINT")).to_sql
assert_equal expected, query
end
end
end

require "models/post"
class SanitizeTest < ActiveRecord::TestCase
# Use nvarchar string (N'') in assert
Expand Down
72 changes: 72 additions & 0 deletions test/cases/optimizer_hints_test_sqlserver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

require "cases/helper_sqlserver"
require "models/company"

class OptimizerHitsTestSQLServer < ActiveRecord::TestCase
fixtures :companies

it "apply optimizations" do
assert_sql(%r{\ASELECT .+ FROM .+ OPTION \(HASH GROUP\)\z}) do
companies = Company.optimizer_hints("HASH GROUP")
companies = companies.distinct.select("firm_id")
assert_includes companies.explain, "| Hash Match | Aggregate |"
end

assert_sql(%r{\ASELECT .+ FROM .+ OPTION \(ORDER GROUP\)\z}) do
companies = Company.optimizer_hints("ORDER GROUP")
companies = companies.distinct.select("firm_id")
assert_includes companies.explain, "| Stream Aggregate | Aggregate |"
end
end

it "apply multiple optimizations" do
assert_sql(%r{\ASELECT .+ FROM .+ OPTION \(HASH GROUP, FAST 1\)\z}) do
companies = Company.optimizer_hints("HASH GROUP", "FAST 1")
companies = companies.distinct.select("firm_id")
assert_includes companies.explain, "| Hash Match | Flow Distinct |"
end
end

it "support subqueries" do
assert_sql(%r{.*'SELECT COUNT\(count_column\) FROM \(SELECT .*\) subquery_for_count OPTION \(MAXDOP 2\)'.*}) do
companies = Company.optimizer_hints("MAXDOP 2")
companies = companies.select(:id).where(firm_id: [0, 1]).limit(3)
assert_equal 3, companies.count
end
end

it "sanitize values" do
assert_sql(%r{\ASELECT .+ FROM .+ OPTION \(HASH GROUP\)\z}) do
companies = Company.optimizer_hints("OPTION (HASH GROUP)")
companies = companies.distinct.select("firm_id")
companies.to_a
end

assert_sql(%r{\ASELECT .+ FROM .+ OPTION \(HASH GROUP\)\z}) do
companies = Company.optimizer_hints("OPTION(HASH GROUP)")
companies = companies.distinct.select("firm_id")
companies.to_a
end

assert_sql(%r{\ASELECT .+ FROM .+ OPTION \(TABLE HINT \(\[companies\], INDEX\(1\)\)\)\z}) do
companies = Company.optimizer_hints("OPTION(TABLE HINT ([companies], INDEX(1)))")
companies = companies.distinct.select("firm_id")
companies.to_a
end

assert_sql(%r{\ASELECT .+ FROM .+ OPTION \(HASH GROUP\)\z}) do
companies = Company.optimizer_hints("Option(HASH GROUP)")
companies = companies.distinct.select("firm_id")
companies.to_a
end
end

it "skip optimization after unscope" do
assert_sql("SELECT DISTINCT [companies].[firm_id] FROM [companies]") do
companies = Company.optimizer_hints("HASH GROUP")
companies = companies.distinct.select("firm_id")
companies.unscope(:optimizer_hints).load
end
end
end