activerecord-sqlserver-adapter 5.2.1 → 7.0.0.0

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