activerecord-sqlserver-adapter 5.2.1 → 7.0.0.0

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 (164) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +9 -0
  3. data/.github/issue_template.md +23 -0
  4. data/.github/workflows/ci.yml +29 -0
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +29 -0
  7. data/CHANGELOG.md +17 -27
  8. data/{Dockerfile → Dockerfile.ci} +1 -1
  9. data/Gemfile +49 -41
  10. data/Guardfile +9 -8
  11. data/MIT-LICENSE +1 -1
  12. data/README.md +65 -42
  13. data/RUNNING_UNIT_TESTS.md +3 -0
  14. data/Rakefile +14 -16
  15. data/VERSION +1 -1
  16. data/activerecord-sqlserver-adapter.gemspec +25 -14
  17. data/appveyor.yml +22 -17
  18. data/docker-compose.ci.yml +7 -5
  19. data/guides/RELEASING.md +11 -0
  20. data/lib/active_record/connection_adapters/sqlserver/core_ext/active_record.rb +2 -4
  21. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +5 -4
  22. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +10 -14
  23. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +12 -5
  24. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +2 -0
  25. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +10 -7
  26. data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +30 -0
  27. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +9 -4
  28. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +117 -52
  29. data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +9 -12
  30. data/lib/active_record/connection_adapters/sqlserver/errors.rb +2 -3
  31. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +51 -14
  32. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +40 -6
  33. data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +18 -10
  34. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +235 -167
  35. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_table.rb +4 -2
  36. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_xml.rb +3 -1
  37. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +8 -8
  38. data/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb +36 -7
  39. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +43 -45
  40. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +8 -10
  41. data/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb +3 -3
  42. data/lib/active_record/connection_adapters/sqlserver/type/binary.rb +5 -4
  43. data/lib/active_record/connection_adapters/sqlserver/type/boolean.rb +3 -3
  44. data/lib/active_record/connection_adapters/sqlserver/type/char.rb +7 -4
  45. data/lib/active_record/connection_adapters/sqlserver/type/data.rb +5 -3
  46. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +7 -5
  47. data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +8 -8
  48. data/lib/active_record/connection_adapters/sqlserver/type/datetime2.rb +2 -2
  49. data/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb +2 -2
  50. data/lib/active_record/connection_adapters/sqlserver/type/decimal.rb +5 -4
  51. data/lib/active_record/connection_adapters/sqlserver/type/decimal_without_scale.rb +22 -0
  52. data/lib/active_record/connection_adapters/sqlserver/type/float.rb +3 -3
  53. data/lib/active_record/connection_adapters/sqlserver/type/integer.rb +3 -3
  54. data/lib/active_record/connection_adapters/sqlserver/type/json.rb +2 -1
  55. data/lib/active_record/connection_adapters/sqlserver/type/money.rb +4 -4
  56. data/lib/active_record/connection_adapters/sqlserver/type/real.rb +3 -3
  57. data/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb +3 -3
  58. data/lib/active_record/connection_adapters/sqlserver/type/small_money.rb +4 -4
  59. data/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb +3 -3
  60. data/lib/active_record/connection_adapters/sqlserver/type/string.rb +2 -2
  61. data/lib/active_record/connection_adapters/sqlserver/type/text.rb +3 -3
  62. data/lib/active_record/connection_adapters/sqlserver/type/time.rb +6 -6
  63. data/lib/active_record/connection_adapters/sqlserver/type/time_value_fractional.rb +8 -9
  64. data/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb +3 -3
  65. data/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb +3 -3
  66. data/lib/active_record/connection_adapters/sqlserver/type/unicode_char.rb +5 -4
  67. data/lib/active_record/connection_adapters/sqlserver/type/unicode_string.rb +2 -2
  68. data/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb +3 -3
  69. data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar.rb +6 -5
  70. data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb +4 -4
  71. data/lib/active_record/connection_adapters/sqlserver/type/uuid.rb +4 -3
  72. data/lib/active_record/connection_adapters/sqlserver/type/varbinary.rb +6 -5
  73. data/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb +4 -4
  74. data/lib/active_record/connection_adapters/sqlserver/type/varchar.rb +6 -5
  75. data/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb +4 -4
  76. data/lib/active_record/connection_adapters/sqlserver/type.rb +38 -35
  77. data/lib/active_record/connection_adapters/sqlserver/utils.rb +26 -12
  78. data/lib/active_record/connection_adapters/sqlserver/version.rb +2 -2
  79. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +271 -180
  80. data/lib/active_record/connection_adapters/sqlserver_column.rb +76 -16
  81. data/lib/active_record/sqlserver_base.rb +11 -9
  82. data/lib/active_record/tasks/sqlserver_database_tasks.rb +38 -39
  83. data/lib/activerecord-sqlserver-adapter.rb +3 -1
  84. data/lib/arel/visitors/sqlserver.rb +177 -56
  85. data/lib/arel_sqlserver.rb +4 -2
  86. data/test/appveyor/dbsetup.ps1 +4 -4
  87. data/test/cases/active_schema_test_sqlserver.rb +55 -0
  88. data/test/cases/adapter_test_sqlserver.rb +258 -173
  89. data/test/cases/change_column_collation_test_sqlserver.rb +33 -0
  90. data/test/cases/change_column_null_test_sqlserver.rb +14 -12
  91. data/test/cases/coerced_tests.rb +1421 -397
  92. data/test/cases/column_test_sqlserver.rb +321 -315
  93. data/test/cases/connection_test_sqlserver.rb +17 -20
  94. data/test/cases/disconnected_test_sqlserver.rb +39 -0
  95. data/test/cases/eager_load_too_many_ids_test_sqlserver.rb +18 -0
  96. data/test/cases/execute_procedure_test_sqlserver.rb +28 -19
  97. data/test/cases/fetch_test_sqlserver.rb +33 -21
  98. data/test/cases/fully_qualified_identifier_test_sqlserver.rb +15 -19
  99. data/test/cases/helper_sqlserver.rb +15 -15
  100. data/test/cases/in_clause_test_sqlserver.rb +63 -0
  101. data/test/cases/index_test_sqlserver.rb +15 -15
  102. data/test/cases/json_test_sqlserver.rb +25 -25
  103. data/test/cases/lateral_test_sqlserver.rb +35 -0
  104. data/test/cases/migration_test_sqlserver.rb +74 -27
  105. data/test/cases/optimizer_hints_test_sqlserver.rb +72 -0
  106. data/test/cases/order_test_sqlserver.rb +59 -53
  107. data/test/cases/pessimistic_locking_test_sqlserver.rb +27 -33
  108. data/test/cases/primary_keys_test_sqlserver.rb +103 -0
  109. data/test/cases/rake_test_sqlserver.rb +70 -45
  110. data/test/cases/schema_dumper_test_sqlserver.rb +124 -109
  111. data/test/cases/schema_test_sqlserver.rb +20 -26
  112. data/test/cases/scratchpad_test_sqlserver.rb +4 -4
  113. data/test/cases/showplan_test_sqlserver.rb +28 -35
  114. data/test/cases/specific_schema_test_sqlserver.rb +68 -65
  115. data/test/cases/transaction_test_sqlserver.rb +18 -20
  116. data/test/cases/trigger_test_sqlserver.rb +14 -13
  117. data/test/cases/utils_test_sqlserver.rb +70 -70
  118. data/test/cases/uuid_test_sqlserver.rb +13 -14
  119. data/test/debug.rb +8 -6
  120. data/test/migrations/create_clients_and_change_column_collation.rb +19 -0
  121. data/test/migrations/create_clients_and_change_column_null.rb +3 -1
  122. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +4 -4
  123. data/test/models/sqlserver/booking.rb +3 -1
  124. data/test/models/sqlserver/composite_pk.rb +9 -0
  125. data/test/models/sqlserver/customers_view.rb +3 -1
  126. data/test/models/sqlserver/datatype.rb +2 -0
  127. data/test/models/sqlserver/datatype_migration.rb +2 -0
  128. data/test/models/sqlserver/dollar_table_name.rb +3 -1
  129. data/test/models/sqlserver/edge_schema.rb +3 -3
  130. data/test/models/sqlserver/fk_has_fk.rb +3 -1
  131. data/test/models/sqlserver/fk_has_pk.rb +3 -1
  132. data/test/models/sqlserver/natural_pk_data.rb +4 -2
  133. data/test/models/sqlserver/natural_pk_int_data.rb +3 -1
  134. data/test/models/sqlserver/no_pk_data.rb +3 -1
  135. data/test/models/sqlserver/object_default.rb +3 -1
  136. data/test/models/sqlserver/quoted_table.rb +4 -2
  137. data/test/models/sqlserver/quoted_view_1.rb +3 -1
  138. data/test/models/sqlserver/quoted_view_2.rb +3 -1
  139. data/test/models/sqlserver/sst_memory.rb +3 -1
  140. data/test/models/sqlserver/sst_string_collation.rb +3 -0
  141. data/test/models/sqlserver/string_default.rb +3 -1
  142. data/test/models/sqlserver/string_defaults_big_view.rb +3 -1
  143. data/test/models/sqlserver/string_defaults_view.rb +3 -1
  144. data/test/models/sqlserver/tinyint_pk.rb +3 -1
  145. data/test/models/sqlserver/trigger.rb +4 -2
  146. data/test/models/sqlserver/trigger_history.rb +3 -1
  147. data/test/models/sqlserver/upper.rb +3 -1
  148. data/test/models/sqlserver/uppered.rb +3 -1
  149. data/test/models/sqlserver/uuid.rb +3 -1
  150. data/test/schema/sqlserver_specific_schema.rb +56 -21
  151. data/test/support/coerceable_test_sqlserver.rb +19 -13
  152. data/test/support/connection_reflection.rb +3 -2
  153. data/test/support/core_ext/query_cache.rb +4 -1
  154. data/test/support/load_schema_sqlserver.rb +5 -5
  155. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic.dump +0 -0
  156. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic_associations.dump +0 -0
  157. data/test/support/minitest_sqlserver.rb +3 -1
  158. data/test/support/paths_sqlserver.rb +11 -11
  159. data/test/support/rake_helpers.rb +15 -10
  160. data/test/support/sql_counter_sqlserver.rb +16 -15
  161. data/test/support/test_in_memory_oltp.rb +9 -7
  162. metadata +47 -13
  163. data/.travis.yml +0 -25
  164. data/lib/active_record/connection_adapters/sqlserver/core_ext/query_methods.rb +0 -26
@@ -1,16 +1,18 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module ConnectionHandling
3
5
  def sqlserver_connection(config) #:nodoc:
4
6
  config = config.symbolize_keys
5
- config.reverse_merge! mode: :dblib
6
- mode = config[:mode].to_s.downcase.underscore.to_sym
7
- case mode
8
- when :dblib
9
- require 'tiny_tds'
10
- else
11
- raise ArgumentError, "Unknown connection mode in #{config.inspect}."
12
- end
13
- ConnectionAdapters::SQLServerAdapter.new(nil, nil, config.merge(mode: mode))
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
+ )
14
16
  end
15
17
  end
16
18
  end
@@ -1,28 +1,33 @@
1
- require 'active_record/tasks/database_tasks'
2
- require 'shellwords'
3
- require 'ipaddr'
4
- require 'socket'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/tasks/database_tasks"
4
+ require "shellwords"
5
+ require "ipaddr"
6
+ require "socket"
5
7
 
6
8
  module ActiveRecord
7
9
  module Tasks
8
-
9
10
  class SQLServerDatabaseTasks
10
-
11
- DEFAULT_COLLATION = 'SQL_Latin1_General_CP1_CI_AS'
11
+ DEFAULT_COLLATION = "SQL_Latin1_General_CP1_CI_AS"
12
12
 
13
13
  delegate :connection, :establish_connection, :clear_active_connections!,
14
14
  to: ActiveRecord::Base
15
15
 
16
+ def self.using_database_configurations?
17
+ true
18
+ end
19
+
16
20
  def initialize(configuration)
17
21
  @configuration = configuration
22
+ @configuration_hash = @configuration.configuration_hash
18
23
  end
19
24
 
20
25
  def create(master_established = false)
21
26
  establish_master_connection unless master_established
22
- connection.create_database configuration['database'], configuration.merge('collation' => default_collation)
27
+ connection.create_database configuration.database, configuration_hash.merge(collation: default_collation)
23
28
  establish_connection configuration
24
- rescue ActiveRecord::StatementInvalid => error
25
- if /database .* already exists/i === error.message
29
+ rescue ActiveRecord::StatementInvalid => e
30
+ if /database .* already exists/i === e.message
26
31
  raise DatabaseAlreadyExists
27
32
  else
28
33
  raise
@@ -31,7 +36,7 @@ module ActiveRecord
31
36
 
32
37
  def drop
33
38
  establish_master_connection
34
- connection.drop_database configuration['database']
39
+ connection.drop_database configuration.database
35
40
  end
36
41
 
37
42
  def charset
@@ -49,27 +54,28 @@ module ActiveRecord
49
54
  end
50
55
 
51
56
  def structure_dump(filename, extra_flags)
52
- server_arg = "-S #{Shellwords.escape(configuration['host'])}"
53
- 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]
54
59
  command = [
55
60
  "defncopy-ttds",
56
61
  server_arg,
57
- "-D #{Shellwords.escape(configuration['database'])}",
58
- "-U #{Shellwords.escape(configuration['username'])}",
59
- "-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])}",
60
65
  "-o #{Shellwords.escape(filename)}",
61
66
  ]
62
67
  table_args = connection.tables.map { |t| Shellwords.escape(t) }
63
68
  command.concat(table_args)
64
69
  view_args = connection.views.map { |v| Shellwords.escape(v) }
65
70
  command.concat(view_args)
66
- raise 'Error dumping database' unless Kernel.system(command.join(' '))
71
+ raise "Error dumping database" unless Kernel.system(command.join(" "))
72
+
67
73
  dump = File.read(filename)
68
- dump.gsub!(/^USE .*$\nGO\n/, '') # Strip db USE statements
69
- dump.gsub!(/^GO\n/, '') # Strip db GO statements
70
- dump.gsub!(/nvarchar\(8000\)/, 'nvarchar(4000)') # Fix nvarchar(8000) column defs
71
- dump.gsub!(/nvarchar\(-1\)/, 'nvarchar(max)') # Fix nvarchar(-1) column defs
72
- 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
73
79
  File.open(filename, "w") { |file| file.puts dump }
74
80
  end
75
81
 
@@ -77,33 +83,27 @@ module ActiveRecord
77
83
  connection.execute File.read(filename)
78
84
  end
79
85
 
80
-
81
86
  private
82
87
 
83
- def configuration
84
- @configuration
85
- end
88
+ attr_reader :configuration, :configuration_hash
86
89
 
87
90
  def default_collation
88
- configuration['collation'] || DEFAULT_COLLATION
91
+ configuration_hash[:collation] || DEFAULT_COLLATION
89
92
  end
90
93
 
91
94
  def establish_master_connection
92
- establish_connection configuration.merge('database' => 'master')
95
+ establish_connection configuration_hash.merge(database: "master")
93
96
  end
94
-
95
97
  end
96
98
 
97
99
  module DatabaseTasksSQLServer
98
-
99
100
  extend ActiveSupport::Concern
100
101
 
101
102
  module ClassMethods
102
-
103
103
  LOCAL_IPADDR = [
104
- IPAddr.new('192.168.0.0/16'),
105
- IPAddr.new('10.0.0.0/8'),
106
- 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")
107
107
  ]
108
108
 
109
109
  private
@@ -113,21 +113,20 @@ module ActiveRecord
113
113
  end
114
114
 
115
115
  def configuration_host_ip(configuration)
116
- return nil unless configuration['host']
117
- 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]
118
119
  end
119
120
 
120
121
  def local_ipaddr?(host_ip)
121
122
  return false unless host_ip
123
+
122
124
  LOCAL_IPADDR.any? { |ip| ip.include?(host_ip) }
123
125
  end
124
-
125
126
  end
126
-
127
127
  end
128
128
 
129
129
  DatabaseTasks.register_task %r{sqlserver}, SQLServerDatabaseTasks
130
130
  DatabaseTasks.send :include, DatabaseTasksSQLServer
131
-
132
131
  end
133
132
  end
@@ -1 +1,3 @@
1
- require 'active_record/connection_adapters/sqlserver_adapter'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/connection_adapters/sqlserver_adapter"
@@ -1,47 +1,54 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Arel
2
4
  module Visitors
3
5
  class SQLServer < Arel::Visitors::ToSql
4
-
5
6
  OFFSET = " OFFSET "
6
7
  ROWS = " ROWS"
7
8
  FETCH = " FETCH NEXT "
8
9
  FETCH0 = " FETCH FIRST (SELECT 0) "
9
10
  ROWS_ONLY = " ROWS ONLY"
10
11
 
11
-
12
12
  private
13
13
 
14
- # SQLServer ToSql/Visitor (Overides)
14
+ # SQLServer ToSql/Visitor (Overrides)
15
15
 
16
- def visit_Arel_Nodes_BindParam o, collector
17
- collector.add_bind(o.value) { |i| "@#{i-1}" }
18
- end
16
+ BIND_BLOCK = proc { |i| "@#{i - 1}" }
17
+ private_constant :BIND_BLOCK
19
18
 
20
- def visit_Arel_Nodes_Bin o, collector
19
+ def bind_block; BIND_BLOCK; end
20
+
21
+ def visit_Arel_Nodes_Bin(o, collector)
21
22
  visit o.expr, collector
22
23
  collector << " #{ActiveRecord::ConnectionAdapters::SQLServerAdapter.cs_equality_operator} "
23
24
  end
24
25
 
25
- def visit_Arel_Nodes_UpdateStatement(o, a)
26
+ def visit_Arel_Nodes_Concat(o, collector)
27
+ visit o.left, collector
28
+ collector << " + "
29
+ visit o.right, collector
30
+ end
31
+
32
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
26
33
  if o.orders.any? && o.limit.nil?
27
34
  o.limit = Nodes::Limit.new(9_223_372_036_854_775_807)
28
35
  end
29
36
  super
30
37
  end
31
38
 
32
- def visit_Arel_Nodes_Lock o, collector
33
- o.expr = Arel.sql('WITH(UPDLOCK)') if o.expr.to_s =~ /FOR UPDATE/
34
- collector << SPACE
39
+ def visit_Arel_Nodes_Lock(o, collector)
40
+ o.expr = Arel.sql("WITH(UPDLOCK)") if o.expr.to_s =~ /FOR UPDATE/
41
+ collector << " "
35
42
  visit o.expr, collector
36
43
  end
37
44
 
38
- def visit_Arel_Nodes_Offset o, collector
45
+ def visit_Arel_Nodes_Offset(o, collector)
39
46
  collector << OFFSET
40
47
  visit o.expr, collector
41
48
  collector << ROWS
42
49
  end
43
50
 
44
- def visit_Arel_Nodes_Limit o, collector
51
+ def visit_Arel_Nodes_Limit(o, collector)
45
52
  if node_value(o) == 0
46
53
  collector << FETCH0
47
54
  collector << ROWS_ONLY
@@ -52,14 +59,52 @@ module Arel
52
59
  end
53
60
  end
54
61
 
55
- def visit_Arel_Nodes_SelectStatement o, collector
62
+ def visit_Arel_Nodes_Grouping(o, collector)
63
+ remove_invalid_ordering_from_select_statement(o.expr)
64
+ super
65
+ end
66
+
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
+ # Use cast_type on encrypted attributes. Don't encrypt them again
87
+ column_type = column_type.cast_type if column_type.is_a?(ActiveRecord::Encryption::EncryptedAttributeType)
88
+ attrs = values.map { |value| ActiveRecord::Relation::QueryAttribute.new(column_name, value, column_type) }
89
+
90
+ collector.add_binds(attrs, &bind_block)
91
+ # Monkey-patch end.
92
+ else
93
+ collector.add_binds(values, &bind_block)
94
+ end
95
+
96
+ collector << ")"
97
+ collector
98
+ end
99
+
100
+ def visit_Arel_Nodes_SelectStatement(o, collector)
56
101
  @select_statement = o
57
102
  distinct_One_As_One_Is_So_Not_Fetch o
58
103
  if o.with
59
104
  collector = visit o.with, collector
60
- collector << SPACE
105
+ collector << " "
61
106
  end
62
- collector = o.cores.inject(collector) { |c,x|
107
+ collector = o.cores.inject(collector) { |c, x|
63
108
  visit_Arel_Nodes_SelectCore(x, c)
64
109
  }
65
110
  collector = visit_Orders_And_Let_Fetch_Happen o, collector
@@ -69,19 +114,31 @@ module Arel
69
114
  @select_statement = nil
70
115
  end
71
116
 
72
- def visit_Arel_Table o, collector
117
+ def visit_Arel_Nodes_SelectCore(o, collector)
118
+ collector = super
119
+ maybe_visit o.optimizer_hints, collector
120
+ end
121
+
122
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
123
+ hints = o.expr.map { |v| sanitize_as_option_clause(v) }.join(", ")
124
+ collector << "OPTION (#{hints})"
125
+ end
126
+
127
+ def visit_Arel_Table(o, collector)
73
128
  # Apparently, o.engine.connection can actually be a different adapter
74
129
  # than sqlserver. Can be removed if fixed in ActiveRecord. See:
75
130
  # github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/450
76
- table_name = begin
77
- if o.class.engine.connection.respond_to?(:sqlserver?) && o.class.engine.connection.database_prefix_remote_server?
78
- remote_server_table_name(o)
79
- else
131
+ table_name =
132
+ begin
133
+ if o.class.engine.connection.respond_to?(:sqlserver?) && o.class.engine.connection.database_prefix_remote_server?
134
+ remote_server_table_name(o)
135
+ else
136
+ quote_table_name(o.name)
137
+ end
138
+ rescue Exception
80
139
  quote_table_name(o.name)
81
140
  end
82
- rescue Exception => e
83
- quote_table_name(o.name)
84
- end
141
+
85
142
  if o.table_alias
86
143
  collector << "#{table_name} #{quote_table_name o.table_alias}"
87
144
  else
@@ -89,73 +146,109 @@ module Arel
89
146
  end
90
147
  end
91
148
 
92
- def visit_Arel_Nodes_JoinSource o, collector
149
+ def visit_Arel_Nodes_JoinSource(o, collector)
93
150
  if o.left
94
151
  collector = visit o.left, collector
95
152
  collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector
96
153
  end
97
154
  if o.right.any?
98
- collector << SPACE if o.left
99
- collector = inject_join o.right, collector, ' '
155
+ collector << " " if o.left
156
+ collector = inject_join o.right, collector, " "
100
157
  end
101
158
  collector
102
159
  end
103
160
 
104
- def visit_Arel_Nodes_InnerJoin o, collector
105
- collector << "INNER JOIN "
106
- collector = visit o.left, collector
107
- collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
108
- if o.right
109
- collector << SPACE
110
- visit(o.right, collector)
161
+ def visit_Arel_Nodes_InnerJoin(o, collector)
162
+ if o.left.is_a?(Arel::Nodes::As) && o.left.left.is_a?(Arel::Nodes::Lateral)
163
+ collector << "CROSS "
164
+ visit o.left, collector
111
165
  else
112
- collector
166
+ collector << "INNER JOIN "
167
+ collector = visit o.left, collector
168
+ collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
169
+ if o.right
170
+ collector << " "
171
+ visit(o.right, collector)
172
+ else
173
+ collector
174
+ end
113
175
  end
114
176
  end
115
177
 
116
- def visit_Arel_Nodes_OuterJoin o, collector
117
- collector << "LEFT OUTER JOIN "
118
- collector = visit o.left, collector
119
- collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
120
- collector << SPACE
121
- visit o.right, collector
178
+ def visit_Arel_Nodes_OuterJoin(o, collector)
179
+ if o.left.is_a?(Arel::Nodes::As) && o.left.left.is_a?(Arel::Nodes::Lateral)
180
+ collector << "OUTER "
181
+ visit o.left, collector
182
+ else
183
+ collector << "LEFT OUTER JOIN "
184
+ collector = visit o.left, collector
185
+ collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
186
+ collector << " "
187
+ visit o.right, collector
188
+ end
189
+ end
190
+
191
+ def visit_Arel_Nodes_In(o, collector)
192
+ if Array === o.right
193
+ o.right.each { |node| remove_invalid_ordering_from_select_statement(node) }
194
+ else
195
+ remove_invalid_ordering_from_select_statement(o.right)
196
+ end
197
+
198
+ super
199
+ end
200
+
201
+ def collect_optimizer_hints(o, collector)
202
+ collector
122
203
  end
123
204
 
124
205
  # SQLServer ToSql/Visitor (Additions)
125
206
 
126
- def visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, options = {}
207
+ def visit_Arel_Nodes_SelectStatement_SQLServer_Lock(collector, options = {})
127
208
  if select_statement_lock?
128
209
  collector = visit @select_statement.lock, collector
129
- collector << SPACE if options[:space]
210
+ collector << " " if options[:space]
130
211
  end
131
212
  collector
132
213
  end
133
214
 
134
- def visit_Orders_And_Let_Fetch_Happen o, collector
215
+ def visit_Orders_And_Let_Fetch_Happen(o, collector)
135
216
  make_Fetch_Possible_And_Deterministic o
136
217
  unless o.orders.empty?
137
- collector << SPACE
138
- collector << ORDER_BY
218
+ collector << " ORDER BY "
139
219
  len = o.orders.length - 1
140
220
  o.orders.each_with_index { |x, i|
141
221
  collector = visit(x, collector)
142
- collector << COMMA unless len == i
222
+ collector << ", " unless len == i
143
223
  }
144
224
  end
145
225
  collector
146
226
  end
147
227
 
148
- def visit_Make_Fetch_Happen o, collector
228
+ def visit_Make_Fetch_Happen(o, collector)
149
229
  o.offset = Nodes::Offset.new(0) if o.limit && !o.offset
150
230
  collector = visit o.offset, collector if o.offset
151
231
  collector = visit o.limit, collector if o.limit
152
232
  collector
153
233
  end
154
234
 
235
+ def visit_Arel_Nodes_Lateral(o, collector)
236
+ collector << "APPLY"
237
+ collector << " "
238
+ if o.expr.is_a?(Arel::Nodes::SelectStatement)
239
+ collector << "("
240
+ visit(o.expr, collector)
241
+ collector << ")"
242
+ else
243
+ visit(o.expr, collector)
244
+ end
245
+ end
246
+
155
247
  # SQLServer Helpers
156
248
 
157
249
  def node_value(node)
158
250
  return nil unless node
251
+
159
252
  case node.expr
160
253
  when NilClass then nil
161
254
  when Numeric then node.expr
@@ -167,18 +260,20 @@ module Arel
167
260
  @select_statement && @select_statement.lock
168
261
  end
169
262
 
170
- def make_Fetch_Possible_And_Deterministic o
263
+ def make_Fetch_Possible_And_Deterministic(o)
171
264
  return if o.limit.nil? && o.offset.nil?
265
+
172
266
  t = table_From_Statement o
173
267
  pk = primary_Key_From_Table t
174
268
  return unless pk
269
+
175
270
  if o.orders.empty?
176
271
  # Prefer deterministic vs a simple `(SELECT NULL)` expr.
177
272
  o.orders = [pk.asc]
178
273
  end
179
274
  end
180
275
 
181
- def distinct_One_As_One_Is_So_Not_Fetch o
276
+ def distinct_One_As_One_Is_So_Not_Fetch(o)
182
277
  core = o.cores.first
183
278
  distinct = Nodes::Distinct === core.set_quantifier
184
279
  oneasone = core.projections.all? { |x| x == ActiveRecord::FinderMethods::ONE_AS_ONE }
@@ -189,30 +284,56 @@ module Arel
189
284
  end
190
285
  end
191
286
 
192
- def table_From_Statement o
287
+ def table_From_Statement(o)
193
288
  core = o.cores.first
194
289
  if Arel::Table === core.from
195
290
  core.from
196
291
  elsif Arel::Nodes::SqlLiteral === core.from
197
292
  Arel::Table.new(core.from)
198
293
  elsif Arel::Nodes::JoinSource === core.source
199
- Arel::Nodes::SqlLiteral === core.source.left ? Arel::Table.new(core.source.left, @engine) : core.source.left
294
+ Arel::Nodes::SqlLiteral === core.source.left ? Arel::Table.new(core.source.left, @engine) : core.source.left.left
200
295
  end
201
296
  end
202
297
 
203
- def primary_Key_From_Table t
298
+ def primary_Key_From_Table(t)
204
299
  return unless t
205
- column_name = @connection.schema_cache.primary_keys(t.name) ||
206
- @connection.schema_cache.columns_hash(t.name).first.try(:second).try(:name)
300
+
301
+ primary_keys = @connection.schema_cache.primary_keys(t.name)
302
+ column_name = nil
303
+
304
+ case primary_keys
305
+ when NilClass
306
+ column_name = @connection.schema_cache.columns_hash(t.name).first.try(:second).try(:name)
307
+ when String
308
+ column_name = primary_keys
309
+ when Array
310
+ candidate_columns = @connection.schema_cache.columns_hash(t.name).slice(*primary_keys).values
311
+ candidate_column = candidate_columns.find(&:is_identity?)
312
+ candidate_column ||= candidate_columns.first
313
+ column_name = candidate_column.try(:name)
314
+ end
315
+
207
316
  column_name ? t[column_name] : nil
208
317
  end
209
318
 
210
- def remote_server_table_name o
319
+ def remote_server_table_name(o)
211
320
  ActiveRecord::ConnectionAdapters::SQLServer::Utils.extract_identifiers(
212
321
  "#{o.class.engine.connection.database_prefix}#{o.name}"
213
322
  ).quoted
214
323
  end
215
324
 
325
+ # Need to remove ordering from subqueries unless TOP/OFFSET also used. Otherwise, SQLServer
326
+ # returns error "The ORDER BY clause is invalid in views, inline functions, derived tables,
327
+ # subqueries, and common table expressions, unless TOP, OFFSET or FOR XML is also specified."
328
+ def remove_invalid_ordering_from_select_statement(node)
329
+ return unless Arel::Nodes::SelectStatement === node
330
+
331
+ node.orders = [] unless node.offset || node.limit
332
+ end
333
+
334
+ def sanitize_as_option_clause(value)
335
+ value.gsub(%r{OPTION \s* \( (.+) \)}xi, "\\1")
336
+ end
216
337
  end
217
338
  end
218
339
  end
@@ -1,2 +1,4 @@
1
- require 'arel'
2
- require 'arel/visitors/sqlserver'
1
+ # frozen_string_literal: true
2
+
3
+ require "arel"
4
+ require "arel/visitors/sqlserver"
@@ -5,15 +5,15 @@ Write-Output "Setting up..."
5
5
 
6
6
  Write-Output "Setting variables..."
7
7
  $serverName = $env:COMPUTERNAME
8
- $instances = @('SQL2012SP1', 'SQL2014')
8
+ $instanceNames = @('SQL2014')
9
9
  $smo = 'Microsoft.SqlServer.Management.Smo.'
10
10
  $wmi = new-object ($smo + 'Wmi.ManagedComputer')
11
11
 
12
12
  Write-Output "Configure Instances..."
13
- foreach ($instance in $instances) {
14
- Write-Output "Instance $instance ..."
13
+ foreach ($instanceName in $instanceNames) {
14
+ Write-Output "Instance $instanceName ..."
15
15
  Write-Output "Enable TCP/IP and port 1433..."
16
- $uri = "ManagedComputer[@Name='$serverName']/ServerInstance[@Name='$instance']/ServerProtocol[@Name='Tcp']"
16
+ $uri = "ManagedComputer[@Name='$serverName']/ServerInstance[@Name='$instanceName']/ServerProtocol[@Name='Tcp']"
17
17
  $tcp = $wmi.GetSmoObject($uri)
18
18
  $tcp.IsEnabled = $true
19
19
  foreach ($ipAddress in $Tcp.IPAddresses) {
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cases/helper_sqlserver"
4
+
5
+ class ActiveSchemaTestSQLServer < ActiveRecord::TestCase
6
+ before do
7
+ connection.create_table :schema_test_table, force: true, id: false do |t|
8
+ t.column :foo, :string, limit: 100
9
+ t.column :state, :string
10
+ end
11
+ end
12
+
13
+ after do
14
+ connection.drop_table :schema_test_table rescue nil
15
+ end
16
+
17
+ it 'default index' do
18
+ assert_sql('CREATE INDEX [index_schema_test_table_on_foo] ON [schema_test_table] ([foo])') do
19
+ connection.add_index :schema_test_table, "foo"
20
+ end
21
+ end
22
+
23
+ it 'unique index' do
24
+ assert_sql('CREATE UNIQUE INDEX [index_schema_test_table_on_foo] ON [schema_test_table] ([foo])') do
25
+ connection.add_index :schema_test_table, "foo", unique: true
26
+ end
27
+ end
28
+
29
+ it 'where condition on index' do
30
+ assert_sql("CREATE INDEX [index_schema_test_table_on_foo] ON [schema_test_table] ([foo]) WHERE state = 'active'") do
31
+ connection.add_index :schema_test_table, "foo", where: "state = 'active'"
32
+ end
33
+ end
34
+
35
+ it 'if index does not exist' do
36
+ assert_sql("IF NOT EXISTS (SELECT name FROM sysindexes WHERE name = 'index_schema_test_table_on_foo') " \
37
+ "CREATE INDEX [index_schema_test_table_on_foo] ON [schema_test_table] ([foo])") do
38
+ connection.add_index :schema_test_table, "foo", if_not_exists: true
39
+ end
40
+ end
41
+
42
+ describe "index types" do
43
+ it 'clustered index' do
44
+ assert_sql('CREATE CLUSTERED INDEX [index_schema_test_table_on_foo] ON [schema_test_table] ([foo])') do
45
+ connection.add_index :schema_test_table, "foo", type: :clustered
46
+ end
47
+ end
48
+
49
+ it 'nonclustered index' do
50
+ assert_sql('CREATE NONCLUSTERED INDEX [index_schema_test_table_on_foo] ON [schema_test_table] ([foo])') do
51
+ connection.add_index :schema_test_table, "foo", type: :nonclustered
52
+ end
53
+ end
54
+ end
55
+ end