activerecord-sqlserver-adapter 6.0.0.rc1 → 6.1.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +26 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +29 -0
  5. data/CHANGELOG.md +18 -23
  6. data/Gemfile +11 -5
  7. data/Guardfile +9 -8
  8. data/README.md +32 -3
  9. data/RUNNING_UNIT_TESTS.md +1 -1
  10. data/Rakefile +12 -16
  11. data/VERSION +1 -1
  12. data/activerecord-sqlserver-adapter.gemspec +4 -4
  13. data/lib/active_record/connection_adapters/sqlserver/core_ext/active_record.rb +0 -4
  14. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +1 -4
  15. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +3 -13
  16. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +8 -5
  17. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +2 -3
  18. data/lib/active_record/connection_adapters/sqlserver/core_ext/query_methods.rb +2 -3
  19. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +0 -4
  20. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +58 -43
  21. data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +7 -12
  22. data/lib/active_record/connection_adapters/sqlserver/errors.rb +0 -3
  23. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +15 -15
  24. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +22 -3
  25. data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +16 -10
  26. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +131 -105
  27. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +6 -8
  28. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_table.rb +2 -2
  29. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_xml.rb +1 -1
  30. data/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb +25 -7
  31. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +1 -5
  32. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +6 -10
  33. data/lib/active_record/connection_adapters/sqlserver/type.rb +36 -35
  34. data/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb +0 -2
  35. data/lib/active_record/connection_adapters/sqlserver/type/binary.rb +0 -2
  36. data/lib/active_record/connection_adapters/sqlserver/type/boolean.rb +0 -2
  37. data/lib/active_record/connection_adapters/sqlserver/type/char.rb +2 -2
  38. data/lib/active_record/connection_adapters/sqlserver/type/data.rb +0 -2
  39. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +2 -3
  40. data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +2 -3
  41. data/lib/active_record/connection_adapters/sqlserver/type/datetime2.rb +0 -2
  42. data/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb +0 -2
  43. data/lib/active_record/connection_adapters/sqlserver/type/decimal.rb +0 -2
  44. data/lib/active_record/connection_adapters/sqlserver/type/decimal_without_scale.rb +22 -0
  45. data/lib/active_record/connection_adapters/sqlserver/type/float.rb +0 -2
  46. data/lib/active_record/connection_adapters/sqlserver/type/integer.rb +0 -2
  47. data/lib/active_record/connection_adapters/sqlserver/type/json.rb +0 -1
  48. data/lib/active_record/connection_adapters/sqlserver/type/money.rb +0 -2
  49. data/lib/active_record/connection_adapters/sqlserver/type/real.rb +0 -2
  50. data/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb +0 -2
  51. data/lib/active_record/connection_adapters/sqlserver/type/small_money.rb +0 -2
  52. data/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb +0 -2
  53. data/lib/active_record/connection_adapters/sqlserver/type/string.rb +0 -2
  54. data/lib/active_record/connection_adapters/sqlserver/type/text.rb +0 -2
  55. data/lib/active_record/connection_adapters/sqlserver/type/time.rb +2 -3
  56. data/lib/active_record/connection_adapters/sqlserver/type/time_value_fractional.rb +6 -9
  57. data/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb +0 -2
  58. data/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb +0 -2
  59. data/lib/active_record/connection_adapters/sqlserver/type/unicode_char.rb +1 -3
  60. data/lib/active_record/connection_adapters/sqlserver/type/unicode_string.rb +0 -2
  61. data/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb +0 -2
  62. data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar.rb +0 -2
  63. data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb +0 -2
  64. data/lib/active_record/connection_adapters/sqlserver/type/uuid.rb +1 -2
  65. data/lib/active_record/connection_adapters/sqlserver/type/varbinary.rb +1 -3
  66. data/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb +0 -2
  67. data/lib/active_record/connection_adapters/sqlserver/type/varchar.rb +1 -3
  68. data/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb +0 -2
  69. data/lib/active_record/connection_adapters/sqlserver/utils.rb +8 -11
  70. data/lib/active_record/connection_adapters/sqlserver/version.rb +0 -2
  71. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +170 -136
  72. data/lib/active_record/connection_adapters/sqlserver_column.rb +16 -1
  73. data/lib/active_record/sqlserver_base.rb +9 -15
  74. data/lib/active_record/tasks/sqlserver_database_tasks.rb +36 -39
  75. data/lib/activerecord-sqlserver-adapter.rb +1 -1
  76. data/lib/arel/visitors/sqlserver.rb +126 -50
  77. data/lib/arel_sqlserver.rb +2 -2
  78. data/test/cases/adapter_test_sqlserver.rb +203 -190
  79. data/test/cases/change_column_collation_test_sqlserver.rb +33 -0
  80. data/test/cases/change_column_null_test_sqlserver.rb +12 -12
  81. data/test/cases/coerced_tests.rb +656 -318
  82. data/test/cases/column_test_sqlserver.rb +285 -284
  83. data/test/cases/connection_test_sqlserver.rb +15 -20
  84. data/test/cases/disconnected_test_sqlserver.rb +39 -0
  85. data/test/cases/execute_procedure_test_sqlserver.rb +26 -19
  86. data/test/cases/fetch_test_sqlserver.rb +14 -22
  87. data/test/cases/fully_qualified_identifier_test_sqlserver.rb +12 -18
  88. data/test/cases/helper_sqlserver.rb +13 -15
  89. data/test/cases/in_clause_test_sqlserver.rb +36 -9
  90. data/test/cases/index_test_sqlserver.rb +13 -15
  91. data/test/cases/json_test_sqlserver.rb +23 -25
  92. data/test/cases/lateral_test_sqlserver.rb +35 -0
  93. data/test/cases/migration_test_sqlserver.rb +71 -26
  94. data/test/cases/optimizer_hints_test_sqlserver.rb +72 -0
  95. data/test/cases/order_test_sqlserver.rb +57 -53
  96. data/test/cases/pessimistic_locking_test_sqlserver.rb +25 -33
  97. data/test/cases/primary_keys_test_sqlserver.rb +103 -0
  98. data/test/cases/rake_test_sqlserver.rb +33 -46
  99. data/test/cases/schema_dumper_test_sqlserver.rb +121 -108
  100. data/test/cases/schema_test_sqlserver.rb +18 -26
  101. data/test/cases/scratchpad_test_sqlserver.rb +2 -4
  102. data/test/cases/showplan_test_sqlserver.rb +24 -33
  103. data/test/cases/specific_schema_test_sqlserver.rb +66 -65
  104. data/test/cases/transaction_test_sqlserver.rb +16 -19
  105. data/test/cases/trigger_test_sqlserver.rb +12 -12
  106. data/test/cases/utils_test_sqlserver.rb +68 -70
  107. data/test/cases/uuid_test_sqlserver.rb +11 -13
  108. data/test/debug.rb +6 -6
  109. data/test/migrations/create_clients_and_change_column_collation.rb +19 -0
  110. data/test/migrations/create_clients_and_change_column_null.rb +1 -1
  111. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +2 -4
  112. data/test/models/sqlserver/booking.rb +1 -1
  113. data/test/models/sqlserver/customers_view.rb +1 -1
  114. data/test/models/sqlserver/dollar_table_name.rb +1 -1
  115. data/test/models/sqlserver/edge_schema.rb +1 -3
  116. data/test/models/sqlserver/fk_has_fk.rb +1 -1
  117. data/test/models/sqlserver/fk_has_pk.rb +1 -1
  118. data/test/models/sqlserver/natural_pk_data.rb +2 -2
  119. data/test/models/sqlserver/natural_pk_int_data.rb +1 -1
  120. data/test/models/sqlserver/no_pk_data.rb +1 -1
  121. data/test/models/sqlserver/object_default.rb +1 -1
  122. data/test/models/sqlserver/quoted_table.rb +2 -2
  123. data/test/models/sqlserver/quoted_view_1.rb +1 -1
  124. data/test/models/sqlserver/quoted_view_2.rb +1 -1
  125. data/test/models/sqlserver/sst_memory.rb +1 -1
  126. data/test/models/sqlserver/sst_string_collation.rb +3 -0
  127. data/test/models/sqlserver/string_default.rb +1 -1
  128. data/test/models/sqlserver/string_defaults_big_view.rb +1 -1
  129. data/test/models/sqlserver/string_defaults_view.rb +1 -1
  130. data/test/models/sqlserver/tinyint_pk.rb +1 -1
  131. data/test/models/sqlserver/trigger.rb +2 -2
  132. data/test/models/sqlserver/trigger_history.rb +1 -1
  133. data/test/models/sqlserver/upper.rb +1 -1
  134. data/test/models/sqlserver/uppered.rb +1 -1
  135. data/test/models/sqlserver/uuid.rb +1 -1
  136. data/test/schema/sqlserver_specific_schema.rb +36 -21
  137. data/test/support/coerceable_test_sqlserver.rb +1 -4
  138. data/test/support/connection_reflection.rb +1 -2
  139. data/test/support/core_ext/query_cache.rb +1 -1
  140. data/test/support/load_schema_sqlserver.rb +3 -5
  141. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic.dump +0 -0
  142. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic_associations.dump +0 -0
  143. data/test/support/minitest_sqlserver.rb +1 -1
  144. data/test/support/paths_sqlserver.rb +9 -11
  145. data/test/support/rake_helpers.rb +12 -10
  146. data/test/support/sql_counter_sqlserver.rb +14 -16
  147. data/test/support/test_in_memory_oltp.rb +7 -7
  148. metadata +31 -11
  149. data/.travis.yml +0 -23
@@ -3,7 +3,6 @@
3
3
  module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  class SQLServerColumn < Column
6
-
7
6
  def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation: nil, comment: nil, **sqlserver_options)
8
7
  @sqlserver_options = sqlserver_options
9
8
  super
@@ -29,6 +28,22 @@ module ActiveRecord
29
28
  collation && collation.match(/_CS/)
30
29
  end
31
30
 
31
+ private
32
+
33
+ # In the Rails version of this method there is an assumption that the `default` value will always be a
34
+ # `String` class, which must be true for the MySQL/PostgreSQL/SQLite adapters. However, in the SQL Server
35
+ # adapter the `default` value can also be Boolean/Date/Time/etc. Changed the implementation of this method
36
+ # to handle non-String `default` objects.
37
+ def deduplicated
38
+ @name = -name
39
+ @sql_type_metadata = sql_type_metadata.deduplicate if sql_type_metadata
40
+ @default = (default.is_a?(String) ? -default : default.dup.freeze) if default
41
+ @default_function = -default_function if default_function
42
+ @collation = -collation if collation
43
+ @comment = -comment if comment
44
+
45
+ freeze
46
+ end
32
47
  end
33
48
  end
34
49
  end
@@ -4,21 +4,15 @@ module ActiveRecord
4
4
  module ConnectionHandling
5
5
  def sqlserver_connection(config) #:nodoc:
6
6
  config = config.symbolize_keys
7
- config.reverse_merge! mode: :dblib
8
- mode = config[:mode].to_s.downcase.underscore.to_sym
9
- case mode
10
- when :dblib
11
- require 'tiny_tds'
12
- else
13
- raise ArgumentError, "Unknown connection mode in #{config.inspect}."
14
- end
15
- ConnectionAdapters::SQLServerAdapter.new(nil, nil, config.merge(mode: mode))
16
- rescue TinyTds::Error => e
17
- if e.message.match(/database .* does not exist/i)
18
- raise ActiveRecord::NoDatabaseError
19
- else
20
- raise
21
- end
7
+ config.reverse_merge!(mode: :dblib)
8
+ config[:mode] = config[:mode].to_s.downcase.underscore.to_sym
9
+
10
+ ConnectionAdapters::SQLServerAdapter.new(
11
+ ConnectionAdapters::SQLServerAdapter.new_client(config),
12
+ logger,
13
+ nil,
14
+ config
15
+ )
22
16
  end
23
17
  end
24
18
  end
@@ -1,30 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_record/tasks/database_tasks'
4
- require 'shellwords'
5
- require 'ipaddr'
6
- require 'socket'
3
+ require "active_record/tasks/database_tasks"
4
+ require "shellwords"
5
+ require "ipaddr"
6
+ require "socket"
7
7
 
8
8
  module ActiveRecord
9
9
  module Tasks
10
-
11
10
  class SQLServerDatabaseTasks
12
-
13
- DEFAULT_COLLATION = 'SQL_Latin1_General_CP1_CI_AS'
11
+ DEFAULT_COLLATION = "SQL_Latin1_General_CP1_CI_AS"
14
12
 
15
13
  delegate :connection, :establish_connection, :clear_active_connections!,
16
14
  to: ActiveRecord::Base
17
15
 
16
+ def self.using_database_configurations?
17
+ true
18
+ end
19
+
18
20
  def initialize(configuration)
19
21
  @configuration = configuration
22
+ @configuration_hash = @configuration.configuration_hash
20
23
  end
21
24
 
22
25
  def create(master_established = false)
23
26
  establish_master_connection unless master_established
24
- connection.create_database configuration['database'], configuration.merge('collation' => default_collation)
27
+ connection.create_database configuration.database, configuration_hash.merge(collation: default_collation)
25
28
  establish_connection configuration
26
- rescue ActiveRecord::StatementInvalid => error
27
- if /database .* already exists/i === error.message
29
+ rescue ActiveRecord::StatementInvalid => e
30
+ if /database .* already exists/i === e.message
28
31
  raise DatabaseAlreadyExists
29
32
  else
30
33
  raise
@@ -33,7 +36,7 @@ module ActiveRecord
33
36
 
34
37
  def drop
35
38
  establish_master_connection
36
- connection.drop_database configuration['database']
39
+ connection.drop_database configuration.database
37
40
  end
38
41
 
39
42
  def charset
@@ -51,27 +54,28 @@ module ActiveRecord
51
54
  end
52
55
 
53
56
  def structure_dump(filename, extra_flags)
54
- server_arg = "-S #{Shellwords.escape(configuration['host'])}"
55
- server_arg += ":#{Shellwords.escape(configuration['port'])}" if configuration['port']
57
+ server_arg = "-S #{Shellwords.escape(configuration_hash[:host])}"
58
+ server_arg += ":#{Shellwords.escape(configuration_hash[:port])}" if configuration_hash[:port]
56
59
  command = [
57
60
  "defncopy-ttds",
58
61
  server_arg,
59
- "-D #{Shellwords.escape(configuration['database'])}",
60
- "-U #{Shellwords.escape(configuration['username'])}",
61
- "-P #{Shellwords.escape(configuration['password'])}",
62
+ "-D #{Shellwords.escape(configuration_hash[:database])}",
63
+ "-U #{Shellwords.escape(configuration_hash[:username])}",
64
+ "-P #{Shellwords.escape(configuration_hash[:password])}",
62
65
  "-o #{Shellwords.escape(filename)}",
63
66
  ]
64
67
  table_args = connection.tables.map { |t| Shellwords.escape(t) }
65
68
  command.concat(table_args)
66
69
  view_args = connection.views.map { |v| Shellwords.escape(v) }
67
70
  command.concat(view_args)
68
- raise 'Error dumping database' unless Kernel.system(command.join(' '))
71
+ raise "Error dumping database" unless Kernel.system(command.join(" "))
72
+
69
73
  dump = File.read(filename)
70
- dump.gsub!(/^USE .*$\nGO\n/, '') # Strip db USE statements
71
- dump.gsub!(/^GO\n/, '') # Strip db GO statements
72
- dump.gsub!(/nvarchar\(8000\)/, 'nvarchar(4000)') # Fix nvarchar(8000) column defs
73
- dump.gsub!(/nvarchar\(-1\)/, 'nvarchar(max)') # Fix nvarchar(-1) column defs
74
- dump.gsub!(/text\(\d+\)/, 'text') # Fix text(16) column defs
74
+ dump.gsub!(/^USE .*$\nGO\n/, "") # Strip db USE statements
75
+ dump.gsub!(/^GO\n/, "") # Strip db GO statements
76
+ dump.gsub!(/nvarchar\(8000\)/, "nvarchar(4000)") # Fix nvarchar(8000) column defs
77
+ dump.gsub!(/nvarchar\(-1\)/, "nvarchar(max)") # Fix nvarchar(-1) column defs
78
+ dump.gsub!(/text\(\d+\)/, "text") # Fix text(16) column defs
75
79
  File.open(filename, "w") { |file| file.puts dump }
76
80
  end
77
81
 
@@ -79,33 +83,27 @@ module ActiveRecord
79
83
  connection.execute File.read(filename)
80
84
  end
81
85
 
82
-
83
86
  private
84
87
 
85
- def configuration
86
- @configuration
87
- end
88
+ attr_reader :configuration, :configuration_hash
88
89
 
89
90
  def default_collation
90
- configuration['collation'] || DEFAULT_COLLATION
91
+ configuration_hash[:collation] || DEFAULT_COLLATION
91
92
  end
92
93
 
93
94
  def establish_master_connection
94
- establish_connection configuration.merge('database' => 'master')
95
+ establish_connection configuration_hash.merge(database: "master")
95
96
  end
96
-
97
97
  end
98
98
 
99
99
  module DatabaseTasksSQLServer
100
-
101
100
  extend ActiveSupport::Concern
102
101
 
103
102
  module ClassMethods
104
-
105
103
  LOCAL_IPADDR = [
106
- IPAddr.new('192.168.0.0/16'),
107
- IPAddr.new('10.0.0.0/8'),
108
- IPAddr.new('172.16.0.0/12')
104
+ IPAddr.new("192.168.0.0/16"),
105
+ IPAddr.new("10.0.0.0/8"),
106
+ IPAddr.new("172.16.0.0/12")
109
107
  ]
110
108
 
111
109
  private
@@ -115,21 +113,20 @@ module ActiveRecord
115
113
  end
116
114
 
117
115
  def configuration_host_ip(configuration)
118
- return nil unless configuration['host']
119
- Socket::getaddrinfo(configuration['host'], 'echo', Socket::AF_INET)[0][3]
116
+ return nil unless configuration.host
117
+
118
+ Socket::getaddrinfo(configuration.host, "echo", Socket::AF_INET)[0][3]
120
119
  end
121
120
 
122
121
  def local_ipaddr?(host_ip)
123
122
  return false unless host_ip
123
+
124
124
  LOCAL_IPADDR.any? { |ip| ip.include?(host_ip) }
125
125
  end
126
-
127
126
  end
128
-
129
127
  end
130
128
 
131
129
  DatabaseTasks.register_task %r{sqlserver}, SQLServerDatabaseTasks
132
130
  DatabaseTasks.send :include, DatabaseTasksSQLServer
133
-
134
131
  end
135
132
  end
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_record/connection_adapters/sqlserver_adapter'
3
+ require "active_record/connection_adapters/sqlserver_adapter"
@@ -3,23 +3,22 @@
3
3
  module Arel
4
4
  module Visitors
5
5
  class SQLServer < Arel::Visitors::ToSql
6
-
7
6
  OFFSET = " OFFSET "
8
7
  ROWS = " ROWS"
9
8
  FETCH = " FETCH NEXT "
10
9
  FETCH0 = " FETCH FIRST (SELECT 0) "
11
10
  ROWS_ONLY = " ROWS ONLY"
12
11
 
13
-
14
12
  private
15
13
 
16
- # SQLServer ToSql/Visitor (Overides)
14
+ # SQLServer ToSql/Visitor (Overrides)
17
15
 
18
- def visit_Arel_Nodes_BindParam o, collector
19
- collector.add_bind(o.value) { |i| "@#{i-1}" }
20
- end
16
+ BIND_BLOCK = proc { |i| "@#{i - 1}" }
17
+ private_constant :BIND_BLOCK
21
18
 
22
- def visit_Arel_Nodes_Bin o, collector
19
+ def bind_block; BIND_BLOCK; end
20
+
21
+ def visit_Arel_Nodes_Bin(o, collector)
23
22
  visit o.expr, collector
24
23
  collector << " #{ActiveRecord::ConnectionAdapters::SQLServerAdapter.cs_equality_operator} "
25
24
  end
@@ -30,26 +29,26 @@ module Arel
30
29
  visit o.right, collector
31
30
  end
32
31
 
33
- def visit_Arel_Nodes_UpdateStatement(o, a)
32
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
34
33
  if o.orders.any? && o.limit.nil?
35
34
  o.limit = Nodes::Limit.new(9_223_372_036_854_775_807)
36
35
  end
37
36
  super
38
37
  end
39
38
 
40
- def visit_Arel_Nodes_Lock o, collector
41
- o.expr = Arel.sql('WITH(UPDLOCK)') if o.expr.to_s =~ /FOR UPDATE/
39
+ def visit_Arel_Nodes_Lock(o, collector)
40
+ o.expr = Arel.sql("WITH(UPDLOCK)") if o.expr.to_s =~ /FOR UPDATE/
42
41
  collector << " "
43
42
  visit o.expr, collector
44
43
  end
45
44
 
46
- def visit_Arel_Nodes_Offset o, collector
45
+ def visit_Arel_Nodes_Offset(o, collector)
47
46
  collector << OFFSET
48
47
  visit o.expr, collector
49
48
  collector << ROWS
50
49
  end
51
50
 
52
- def visit_Arel_Nodes_Limit o, collector
51
+ def visit_Arel_Nodes_Limit(o, collector)
53
52
  if node_value(o) == 0
54
53
  collector << FETCH0
55
54
  collector << ROWS_ONLY
@@ -65,14 +64,45 @@ module Arel
65
64
  super
66
65
  end
67
66
 
68
- def visit_Arel_Nodes_SelectStatement o, collector
67
+ def visit_Arel_Nodes_HomogeneousIn(o, collector)
68
+ collector.preparable = false
69
+
70
+ collector << quote_table_name(o.table_name) << "." << quote_column_name(o.column_name)
71
+
72
+ if o.type == :in
73
+ collector << " IN ("
74
+ else
75
+ collector << " NOT IN ("
76
+ end
77
+
78
+ values = o.casted_values
79
+
80
+ if values.empty?
81
+ collector << @connection.quote(nil)
82
+ elsif @connection.prepared_statements
83
+ # Monkey-patch start. Add query attribute bindings rather than just values.
84
+ column_name = o.column_name
85
+ column_type = o.attribute.relation.type_for_attribute(o.column_name)
86
+ attrs = values.map { |value| ActiveRecord::Relation::QueryAttribute.new(column_name, value, column_type) }
87
+
88
+ collector.add_binds(attrs, &bind_block)
89
+ # Monkey-patch end.
90
+ else
91
+ collector.add_binds(values, &bind_block)
92
+ end
93
+
94
+ collector << ")"
95
+ collector
96
+ end
97
+
98
+ def visit_Arel_Nodes_SelectStatement(o, collector)
69
99
  @select_statement = o
70
100
  distinct_One_As_One_Is_So_Not_Fetch o
71
101
  if o.with
72
102
  collector = visit o.with, collector
73
103
  collector << " "
74
104
  end
75
- collector = o.cores.inject(collector) { |c,x|
105
+ collector = o.cores.inject(collector) { |c, x|
76
106
  visit_Arel_Nodes_SelectCore(x, c)
77
107
  }
78
108
  collector = visit_Orders_And_Let_Fetch_Happen o, collector
@@ -82,19 +112,31 @@ module Arel
82
112
  @select_statement = nil
83
113
  end
84
114
 
85
- def visit_Arel_Table o, collector
115
+ def visit_Arel_Nodes_SelectCore(o, collector)
116
+ collector = super
117
+ maybe_visit o.optimizer_hints, collector
118
+ end
119
+
120
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
121
+ hints = o.expr.map { |v| sanitize_as_option_clause(v) }.join(", ")
122
+ collector << "OPTION (#{hints})"
123
+ end
124
+
125
+ def visit_Arel_Table(o, collector)
86
126
  # Apparently, o.engine.connection can actually be a different adapter
87
127
  # than sqlserver. Can be removed if fixed in ActiveRecord. See:
88
128
  # github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/450
89
- table_name = begin
90
- if o.class.engine.connection.respond_to?(:sqlserver?) && o.class.engine.connection.database_prefix_remote_server?
91
- remote_server_table_name(o)
92
- else
129
+ table_name =
130
+ begin
131
+ if o.class.engine.connection.respond_to?(:sqlserver?) && o.class.engine.connection.database_prefix_remote_server?
132
+ remote_server_table_name(o)
133
+ else
134
+ quote_table_name(o.name)
135
+ end
136
+ rescue Exception
93
137
  quote_table_name(o.name)
94
138
  end
95
- rescue Exception => e
96
- quote_table_name(o.name)
97
- end
139
+
98
140
  if o.table_alias
99
141
  collector << "#{table_name} #{quote_table_name o.table_alias}"
100
142
  else
@@ -102,51 +144,65 @@ module Arel
102
144
  end
103
145
  end
104
146
 
105
- def visit_Arel_Nodes_JoinSource o, collector
147
+ def visit_Arel_Nodes_JoinSource(o, collector)
106
148
  if o.left
107
149
  collector = visit o.left, collector
108
150
  collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector
109
151
  end
110
152
  if o.right.any?
111
153
  collector << " " if o.left
112
- collector = inject_join o.right, collector, ' '
154
+ collector = inject_join o.right, collector, " "
113
155
  end
114
156
  collector
115
157
  end
116
158
 
117
- def visit_Arel_Nodes_InnerJoin o, collector
118
- collector << "INNER JOIN "
119
- collector = visit o.left, collector
120
- collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
121
- if o.right
122
- collector << " "
123
- visit(o.right, collector)
159
+ def visit_Arel_Nodes_InnerJoin(o, collector)
160
+ if o.left.is_a?(Arel::Nodes::As) && o.left.left.is_a?(Arel::Nodes::Lateral)
161
+ collector << "CROSS "
162
+ visit o.left, collector
124
163
  else
125
- collector
164
+ collector << "INNER JOIN "
165
+ collector = visit o.left, collector
166
+ collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
167
+ if o.right
168
+ collector << " "
169
+ visit(o.right, collector)
170
+ else
171
+ collector
172
+ end
126
173
  end
127
174
  end
128
175
 
129
- def visit_Arel_Nodes_OuterJoin o, collector
130
- collector << "LEFT OUTER JOIN "
131
- collector = visit o.left, collector
132
- collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
133
- collector << " "
134
- visit o.right, collector
176
+ def visit_Arel_Nodes_OuterJoin(o, collector)
177
+ if o.left.is_a?(Arel::Nodes::As) && o.left.left.is_a?(Arel::Nodes::Lateral)
178
+ collector << "OUTER "
179
+ visit o.left, collector
180
+ else
181
+ collector << "LEFT OUTER JOIN "
182
+ collector = visit o.left, collector
183
+ collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
184
+ collector << " "
185
+ visit o.right, collector
186
+ end
135
187
  end
136
188
 
137
- def collect_in_clause(left, right, collector)
138
- if Array === right
139
- right.each { |node| remove_invalid_ordering_from_select_statement(node) }
189
+ def visit_Arel_Nodes_In(o, collector)
190
+ if Array === o.right
191
+ o.right.each { |node| remove_invalid_ordering_from_select_statement(node) }
140
192
  else
141
- remove_invalid_ordering_from_select_statement(right)
193
+ remove_invalid_ordering_from_select_statement(o.right)
142
194
  end
143
195
 
144
196
  super
145
197
  end
146
198
 
199
+ def collect_optimizer_hints(o, collector)
200
+ collector
201
+ end
202
+
147
203
  # SQLServer ToSql/Visitor (Additions)
148
204
 
149
- def visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, options = {}
205
+ def visit_Arel_Nodes_SelectStatement_SQLServer_Lock(collector, options = {})
150
206
  if select_statement_lock?
151
207
  collector = visit @select_statement.lock, collector
152
208
  collector << " " if options[:space]
@@ -154,7 +210,7 @@ module Arel
154
210
  collector
155
211
  end
156
212
 
157
- def visit_Orders_And_Let_Fetch_Happen o, collector
213
+ def visit_Orders_And_Let_Fetch_Happen(o, collector)
158
214
  make_Fetch_Possible_And_Deterministic o
159
215
  unless o.orders.empty?
160
216
  collector << " ORDER BY "
@@ -167,17 +223,30 @@ module Arel
167
223
  collector
168
224
  end
169
225
 
170
- def visit_Make_Fetch_Happen o, collector
226
+ def visit_Make_Fetch_Happen(o, collector)
171
227
  o.offset = Nodes::Offset.new(0) if o.limit && !o.offset
172
228
  collector = visit o.offset, collector if o.offset
173
229
  collector = visit o.limit, collector if o.limit
174
230
  collector
175
231
  end
176
232
 
233
+ def visit_Arel_Nodes_Lateral(o, collector)
234
+ collector << "APPLY"
235
+ collector << " "
236
+ if o.expr.is_a?(Arel::Nodes::SelectStatement)
237
+ collector << "("
238
+ visit(o.expr, collector)
239
+ collector << ")"
240
+ else
241
+ visit(o.expr, collector)
242
+ end
243
+ end
244
+
177
245
  # SQLServer Helpers
178
246
 
179
247
  def node_value(node)
180
248
  return nil unless node
249
+
181
250
  case node.expr
182
251
  when NilClass then nil
183
252
  when Numeric then node.expr
@@ -189,18 +258,20 @@ module Arel
189
258
  @select_statement && @select_statement.lock
190
259
  end
191
260
 
192
- def make_Fetch_Possible_And_Deterministic o
261
+ def make_Fetch_Possible_And_Deterministic(o)
193
262
  return if o.limit.nil? && o.offset.nil?
263
+
194
264
  t = table_From_Statement o
195
265
  pk = primary_Key_From_Table t
196
266
  return unless pk
267
+
197
268
  if o.orders.empty?
198
269
  # Prefer deterministic vs a simple `(SELECT NULL)` expr.
199
270
  o.orders = [pk.asc]
200
271
  end
201
272
  end
202
273
 
203
- def distinct_One_As_One_Is_So_Not_Fetch o
274
+ def distinct_One_As_One_Is_So_Not_Fetch(o)
204
275
  core = o.cores.first
205
276
  distinct = Nodes::Distinct === core.set_quantifier
206
277
  oneasone = core.projections.all? { |x| x == ActiveRecord::FinderMethods::ONE_AS_ONE }
@@ -211,7 +282,7 @@ module Arel
211
282
  end
212
283
  end
213
284
 
214
- def table_From_Statement o
285
+ def table_From_Statement(o)
215
286
  core = o.cores.first
216
287
  if Arel::Table === core.from
217
288
  core.from
@@ -222,14 +293,15 @@ module Arel
222
293
  end
223
294
  end
224
295
 
225
- def primary_Key_From_Table t
296
+ def primary_Key_From_Table(t)
226
297
  return unless t
298
+
227
299
  column_name = @connection.schema_cache.primary_keys(t.name) ||
228
300
  @connection.schema_cache.columns_hash(t.name).first.try(:second).try(:name)
229
301
  column_name ? t[column_name] : nil
230
302
  end
231
303
 
232
- def remote_server_table_name o
304
+ def remote_server_table_name(o)
233
305
  ActiveRecord::ConnectionAdapters::SQLServer::Utils.extract_identifiers(
234
306
  "#{o.class.engine.connection.database_prefix}#{o.name}"
235
307
  ).quoted
@@ -243,6 +315,10 @@ module Arel
243
315
 
244
316
  node.orders = [] unless node.offset || node.limit
245
317
  end
318
+
319
+ def sanitize_as_option_clause(value)
320
+ value.gsub(%r{OPTION \s* \( (.+) \)}xi, "\\1")
321
+ end
246
322
  end
247
323
  end
248
324
  end