activerecord-sqlserver-adapter 6.1.2.1 → 7.2.4

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +30 -0
  3. data/.devcontainer/boot.sh +22 -0
  4. data/.devcontainer/devcontainer.json +38 -0
  5. data/.devcontainer/docker-compose.yml +42 -0
  6. data/.github/workflows/ci.yml +7 -4
  7. data/.gitignore +3 -1
  8. data/CHANGELOG.md +19 -42
  9. data/Dockerfile.ci +3 -3
  10. data/Gemfile +6 -1
  11. data/MIT-LICENSE +1 -1
  12. data/README.md +113 -27
  13. data/RUNNING_UNIT_TESTS.md +27 -14
  14. data/Rakefile +2 -6
  15. data/VERSION +1 -1
  16. data/activerecord-sqlserver-adapter.gemspec +3 -3
  17. data/appveyor.yml +4 -6
  18. data/docker-compose.ci.yml +2 -1
  19. data/lib/active_record/connection_adapters/sqlserver/core_ext/abstract_adapter.rb +20 -0
  20. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +6 -4
  21. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +5 -23
  22. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +10 -7
  23. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +2 -0
  24. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +12 -2
  25. data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +24 -16
  26. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +0 -31
  27. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +143 -155
  28. data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +5 -5
  29. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +57 -56
  30. data/lib/active_record/connection_adapters/sqlserver/savepoints.rb +26 -0
  31. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +14 -12
  32. data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +11 -0
  33. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +213 -57
  34. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +3 -3
  35. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +13 -2
  36. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +4 -6
  37. data/lib/active_record/connection_adapters/sqlserver/type/data.rb +19 -1
  38. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +1 -1
  39. data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +1 -1
  40. data/lib/active_record/connection_adapters/sqlserver/type/time.rb +1 -1
  41. data/lib/active_record/connection_adapters/sqlserver/utils.rb +21 -10
  42. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +187 -187
  43. data/lib/active_record/connection_adapters/sqlserver_column.rb +1 -0
  44. data/lib/active_record/tasks/sqlserver_database_tasks.rb +42 -33
  45. data/lib/arel/visitors/sqlserver.rb +77 -34
  46. data/test/cases/active_schema_test_sqlserver.rb +127 -0
  47. data/test/cases/adapter_test_sqlserver.rb +114 -26
  48. data/test/cases/coerced_tests.rb +1121 -340
  49. data/test/cases/column_test_sqlserver.rb +67 -64
  50. data/test/cases/connection_test_sqlserver.rb +3 -6
  51. data/test/cases/dbconsole.rb +19 -0
  52. data/test/cases/disconnected_test_sqlserver.rb +8 -5
  53. data/test/cases/eager_load_too_many_ids_test_sqlserver.rb +18 -0
  54. data/test/cases/enum_test_sqlserver.rb +49 -0
  55. data/test/cases/execute_procedure_test_sqlserver.rb +9 -5
  56. data/test/cases/fetch_test_sqlserver.rb +19 -0
  57. data/test/cases/helper_sqlserver.rb +11 -5
  58. data/test/cases/index_test_sqlserver.rb +8 -6
  59. data/test/cases/json_test_sqlserver.rb +1 -1
  60. data/test/cases/lateral_test_sqlserver.rb +2 -2
  61. data/test/cases/migration_test_sqlserver.rb +19 -1
  62. data/test/cases/optimizer_hints_test_sqlserver.rb +21 -12
  63. data/test/cases/pessimistic_locking_test_sqlserver.rb +8 -7
  64. data/test/cases/primary_keys_test_sqlserver.rb +2 -2
  65. data/test/cases/rake_test_sqlserver.rb +10 -5
  66. data/test/cases/schema_dumper_test_sqlserver.rb +155 -109
  67. data/test/cases/schema_test_sqlserver.rb +64 -1
  68. data/test/cases/showplan_test_sqlserver.rb +7 -7
  69. data/test/cases/specific_schema_test_sqlserver.rb +17 -13
  70. data/test/cases/transaction_test_sqlserver.rb +13 -8
  71. data/test/cases/trigger_test_sqlserver.rb +20 -0
  72. data/test/cases/utils_test_sqlserver.rb +2 -2
  73. data/test/cases/uuid_test_sqlserver.rb +8 -0
  74. data/test/cases/view_test_sqlserver.rb +58 -0
  75. data/test/config.yml +1 -2
  76. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +1 -1
  77. data/test/models/sqlserver/alien.rb +5 -0
  78. data/test/models/sqlserver/table_with_spaces.rb +5 -0
  79. data/test/models/sqlserver/trigger.rb +8 -0
  80. data/test/schema/sqlserver_specific_schema.rb +54 -6
  81. data/test/support/coerceable_test_sqlserver.rb +4 -4
  82. data/test/support/connection_reflection.rb +3 -9
  83. data/test/support/core_ext/query_cache.rb +7 -1
  84. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic.dump +0 -0
  85. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic_associations.dump +0 -0
  86. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic.dump +0 -0
  87. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic_associations.dump +0 -0
  88. data/test/support/query_assertions.rb +49 -0
  89. data/test/support/rake_helpers.rb +3 -1
  90. data/test/support/table_definition_sqlserver.rb +24 -0
  91. data/test/support/test_in_memory_oltp.rb +2 -2
  92. metadata +41 -17
  93. data/lib/active_record/sqlserver_base.rb +0 -18
  94. data/test/cases/scratchpad_test_sqlserver.rb +0 -8
  95. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic.dump +0 -0
  96. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic_associations.dump +0 -0
  97. data/test/support/sql_counter_sqlserver.rb +0 -29
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module SQLServer
6
+ module Savepoints
7
+ def current_savepoint_name
8
+ current_transaction.savepoint_name
9
+ end
10
+
11
+ def create_savepoint(name = current_savepoint_name)
12
+ internal_execute("SAVE TRANSACTION #{name}", "TRANSACTION")
13
+ end
14
+
15
+ def exec_rollback_to_savepoint(name = current_savepoint_name)
16
+ internal_execute("ROLLBACK TRANSACTION #{name}", "TRANSACTION")
17
+ end
18
+
19
+ # SQL Server does require save-points to be explicitly released.
20
+ # See https://stackoverflow.com/questions/3101312/sql-server-2008-no-release-savepoint-for-current-transaction
21
+ def release_savepoint(_name)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -34,27 +34,29 @@ module ActiveRecord
34
34
  end
35
35
 
36
36
  def visit_CreateIndexDefinition(o)
37
- if_not_exists = o.if_not_exists
38
-
39
- o.if_not_exists = false
40
-
41
- sql = super
37
+ index = o.index
42
38
 
43
- if if_not_exists
44
- sql = "IF NOT EXISTS (SELECT name FROM sysindexes WHERE name = '#{o.index.name}') #{sql}"
45
- end
39
+ sql = []
40
+ sql << "IF NOT EXISTS (SELECT name FROM sysindexes WHERE name = '#{o.index.name}')" if o.if_not_exists
41
+ sql << "CREATE"
42
+ sql << "UNIQUE" if index.unique
43
+ sql << index.type.upcase if index.type
44
+ sql << "INDEX"
45
+ sql << "#{quote_column_name(index.name)} ON #{quote_table_name(index.table)}"
46
+ sql << "(#{quoted_columns(index)})"
47
+ sql << "WHERE #{index.where}" if index.where
46
48
 
47
- sql
49
+ sql.join(" ")
48
50
  end
49
51
 
50
52
  def add_column_options!(sql, options)
51
53
  sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
52
- if options[:null] == false
53
- sql << " NOT NULL"
54
- end
55
54
  if options[:collation].present?
56
55
  sql << " COLLATE #{options[:collation]}"
57
56
  end
57
+ if options[:null] == false
58
+ sql << " NOT NULL"
59
+ end
58
60
  if options[:is_identity] == true
59
61
  sql << " IDENTITY(1,1)"
60
62
  end
@@ -39,6 +39,17 @@ module ActiveRecord
39
39
  def default_primary_key?(column)
40
40
  super && column.is_identity?
41
41
  end
42
+
43
+ def schemas(stream)
44
+ schema_names = @connection.schema_names
45
+
46
+ if schema_names.any?
47
+ schema_names.sort.each do |name|
48
+ stream.puts " create_schema #{name.inspect}"
49
+ end
50
+ stream.puts
51
+ end
52
+ end
42
53
  end
43
54
  end
44
55
  end
@@ -23,10 +23,10 @@ module ActiveRecord
23
23
  pktable = fkdata["PKTABLE_NAME"]
24
24
  pkcolmn = fkdata["PKCOLUMN_NAME"]
25
25
  remove_foreign_key fktable, name: fkdata["FK_NAME"]
26
- do_execute "DELETE FROM #{quote_table_name(fktable)} WHERE #{quote_column_name(fkcolmn)} IN ( SELECT #{quote_column_name(pkcolmn)} FROM #{quote_table_name(pktable)} )"
26
+ execute "DELETE FROM #{quote_table_name(fktable)} WHERE #{quote_column_name(fkcolmn)} IN ( SELECT #{quote_column_name(pkcolmn)} FROM #{quote_table_name(pktable)} )"
27
27
  end
28
28
  end
29
- if options[:if_exists] && @version_year < 2016
29
+ if options[:if_exists] && version_year < 2016
30
30
  execute "IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = #{quote(table_name)}) DROP TABLE #{quote_table_name(table_name)}", "SCHEMA"
31
31
  else
32
32
  super
@@ -39,12 +39,12 @@ module ActiveRecord
39
39
  data.reduce([]) do |indexes, index|
40
40
  index = index.with_indifferent_access
41
41
 
42
- if index[:index_description] =~ /primary key/
42
+ if index[:index_description].match?(/primary key/)
43
43
  indexes
44
44
  else
45
45
  name = index[:index_name]
46
- unique = index[:index_description] =~ /unique/
47
- where = select_value("SELECT [filter_definition] FROM sys.indexes WHERE name = #{quote(name)}")
46
+ unique = index[:index_description].match?(/unique/)
47
+ where = select_value("SELECT [filter_definition] FROM sys.indexes WHERE name = #{quote(name)}", "SCHEMA")
48
48
  orders = {}
49
49
  columns = []
50
50
 
@@ -118,16 +118,21 @@ module ActiveRecord
118
118
  AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY'
119
119
  ORDER BY KCU.ORDINAL_POSITION ASC
120
120
  }.gsub(/[[:space:]]/, " ")
121
+
121
122
  binds = []
122
123
  nv128 = SQLServer::Type::UnicodeVarchar.new limit: 128
123
124
  binds << Relation::QueryAttribute.new("TABLE_NAME", identifier.object, nv128)
124
125
  binds << Relation::QueryAttribute.new("TABLE_SCHEMA", identifier.schema, nv128) unless identifier.schema.blank?
125
- sp_executesql(sql, "SCHEMA", binds).map { |r| r["name"] }
126
+
127
+ internal_exec_query(sql, "SCHEMA", binds).map { |row| row["name"] }
126
128
  end
127
129
 
128
- def rename_table(table_name, new_name)
129
- do_execute "EXEC sp_rename '#{table_name}', '#{new_name}'"
130
- rename_table_indexes(table_name, new_name)
130
+ def rename_table(table_name, new_name, **options)
131
+ validate_table_length!(new_name) unless options[:_uses_legacy_table_name]
132
+ schema_cache.clear_data_source_cache!(table_name.to_s)
133
+ schema_cache.clear_data_source_cache!(new_name.to_s)
134
+ execute "EXEC sp_rename '#{table_name}', '#{new_name}'"
135
+ rename_table_indexes(table_name, new_name, **options)
131
136
  end
132
137
 
133
138
  def remove_column(table_name, column_name, type = nil, **options)
@@ -137,12 +142,21 @@ module ActiveRecord
137
142
  remove_check_constraints(table_name, column_name)
138
143
  remove_default_constraint(table_name, column_name)
139
144
  remove_indexes(table_name, column_name)
140
- do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
145
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
141
146
  end
142
147
 
143
148
  def change_column(table_name, column_name, type, options = {})
144
149
  sql_commands = []
145
150
  indexes = []
151
+
152
+ if type == :datetime
153
+ # If no precision then default it to 6.
154
+ options[:precision] = 6 unless options.key?(:precision)
155
+
156
+ # If there is precision then column must be of type 'datetime2'.
157
+ type = :datetime2 unless options[:precision].nil?
158
+ end
159
+
146
160
  column_object = schema_cache.columns(table_name).find { |c| c.name.to_s == column_name.to_s }
147
161
  without_constraints = options.key?(:default) || options.key?(:limit)
148
162
  default = if !options.key?(:default) && column_object
@@ -150,25 +164,30 @@ module ActiveRecord
150
164
  else
151
165
  options[:default]
152
166
  end
167
+
153
168
  if without_constraints || (column_object && column_object.type != type.to_sym)
154
169
  remove_default_constraint(table_name, column_name)
155
170
  indexes = indexes(table_name).select { |index| index.columns.include?(column_name.to_s) }
156
171
  remove_indexes(table_name, column_name)
157
172
  end
173
+
158
174
  sql_commands << "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(options[:default], column_object)} WHERE #{quote_column_name(column_name)} IS NULL" if !options[:null].nil? && options[:null] == false && !options[:default].nil?
159
175
  alter_command = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, limit: options[:limit], precision: options[:precision], scale: options[:scale])}"
160
176
  alter_command += " COLLATE #{options[:collation]}" if options[:collation].present?
161
177
  alter_command += " NOT NULL" if !options[:null].nil? && options[:null] == false
162
178
  sql_commands << alter_command
179
+
163
180
  if without_constraints
164
181
  default = quote_default_expression(default, column_object || column_for(table_name, column_name))
165
182
  sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{default} FOR #{quote_column_name(column_name)}"
166
183
  end
184
+
167
185
  # Add any removed indexes back
168
186
  indexes.each do |index|
169
187
  sql_commands << "CREATE INDEX #{quote_table_name(index.name)} ON #{quote_table_name(table_name)} (#{index.columns.map { |c| quote_column_name(c) }.join(', ')})"
170
188
  end
171
- sql_commands.each { |c| do_execute(c) }
189
+
190
+ sql_commands.each { |c| execute(c) }
172
191
  clear_cache!
173
192
  end
174
193
 
@@ -179,7 +198,7 @@ module ActiveRecord
179
198
 
180
199
  remove_default_constraint(table_name, column_name)
181
200
  default = extract_new_default_value(default_or_changes)
182
- do_execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote_default_expression(default, column)} FOR #{quote_column_name(column_name)}"
201
+ execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote_default_expression(default, column)} FOR #{quote_column_name(column_name)}"
183
202
  clear_cache!
184
203
  end
185
204
 
@@ -199,23 +218,45 @@ module ActiveRecord
199
218
  end
200
219
 
201
220
  def remove_index!(table_name, index_name)
202
- do_execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
221
+ execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
222
+ end
223
+
224
+ def build_change_column_definition(table_name, column_name, type, **options) # :nodoc:
225
+ td = create_table_definition(table_name)
226
+ cd = td.new_column_definition(column_name, type, **options)
227
+ ChangeColumnDefinition.new(cd, column_name)
228
+ end
229
+
230
+ def build_change_column_default_definition(table_name, column_name, default_or_changes) # :nodoc:
231
+ column = column_for(table_name, column_name)
232
+ return unless column
233
+
234
+ default = extract_new_default_value(default_or_changes)
235
+ ChangeColumnDefaultDefinition.new(column, default)
203
236
  end
204
237
 
205
238
  def foreign_keys(table_name)
206
239
  identifier = SQLServer::Utils.extract_identifiers(table_name)
207
240
  fk_info = execute_procedure :sp_fkeys, nil, identifier.schema, nil, identifier.object, identifier.schema
208
- fk_info.map do |row|
209
- from_table = identifier.object
210
- to_table = row["PKTABLE_NAME"]
241
+
242
+ grouped_fk = fk_info.group_by { |row| row["FK_NAME"] }.values.each { |group| group.sort_by! { |row| row["KEY_SEQ"] } }
243
+ grouped_fk.map do |group|
244
+ row = group.first
211
245
  options = {
212
246
  name: row["FK_NAME"],
213
- column: row["FKCOLUMN_NAME"],
214
- primary_key: row["PKCOLUMN_NAME"],
215
247
  on_update: extract_foreign_key_action("update", row["FK_NAME"]),
216
248
  on_delete: extract_foreign_key_action("delete", row["FK_NAME"])
217
249
  }
218
- ForeignKeyDefinition.new from_table, to_table, options
250
+
251
+ if group.one?
252
+ options[:column] = row["FKCOLUMN_NAME"]
253
+ options[:primary_key] = row["PKCOLUMN_NAME"]
254
+ else
255
+ options[:column] = group.map { |row| row["FKCOLUMN_NAME"] }
256
+ options[:primary_key] = group.map { |row| row["PKCOLUMN_NAME"] }
257
+ end
258
+
259
+ ForeignKeyDefinition.new(identifier.object, row["PKTABLE_NAME"], options)
219
260
  end
220
261
  end
221
262
 
@@ -226,9 +267,33 @@ module ActiveRecord
226
267
  end
227
268
  end
228
269
 
270
+ def check_constraints(table_name)
271
+ sql = <<~SQL
272
+ select chk.name AS 'name',
273
+ chk.definition AS 'expression'
274
+ from sys.check_constraints chk
275
+ inner join sys.tables st on chk.parent_object_id = st.object_id
276
+ where
277
+ st.name = '#{table_name}'
278
+ SQL
279
+
280
+ chk_info = internal_exec_query(sql, "SCHEMA")
281
+
282
+ chk_info.map do |row|
283
+ options = {
284
+ name: row["name"]
285
+ }
286
+ expression = row["expression"]
287
+ expression = expression[1..-2] if expression.start_with?("(") && expression.end_with?(")")
288
+
289
+ CheckConstraintDefinition.new(table_name, expression, options)
290
+ end
291
+ end
292
+
229
293
  def type_to_sql(type, limit: nil, precision: nil, scale: nil, **)
230
- type_limitable = %w(string integer float char nchar varchar nvarchar).include?(type.to_s)
294
+ type_limitable = %w(string integer float char nchar varchar nvarchar binary_basic).include?(type.to_s)
231
295
  limit = nil unless type_limitable
296
+
232
297
  case type.to_s
233
298
  when "integer"
234
299
  case limit
@@ -238,6 +303,16 @@ module ActiveRecord
238
303
  when 5..8 then "bigint"
239
304
  else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
240
305
  end
306
+ when "time" # https://learn.microsoft.com/en-us/sql/t-sql/data-types/time-transact-sql
307
+ column_type_sql = type.to_s
308
+ if precision
309
+ if (0..7) === precision
310
+ column_type_sql << "(#{precision})"
311
+ else
312
+ raise(ActiveRecordError, "The time type has precision of #{precision}. The allowed range of precision is from 0 to 7")
313
+ end
314
+ end
315
+ column_type_sql
241
316
  when "datetime2"
242
317
  column_type_sql = super
243
318
  if precision
@@ -248,35 +323,59 @@ module ActiveRecord
248
323
  end
249
324
  end
250
325
  column_type_sql
326
+ when "datetimeoffset"
327
+ column_type_sql = super
328
+ if precision
329
+ if (0..7) === precision
330
+ column_type_sql << "(#{precision})"
331
+ else
332
+ raise(ActiveRecordError, "The datetimeoffset type has precision of #{precision}. The allowed range of precision is from 0 to 7")
333
+ end
334
+ end
335
+ column_type_sql
251
336
  else
252
337
  super
253
338
  end
254
339
  end
255
340
 
341
+ # In SQL Server only the first column added should have the `ADD` keyword.
342
+ def add_timestamps(table_name, **options)
343
+ fragments = add_timestamps_for_alter(table_name, **options)
344
+ fragments[1..].each { |fragment| fragment.sub!('ADD ', '') }
345
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{fragments.join(', ')}"
346
+ end
347
+
256
348
  def columns_for_distinct(columns, orders)
257
349
  order_columns = orders.reject(&:blank?).map { |s|
258
- s = s.to_sql unless s.is_a?(String)
350
+ s = visitor.compile(s) unless s.is_a?(String)
259
351
  s.gsub(/\s+(?:ASC|DESC)\b/i, "")
260
352
  .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
261
- }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
353
+ }
354
+ .reject(&:blank?)
355
+ .reject { |s| columns.include?(s) }
356
+
357
+ order_columns_aliased = order_columns.map.with_index { |column, i| "#{column} AS alias_#{i}" }
262
358
 
263
- (order_columns << super).join(", ")
359
+ (order_columns_aliased << super).join(", ")
264
360
  end
265
361
 
266
362
  def update_table_definition(table_name, base)
267
363
  SQLServer::Table.new(table_name, base)
268
364
  end
269
365
 
270
- def change_column_null(table_name, column_name, allow_null, default = nil)
366
+ def change_column_null(table_name, column_name, null, default = nil)
367
+ validate_change_column_null_argument!(null)
368
+
271
369
  table_id = SQLServer::Utils.extract_identifiers(table_name)
272
370
  column_id = SQLServer::Utils.extract_identifiers(column_name)
273
371
  column = column_for(table_name, column_name)
274
- if !allow_null.nil? && allow_null == false && !default.nil?
275
- do_execute("UPDATE #{table_id} SET #{column_id}=#{quote(default)} WHERE #{column_id} IS NULL")
372
+ if !null.nil? && null == false && !default.nil?
373
+ execute("UPDATE #{table_id} SET #{column_id}=#{quote(default)} WHERE #{column_id} IS NULL")
276
374
  end
277
375
  sql = "ALTER TABLE #{table_id} ALTER COLUMN #{column_id} #{type_to_sql column.type, limit: column.limit, precision: column.precision, scale: column.scale}"
278
- sql += " NOT NULL" if !allow_null.nil? && allow_null == false
279
- do_execute sql
376
+ sql += " NOT NULL" if !null.nil? && null == false
377
+
378
+ execute sql
280
379
  end
281
380
 
282
381
  def create_schema_dumper(options)
@@ -298,19 +397,37 @@ module ActiveRecord
298
397
  execute "DROP SCHEMA [#{schema_name}]"
299
398
  end
300
399
 
400
+ # Returns an array of schema names.
401
+ def schema_names
402
+ sql = <<~SQL.squish
403
+ SELECT name
404
+ FROM sys.schemas
405
+ WHERE
406
+ name NOT LIKE 'db_%' AND
407
+ name NOT IN ('INFORMATION_SCHEMA', 'sys', 'guest')
408
+ SQL
409
+
410
+ query_values(sql, "SCHEMA")
411
+ end
412
+
301
413
  private
302
414
 
303
415
  def data_source_sql(name = nil, type: nil)
304
- scope = quoted_scope name, type: type
416
+ scope = quoted_scope(name, type: type)
305
417
 
306
- table_name = lowercase_schema_reflection_sql 'TABLE_NAME'
307
- database = scope[:database].present? ? "#{scope[:database]}." : ""
418
+ table_schema = lowercase_schema_reflection_sql('TABLE_SCHEMA')
419
+ table_name = lowercase_schema_reflection_sql('TABLE_NAME')
420
+ database = scope[:database].present? ? "#{scope[:database]}." : ""
308
421
  table_catalog = scope[:database].present? ? quote(scope[:database]) : "DB_NAME()"
309
422
 
310
- sql = "SELECT #{table_name}"
423
+ sql = "SELECT "
424
+ sql += " CASE"
425
+ sql += " WHEN #{table_schema} = 'dbo' THEN #{table_name}"
426
+ sql += " ELSE CONCAT(#{table_schema}, '.', #{table_name})"
427
+ sql += " END"
311
428
  sql += " FROM #{database}INFORMATION_SCHEMA.TABLES WITH (NOLOCK)"
312
429
  sql += " WHERE TABLE_CATALOG = #{table_catalog}"
313
- sql += " AND TABLE_SCHEMA = #{quote(scope[:schema])}"
430
+ sql += " AND TABLE_SCHEMA = #{quote(scope[:schema])}" if scope[:schema]
314
431
  sql += " AND TABLE_NAME = #{quote(scope[:name])}" if scope[:name]
315
432
  sql += " AND TABLE_TYPE = #{quote(scope[:type])}" if scope[:type]
316
433
  sql += " ORDER BY #{table_name}"
@@ -319,9 +436,10 @@ module ActiveRecord
319
436
 
320
437
  def quoted_scope(name = nil, type: nil)
321
438
  identifier = SQLServer::Utils.extract_identifiers(name)
439
+
322
440
  {}.tap do |scope|
323
441
  scope[:database] = identifier.database if identifier.database
324
- scope[:schema] = identifier.schema || "dbo"
442
+ scope[:schema] = identifier.schema || "dbo" if name.present?
325
443
  scope[:name] = identifier.object if identifier.object
326
444
  scope[:type] = type if type
327
445
  end
@@ -346,7 +464,7 @@ module ActiveRecord
346
464
  datetime2: { name: "datetime2" },
347
465
  datetimeoffset: { name: "datetimeoffset" },
348
466
  smalldatetime: { name: "smalldatetime" },
349
- timestamp: { name: "datetime" },
467
+ timestamp: { name: "datetime2(6)" },
350
468
  time: { name: "time" },
351
469
  char: { name: "char" },
352
470
  varchar: { name: "varchar", limit: 8000 },
@@ -371,14 +489,25 @@ module ActiveRecord
371
489
  view_exists = view_exists?(table_name)
372
490
  view_tblnm = view_table_name(table_name) if view_exists
373
491
 
492
+ if view_exists
493
+ sql = <<~SQL
494
+ SELECT LOWER(c.COLUMN_NAME) AS [name], c.COLUMN_DEFAULT AS [default]
495
+ FROM #{database}.INFORMATION_SCHEMA.COLUMNS c
496
+ WHERE c.TABLE_NAME = #{quote(view_tblnm)}
497
+ SQL
498
+ results = internal_exec_query(sql, "SCHEMA")
499
+ default_functions = results.each.with_object({}) { |row, out| out[row["name"]] = row["default"] }.compact
500
+ end
501
+
374
502
  sql = column_definitions_sql(database, identifier)
375
503
 
376
504
  binds = []
377
505
  nv128 = SQLServer::Type::UnicodeVarchar.new limit: 128
378
506
  binds << Relation::QueryAttribute.new("TABLE_NAME", identifier.object, nv128)
379
507
  binds << Relation::QueryAttribute.new("TABLE_SCHEMA", identifier.schema, nv128) unless identifier.schema.blank?
380
- results = sp_executesql(sql, "SCHEMA", binds)
381
- results.map do |ci|
508
+ results = internal_exec_query(sql, "SCHEMA", binds)
509
+
510
+ columns = results.map do |ci|
382
511
  ci = ci.symbolize_keys
383
512
  ci[:_type] = ci[:type]
384
513
  ci[:table_name] = view_tblnm || table_name
@@ -402,13 +531,8 @@ module ActiveRecord
402
531
  ci[:default_function] = begin
403
532
  default = ci[:default_value]
404
533
  if default.nil? && view_exists
405
- default = select_value %{
406
- SELECT c.COLUMN_DEFAULT
407
- FROM #{database}.INFORMATION_SCHEMA.COLUMNS c
408
- WHERE
409
- c.TABLE_NAME = '#{view_tblnm}'
410
- AND c.COLUMN_NAME = '#{views_real_column_name(table_name, ci[:name])}'
411
- }.squish, "SCHEMA"
534
+ view_column = views_real_column_name(table_name, ci[:name]).downcase
535
+ default = default_functions[view_column] if view_column.present?
412
536
  end
413
537
  case default
414
538
  when nil
@@ -437,6 +561,13 @@ module ActiveRecord
437
561
  ci[:is_identity] = ci[:is_identity].to_i == 1 unless [TrueClass, FalseClass].include?(ci[:is_identity].class)
438
562
  ci
439
563
  end
564
+
565
+ # Since Rails 7, it's expected that all adapter raise error when table doesn't exists.
566
+ # I'm not aware of the possibility of tables without columns on SQL Server (postgres have those).
567
+ # Raise error if the method return an empty array
568
+ columns.tap do |result|
569
+ raise ActiveRecord::StatementInvalid, "Table '#{table_name}' doesn't exist" if result.empty?
570
+ end
440
571
  end
441
572
 
442
573
  def column_definitions_sql(database, identifier)
@@ -507,10 +638,17 @@ module ActiveRecord
507
638
  }.gsub(/[ \t\r\n]+/, " ").strip
508
639
  end
509
640
 
641
+ def remove_columns_for_alter(table_name, *column_names, **options)
642
+ first, *rest = column_names
643
+
644
+ # return an array like this [DROP COLUMN col_1, col_2, col_3]. Abstract adapter joins fragments with ", "
645
+ [remove_column_for_alter(table_name, first)] + rest.map { |column_name| quote_column_name(column_name) }
646
+ end
647
+
510
648
  def remove_check_constraints(table_name, column_name)
511
649
  constraints = select_values "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE where TABLE_NAME = '#{quote_string(table_name)}' and COLUMN_NAME = '#{quote_string(column_name)}'", "SCHEMA"
512
650
  constraints.each do |constraint|
513
- do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
651
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
514
652
  end
515
653
  end
516
654
 
@@ -519,7 +657,7 @@ module ActiveRecord
519
657
  execute_procedure(:sp_helpconstraint, table_name, "nomsg").flatten.select do |row|
520
658
  row["constraint_type"] == "DEFAULT on column #{column_name}"
521
659
  end.each do |row|
522
- do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{row['constraint_name']}"
660
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{row['constraint_name']}"
523
661
  end
524
662
  end
525
663
 
@@ -531,17 +669,31 @@ module ActiveRecord
531
669
 
532
670
  # === SQLServer Specific (Misc Helpers) ========================= #
533
671
 
672
+ # Parses just the table name from the SQL. Table name does not include database/schema/etc.
534
673
  def get_table_name(sql)
535
- tn = if sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)(\s+INTO)?\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
536
- Regexp.last_match[3] || Regexp.last_match[4]
537
- elsif sql =~ /FROM\s+([^\(\s]+)\s*/i
538
- Regexp.last_match[1]
539
- else
540
- nil
541
- end
674
+ tn = get_raw_table_name(sql)
542
675
  SQLServer::Utils.extract_identifiers(tn).object
543
676
  end
544
677
 
678
+ # Parses the raw table name that is used in the SQL. Table name could include database/schema/etc.
679
+ def get_raw_table_name(sql)
680
+ return if sql.blank?
681
+
682
+ s = sql.gsub(/^\s*EXEC sp_executesql N'/i, "")
683
+
684
+ if s.match?(/^\s*INSERT INTO.*/i)
685
+ s.split(/INSERT INTO/i)[1]
686
+ .split(/OUTPUT INSERTED/i)[0]
687
+ .split(/(DEFAULT)?\s+VALUES/i)[0]
688
+ .split(/\bSELECT\b(?![^\[]*\])/i)[0]
689
+ .match(/\s*([^(]*)/i)[0]
690
+ elsif s.match?(/^\s*UPDATE\s+.*/i)
691
+ s.match(/UPDATE\s+([^\(\s]+)\s*/i)[1]
692
+ else
693
+ s.match(/FROM[\s|\(]+((\[[^\(\]]+\])|[^\(\s]+)\s*/i)[1]
694
+ end.strip
695
+ end
696
+
545
697
  def default_constraint_name(table_name, column_name)
546
698
  "DF_#{table_name}_#{column_name}"
547
699
  end
@@ -561,18 +713,21 @@ module ActiveRecord
561
713
  @view_information ||= {}
562
714
  @view_information[table_name] ||= begin
563
715
  identifier = SQLServer::Utils.extract_identifiers(table_name)
564
- view_info = select_one "SELECT * FROM INFORMATION_SCHEMA.VIEWS WITH (NOLOCK) WHERE TABLE_NAME = #{quote(identifier.object)}", "SCHEMA"
716
+ information_query_table = identifier.database.present? ? "[#{identifier.database}].[INFORMATION_SCHEMA].[VIEWS]" : "[INFORMATION_SCHEMA].[VIEWS]"
717
+ view_info = select_one "SELECT * FROM #{information_query_table} WITH (NOLOCK) WHERE TABLE_NAME = #{quote(identifier.object)}", "SCHEMA"
718
+
565
719
  if view_info
566
720
  view_info = view_info.with_indifferent_access
567
721
  if view_info[:VIEW_DEFINITION].blank? || view_info[:VIEW_DEFINITION].length == 4000
568
722
  view_info[:VIEW_DEFINITION] = begin
569
- select_values("EXEC sp_helptext #{identifier.object_quoted}", "SCHEMA").join
723
+ select_values("EXEC sp_helptext #{identifier.object_quoted}", "SCHEMA").join
570
724
  rescue
571
725
  warn "No view definition found, possible permissions problem.\nPlease run GRANT VIEW DEFINITION TO your_user;"
572
726
  nil
573
- end
727
+ end
574
728
  end
575
729
  end
730
+
576
731
  view_info
577
732
  end
578
733
  end
@@ -581,7 +736,8 @@ module ActiveRecord
581
736
  view_definition = view_information(table_name)[:VIEW_DEFINITION]
582
737
  return column_name unless view_definition
583
738
 
584
- match_data = view_definition.match(/([\w-]*)\s+as\s+#{column_name}/im)
739
+ # Remove "CREATE VIEW ... AS SELECT ..." and then match the column name.
740
+ match_data = view_definition.sub(/CREATE\s+VIEW.*AS\s+SELECT\s/, '').match(/([\w-]*)\s+AS\s+#{column_name}\W/im)
585
741
  match_data ? match_data[1] : column_name
586
742
  end
587
743
 
@@ -12,9 +12,9 @@ module ActiveRecord
12
12
  OPTION_XML = "SHOWPLAN_XML"
13
13
  OPTIONS = [OPTION_ALL, OPTION_TEXT, OPTION_XML]
14
14
 
15
- def explain(arel, binds = [])
15
+ def explain(arel, binds = [], options = [])
16
16
  sql = to_sql(arel)
17
- result = with_showplan_on { sp_executesql(sql, "EXPLAIN", binds) }
17
+ result = with_showplan_on { internal_exec_query(sql, "EXPLAIN", binds) }
18
18
  printer = showplan_printer.new(result)
19
19
  printer.pp
20
20
  end
@@ -30,7 +30,7 @@ module ActiveRecord
30
30
 
31
31
  def set_showplan_option(enable = true)
32
32
  sql = "SET #{showplan_option} #{enable ? 'ON' : 'OFF'}"
33
- raw_connection_do(sql)
33
+ raw_execute(sql, "SCHEMA")
34
34
  rescue Exception
35
35
  raise ActiveRecordError, "#{showplan_option} could not be turned #{enable ? 'ON' : 'OFF'}, perhaps you do not have SHOWPLAN permissions?"
36
36
  end
@@ -101,13 +101,24 @@ module ActiveRecord
101
101
 
102
102
  def new_column_definition(name, type, **options)
103
103
  case type
104
- when :datetime
105
- type = :datetime2 if options[:precision]
104
+ when :datetime, :timestamp
105
+ # If no precision then default it to 6.
106
+ options[:precision] = 6 unless options.key?(:precision)
107
+
108
+ # If there is precision then column must be of type 'datetime2'.
109
+ type = :datetime2 unless options[:precision].nil?
106
110
  when :primary_key
107
111
  options[:is_identity] = true
108
112
  end
113
+
109
114
  super
110
115
  end
116
+
117
+ private
118
+
119
+ def valid_column_definition_options
120
+ super + [:is_identity]
121
+ end
111
122
  end
112
123
 
113
124
  class Table < ActiveRecord::ConnectionAdapters::Table