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

Sign up to get free protection for your applications and to get access to all the features.
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