activerecord-sqlserver-adapter 8.0.10 → 8.1.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +1 -1
  3. data/.github/workflows/ci.yml +34 -3
  4. data/CHANGELOG.md +14 -68
  5. data/Dockerfile.ci +1 -1
  6. data/Gemfile +7 -9
  7. data/Guardfile +2 -2
  8. data/README.md +33 -13
  9. data/Rakefile +1 -1
  10. data/VERSION +1 -1
  11. data/activerecord-sqlserver-adapter.gemspec +15 -16
  12. data/compose.ci.yaml +8 -1
  13. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +1 -1
  14. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +1 -2
  15. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +1 -1
  16. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +4 -4
  17. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +118 -83
  18. data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +3 -4
  19. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +7 -7
  20. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +24 -12
  21. data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +17 -8
  22. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +162 -156
  23. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_table.rb +2 -2
  24. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +5 -5
  25. data/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb +2 -7
  26. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +3 -1
  27. data/lib/active_record/connection_adapters/sqlserver/type/data.rb +3 -3
  28. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +3 -3
  29. data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +3 -4
  30. data/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb +1 -1
  31. data/lib/active_record/connection_adapters/sqlserver/type/time.rb +4 -6
  32. data/lib/active_record/connection_adapters/sqlserver/type/time_value_fractional.rb +1 -1
  33. data/lib/active_record/connection_adapters/sqlserver/type/uuid.rb +0 -2
  34. data/lib/active_record/connection_adapters/sqlserver/utils.rb +10 -12
  35. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +118 -66
  36. data/lib/active_record/connection_adapters/sqlserver_column.rb +17 -9
  37. data/lib/active_record/tasks/sqlserver_database_tasks.rb +5 -5
  38. data/lib/arel/visitors/sqlserver.rb +55 -26
  39. data/test/cases/active_schema_test_sqlserver.rb +45 -23
  40. data/test/cases/adapter_test_sqlserver.rb +72 -59
  41. data/test/cases/coerced_tests.rb +365 -161
  42. data/test/cases/column_test_sqlserver.rb +328 -316
  43. data/test/cases/connection_test_sqlserver.rb +15 -11
  44. data/test/cases/enum_test_sqlserver.rb +8 -9
  45. data/test/cases/execute_procedure_test_sqlserver.rb +1 -1
  46. data/test/cases/fetch_test_sqlserver.rb +1 -1
  47. data/test/cases/helper_sqlserver.rb +7 -3
  48. data/test/cases/index_test_sqlserver.rb +8 -6
  49. data/test/cases/insert_all_test_sqlserver.rb +3 -28
  50. data/test/cases/json_test_sqlserver.rb +8 -8
  51. data/test/cases/lateral_test_sqlserver.rb +2 -2
  52. data/test/cases/migration_test_sqlserver.rb +12 -12
  53. data/test/cases/pessimistic_locking_test_sqlserver.rb +6 -6
  54. data/test/cases/primary_keys_test_sqlserver.rb +4 -4
  55. data/test/cases/rake_test_sqlserver.rb +15 -7
  56. data/test/cases/schema_dumper_test_sqlserver.rb +109 -113
  57. data/test/cases/schema_test_sqlserver.rb +7 -7
  58. data/test/cases/transaction_test_sqlserver.rb +6 -8
  59. data/test/cases/trigger_test_sqlserver.rb +1 -1
  60. data/test/cases/utils_test_sqlserver.rb +3 -3
  61. data/test/cases/view_test_sqlserver.rb +12 -8
  62. data/test/cases/virtual_column_test_sqlserver.rb +113 -0
  63. data/test/migrations/create_clients_and_change_column_collation.rb +2 -2
  64. data/test/models/sqlserver/edge_schema.rb +2 -2
  65. data/test/schema/sqlserver_specific_schema.rb +49 -37
  66. data/test/support/coerceable_test_sqlserver.rb +10 -10
  67. data/test/support/connection_reflection.rb +0 -5
  68. data/test/support/core_ext/backtrace_cleaner.rb +36 -0
  69. data/test/support/query_assertions.rb +6 -6
  70. data/test/support/rake_helpers.rb +6 -10
  71. metadata +12 -107
@@ -19,47 +19,57 @@ module ActiveRecord
19
19
  sql = sp_executesql_sql(sql, types, params, notification_payload[:name])
20
20
  end
21
21
 
22
- result = if id_insert_table_name = query_requires_identity_insert?(sql)
23
- with_identity_insert_enabled(id_insert_table_name, raw_connection) do
24
- internal_exec_sql_query(sql, raw_connection)
25
- end
26
- else
27
- internal_exec_sql_query(sql, raw_connection)
28
- end
22
+ id_insert_table_name = query_requires_identity_insert?(sql)
23
+
24
+ result, affected_rows = if id_insert_table_name
25
+ with_identity_insert_enabled(id_insert_table_name, raw_connection) do
26
+ internal_exec_sql_query(sql, raw_connection)
27
+ end
28
+ else
29
+ internal_exec_sql_query(sql, raw_connection)
30
+ end
29
31
 
30
32
  verified!
33
+ notification_payload[:affected_rows] = affected_rows
31
34
  notification_payload[:row_count] = result.count
32
35
  result
33
36
  end
34
37
 
35
- def cast_result(raw_result)
36
- if raw_result.columns.empty?
37
- ActiveRecord::Result.empty
38
- else
39
- ActiveRecord::Result.new(raw_result.columns, raw_result.rows)
40
- end
38
+ # Method `perform_query` already returns an `ActiveRecord::Result` so we have nothing to cast here. This is
39
+ # different to the MySQL/PostgreSQL adapters where the raw result is converted to `ActiveRecord::Result` in
40
+ # `cast_result`.
41
+ def cast_result(result)
42
+ result
41
43
  end
42
44
 
45
+ # Returns the affected rows from results.
43
46
  def affected_rows(raw_result)
44
- column_name = lowercase_schema_reflection ? 'affectedrows' : 'AffectedRows'
45
- raw_result.first[column_name]
47
+ column_name = lowercase_schema_reflection ? "affectedrows" : "AffectedRows"
48
+ raw_result&.first&.fetch(column_name, nil)
49
+ end
50
+
51
+ # Returns the affected rows from results or handle.
52
+ def affected_rows_from_results_or_handle(raw_result, handle)
53
+ affected_rows(raw_result) || handle.affected_rows
46
54
  end
47
55
 
48
56
  def internal_exec_sql_query(sql, conn)
49
57
  handle = internal_raw_execute(sql, conn)
50
- handle_to_names_and_values(handle, ar_result: true)
58
+ results = handle_to_names_and_values(handle, ar_result: true)
59
+
60
+ [results, affected_rows_from_results_or_handle(results, handle)]
51
61
  ensure
52
62
  finish_statement_handle(handle)
53
63
  end
54
64
 
55
65
  def exec_delete(sql, name = nil, binds = [])
56
66
  sql = sql.dup << "; SELECT @@ROWCOUNT AS AffectedRows"
57
- super(sql, name, binds)
67
+ super
58
68
  end
59
69
 
60
70
  def exec_update(sql, name = nil, binds = [])
61
71
  sql = sql.dup << "; SELECT @@ROWCOUNT AS AffectedRows"
62
- super(sql, name, binds)
72
+ super
63
73
  end
64
74
 
65
75
  def begin_db_transaction
@@ -150,7 +160,6 @@ module ActiveRecord
150
160
  end
151
161
  end
152
162
 
153
-
154
163
  def build_sql_for_merge_insert(insert:, insert_all:, columns_with_uniqueness_constraints:) # :nodoc:
155
164
  insert_all.inserts.reverse! if insert.update_duplicates?
156
165
 
@@ -160,7 +169,7 @@ module ActiveRecord
160
169
  SELECT *
161
170
  FROM (
162
171
  SELECT #{insert.send(:columns_list)}, #{partition_by_columns_with_uniqueness_constraints(columns_with_uniqueness_constraints:)}
163
- FROM (#{insert.values_list})
172
+ FROM (#{merge_insert_values_list(insert:, insert_all:)})
164
173
  AS t1 (#{insert.send(:columns_list)})
165
174
  ) AS ranked_source
166
175
  WHERE #{is_first_record_across_all_uniqueness_constraints(columns_with_uniqueness_constraints:)}
@@ -189,21 +198,50 @@ module ActiveRecord
189
198
  sql
190
199
  end
191
200
 
201
+ # For `nil` identity columns we need to ensure that the values do not match so that they are all inserted.
202
+ # Method is a combination of `ActiveRecord::InsertAll#values_list` and `ActiveRecord::ConnectionAdapters::SQLServer::DatabaseStatements#default_insert_value`.
203
+ def merge_insert_values_list(insert:, insert_all:)
204
+ connection = insert.send(:connection)
205
+ identity_index = 0
206
+
207
+ types = insert.send(:extract_types_for, insert.keys_including_timestamps)
208
+
209
+ values_list = insert_all.map_key_with_value do |key, value|
210
+ if Arel::Nodes::SqlLiteral === value
211
+ value
212
+ elsif insert.primary_keys.include?(key) && value.nil?
213
+ column = insert.model.columns_hash[key]
214
+
215
+ if column.is_identity?
216
+ identity_index += 1
217
+ table_name = quote(quote_table_name(column.table_name))
218
+ Arel.sql("IDENT_CURRENT(#{table_name}) + (IDENT_INCR(#{table_name}) * #{identity_index})")
219
+ else
220
+ connection.default_insert_value(column)
221
+ end
222
+ else
223
+ ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value))
224
+ end
225
+ end
226
+
227
+ connection.visitor.compile(Arel::Nodes::ValuesList.new(values_list))
228
+ end
229
+
192
230
  # === SQLServer Specific ======================================== #
193
231
 
194
232
  def execute_procedure(proc_name, *variables)
195
233
  vars = if variables.any? && variables.first.is_a?(Hash)
196
- variables.first.map { |k, v| "@#{k} = #{quote(v)}" }
197
- else
198
- variables.map { |v| quote(v) }
199
- end.join(", ")
234
+ variables.first.map { |k, v| "@#{k} = #{quote(v)}" }
235
+ else
236
+ variables.map { |v| quote(v) }
237
+ end.join(", ")
200
238
  sql = "EXEC #{proc_name} #{vars}".strip
201
239
 
202
240
  log(sql, "Execute Procedure") do |notification_payload|
203
241
  with_raw_connection do |conn|
204
242
  result = internal_raw_execute(sql, conn)
205
243
  verified!
206
- options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc }
244
+ options = {as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc}
207
245
 
208
246
  result.each(options) do |row|
209
247
  r = row.with_indifferent_access
@@ -240,7 +278,7 @@ module ActiveRecord
240
278
 
241
279
  rows = select_rows("DBCC USEROPTIONS WITH NO_INFOMSGS", "SCHEMA")
242
280
  rows = rows.first if rows.size == 2 && rows.last.empty?
243
- rows.reduce(HashWithIndifferentAccess.new) do |values, row|
281
+ rows.each_with_object(HashWithIndifferentAccess.new) do |row, values|
244
282
  if row.instance_of? Hash
245
283
  set_option = row.values[0].gsub(/\s+/, "_")
246
284
  user_value = row.values[1]
@@ -249,7 +287,6 @@ module ActiveRecord
249
287
  user_value = row[1]
250
288
  end
251
289
  values[set_option] = user_value
252
- values
253
290
  end
254
291
  end
255
292
 
@@ -303,35 +340,35 @@ module ActiveRecord
303
340
  end
304
341
 
305
342
  sql = if pk && use_output_inserted? && !database_prefix_remote_server?
306
- table_name ||= get_table_name(sql)
307
- exclude_output_inserted = exclude_output_inserted_table_name?(table_name, sql)
308
-
309
- if exclude_output_inserted
310
- pk_and_types = Array(pk).map do |subkey|
311
- {
312
- quoted: SQLServer::Utils.extract_identifiers(subkey).quoted,
313
- id_sql_type: exclude_output_inserted_id_sql_type(subkey, exclude_output_inserted)
314
- }
315
- end
316
-
317
- <<~SQL.squish
318
- DECLARE @ssaIdInsertTable table (#{pk_and_types.map { |pk_and_type| "#{pk_and_type[:quoted]} #{pk_and_type[:id_sql_type]}"}.join(", ") });
319
- #{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT #{ pk_and_types.map { |pk_and_type| "INSERTED.#{pk_and_type[:quoted]}" }.join(", ") } INTO @ssaIdInsertTable"}
320
- SELECT #{pk_and_types.map {|pk_and_type| "CAST(#{pk_and_type[:quoted]} AS #{pk_and_type[:id_sql_type]}) #{pk_and_type[:quoted]}"}.join(", ")} FROM @ssaIdInsertTable
321
- SQL
322
- else
323
- returning_columns = returning || Array(pk)
324
-
325
- if returning_columns.any?
326
- returning_columns_statements = returning_columns.map { |c| " INSERTED.#{SQLServer::Utils.extract_identifiers(c).quoted}" }
327
- sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT" + returning_columns_statements.join(",")
328
- else
329
- sql
330
- end
331
- end
332
- else
333
- "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
334
- end
343
+ table_name ||= get_table_name(sql)
344
+ exclude_output_inserted = exclude_output_inserted_table_name?(table_name, sql)
345
+
346
+ if exclude_output_inserted
347
+ pk_and_types = Array(pk).map do |subkey|
348
+ {
349
+ quoted: SQLServer::Utils.extract_identifiers(subkey).quoted,
350
+ id_sql_type: exclude_output_inserted_id_sql_type(subkey, exclude_output_inserted)
351
+ }
352
+ end
353
+
354
+ <<~SQL.squish
355
+ DECLARE @ssaIdInsertTable table (#{pk_and_types.map { |pk_and_type| "#{pk_and_type[:quoted]} #{pk_and_type[:id_sql_type]}" }.join(", ")});
356
+ #{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT #{pk_and_types.map { |pk_and_type| "INSERTED.#{pk_and_type[:quoted]}" }.join(", ")} INTO @ssaIdInsertTable"}
357
+ SELECT #{pk_and_types.map { |pk_and_type| "CAST(#{pk_and_type[:quoted]} AS #{pk_and_type[:id_sql_type]}) #{pk_and_type[:quoted]}" }.join(", ")} FROM @ssaIdInsertTable
358
+ SQL
359
+ else
360
+ returning_columns = returning || Array(pk)
361
+
362
+ if returning_columns.any?
363
+ returning_columns_statements = returning_columns.map { |c| " INSERTED.#{SQLServer::Utils.extract_identifiers(c).quoted}" }
364
+ sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT" + returning_columns_statements.join(",")
365
+ else
366
+ sql
367
+ end
368
+ end
369
+ else
370
+ "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
371
+ end
335
372
 
336
373
  [sql, binds]
337
374
  end
@@ -339,9 +376,9 @@ module ActiveRecord
339
376
  # === SQLServer Specific ======================================== #
340
377
 
341
378
  def set_identity_insert(table_name, conn, enable)
342
- internal_raw_execute("SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}", conn , perform_do: true)
343
- rescue Exception
344
- raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
379
+ internal_raw_execute("SET IDENTITY_INSERT #{table_name} #{enable ? "ON" : "OFF"}", conn, perform_do: true)
380
+ rescue
381
+ raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? "ON" : "OFF"} for table #{table_name}"
345
382
  end
346
383
 
347
384
  # === SQLServer Specific (Executing) ============================ #
@@ -359,7 +396,7 @@ module ActiveRecord
359
396
 
360
397
  def sp_executesql_sql_type(attr)
361
398
  if attr.respond_to?(:type)
362
- type = attr.type.is_a?(ActiveRecord::Normalization::NormalizedValueType) ? attr.type.cast_type : attr.type
399
+ type = attr.type.is_a?(ActiveModel::Attributes::Normalization::NormalizedValueType) ? attr.type.cast_type : attr.type
363
400
  type = type.subtype if type.serialized?
364
401
 
365
402
  return type.sqlserver_type if type.respond_to?(:sqlserver_type)
@@ -372,9 +409,9 @@ module ActiveRecord
372
409
  value = active_model_attribute?(attr) ? attr.value_for_database : attr
373
410
 
374
411
  if value.is_a?(Numeric)
375
- value > 2_147_483_647 ? "bigint".freeze : "int".freeze
412
+ (value > 2_147_483_647) ? "bigint" : "int"
376
413
  else
377
- "nvarchar(max)".freeze
414
+ "nvarchar(max)"
378
415
  end
379
416
  end
380
417
 
@@ -474,26 +511,24 @@ module ActiveRecord
474
511
  end
475
512
  results = handle.each(query_options)
476
513
 
477
- columns = handle.fields
478
- # If query returns multiple result sets, only return the columns of the last one.
479
- columns = columns.last if columns.any? && columns.all? { |e| e.is_a?(Array) }
480
- columns = columns.map(&:downcase) if lowercase_schema_reflection
514
+ if options[:ar_result]
515
+ columns = handle.fields
516
+ columns = columns.last if columns.any? && columns.all? { |e| e.is_a?(Array) } # If query returns multiple result sets, only return the columns of the last one.
517
+ columns = columns.map(&:downcase) if lowercase_schema_reflection
481
518
 
482
- options[:ar_result] ? ActiveRecord::Result.new(columns, results) : results
519
+ ActiveRecord::Result.new(columns, results, affected_rows: handle.affected_rows)
520
+ else
521
+ results
522
+ end
483
523
  end
484
524
 
485
525
  def finish_statement_handle(handle)
486
- handle.cancel if handle
526
+ handle&.cancel
487
527
  handle
488
528
  end
489
529
 
490
- # TinyTDS returns false instead of raising an exception if connection fails.
491
- # Getting around this by raising an exception ourselves while PR
492
- # https://github.com/rails-sqlserver/tiny_tds/pull/469 is not released.
493
530
  def internal_raw_execute(sql, raw_connection, perform_do: false)
494
531
  result = raw_connection.execute(sql)
495
- raise TinyTds::Error, "failed to execute statement" if result.is_a?(FalseClass)
496
-
497
532
  perform_do ? result.do : result
498
533
  end
499
534
 
@@ -502,16 +537,16 @@ module ActiveRecord
502
537
  return "" unless insert_all.returning
503
538
 
504
539
  returning_values_sql = if insert_all.returning.is_a?(String)
505
- insert_all.returning
506
- else
507
- Array(insert_all.returning).map do |attribute|
508
- if insert.model.attribute_alias?(attribute)
509
- "INSERTED.#{quote_column_name(insert.model.attribute_alias(attribute))} AS #{quote_column_name(attribute)}"
510
- else
511
- "INSERTED.#{quote_column_name(attribute)}"
512
- end
513
- end.join(",")
514
- end
540
+ insert_all.returning
541
+ else
542
+ Array(insert_all.returning).map do |attribute|
543
+ if insert.model.attribute_alias?(attribute)
544
+ "INSERTED.#{quote_column_name(insert.model.attribute_alias(attribute))} AS #{quote_column_name(attribute)}"
545
+ else
546
+ "INSERTED.#{quote_column_name(attribute)}"
547
+ end
548
+ end.join(",")
549
+ end
515
550
 
516
551
  " OUTPUT #{returning_values_sql}"
517
552
  end
@@ -32,20 +32,19 @@ module ActiveRecord
32
32
  private
33
33
 
34
34
  def create_database_options(options = {})
35
- keys = [:collate]
35
+ keys = [:collate]
36
36
  copts = @connection_parameters
37
- options = {
37
+ {
38
38
  collate: copts[:collation]
39
39
  }.merge(options.symbolize_keys).select { |_, v|
40
40
  v.present?
41
41
  }.slice(*keys).map { |k, v|
42
42
  "#{k.to_s.upcase} #{v}"
43
43
  }.join(" ")
44
- options
45
44
  end
46
45
 
47
46
  def create_database_edition_options(options = {})
48
- keys = [:maxsize, :edition, :service_objective]
47
+ keys = [:maxsize, :edition, :service_objective]
49
48
  copts = @connection_parameters
50
49
  edition_options = {
51
50
  maxsize: copts[:azure_maxsize],
@@ -55,11 +55,11 @@ module ActiveRecord
55
55
  cast_type = lookup_cast_type(sql_type)
56
56
 
57
57
  simple_type = SqlTypeMetadata.new(
58
- sql_type: sql_type,
59
- type: cast_type.type,
60
- limit: cast_type.limit,
58
+ sql_type: sql_type,
59
+ type: cast_type.type,
60
+ limit: cast_type.limit,
61
61
  precision: cast_type.precision,
62
- scale: cast_type.scale
62
+ scale: cast_type.scale
63
63
  )
64
64
 
65
65
  SQLServer::TypeMetadata.new(simple_type, **sqlserver_options)
@@ -79,7 +79,7 @@ module ActiveRecord
79
79
 
80
80
  def quote_default_expression(value, column)
81
81
  cast_type = lookup_cast_type(column.sql_type)
82
- if cast_type.type == :uuid && value.is_a?(String) && value.include?('()')
82
+ if cast_type.type == :uuid && value.is_a?(String) && value.include?("()")
83
83
  value
84
84
  else
85
85
  super
@@ -87,7 +87,7 @@ module ActiveRecord
87
87
  end
88
88
 
89
89
  def quoted_true
90
- '1'
90
+ "1"
91
91
  end
92
92
 
93
93
  def unquoted_true
@@ -95,7 +95,7 @@ module ActiveRecord
95
95
  end
96
96
 
97
97
  def quoted_false
98
- '0'
98
+ "0"
99
99
  end
100
100
 
101
101
  def unquoted_false
@@ -6,10 +6,18 @@ module ActiveRecord
6
6
  class SchemaCreation < SchemaCreation
7
7
  private
8
8
 
9
+ delegate :quoted_include_columns_for_index, to: :@conn
10
+
9
11
  def supports_index_using?
10
12
  false
11
13
  end
12
14
 
15
+ def visit_ColumnDefinition(o)
16
+ column_sql = super
17
+ column_sql = column_sql.sub(" #{o.sql_type}", "") if o.options[:as].present?
18
+ column_sql
19
+ end
20
+
13
21
  def visit_TableDefinition(o)
14
22
  if_not_exists = o.if_not_exists
15
23
 
@@ -44,25 +52,29 @@ module ActiveRecord
44
52
  sql << "INDEX"
45
53
  sql << "#{quote_column_name(index.name)} ON #{quote_table_name(index.table)}"
46
54
  sql << "(#{quoted_columns(index)})"
55
+ sql << "INCLUDE (#{quoted_include_columns(index.include)})" if supports_index_include? && index.include
47
56
  sql << "WHERE #{index.where}" if index.where
48
57
 
49
58
  sql.join(" ")
50
59
  end
51
60
 
61
+ def quoted_include_columns(o)
62
+ (String === o) ? o : quoted_include_columns_for_index(o)
63
+ end
64
+
52
65
  def add_column_options!(sql, options)
53
- sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
54
- if options[:collation].present?
55
- sql << " COLLATE #{options[:collation]}"
56
- end
57
- if options[:null] == false
58
- sql << " NOT NULL"
59
- end
60
- if options[:is_identity] == true
61
- sql << " IDENTITY(1,1)"
62
- end
63
- if options[:primary_key] == true
64
- sql << " PRIMARY KEY"
66
+ sql << " DEFAULT #{quote_default_expression_for_column_definition(options[:default], options[:column])}" if options_include_default?(options)
67
+
68
+ sql << " COLLATE #{options[:collation]}" if options[:collation].present?
69
+ sql << " NOT NULL" if options[:null] == false
70
+ sql << " IDENTITY(1,1)" if options[:is_identity] == true
71
+ sql << " PRIMARY KEY" if options[:primary_key] == true
72
+
73
+ if (as = options[:as])
74
+ sql << " AS #{as}"
75
+ sql << " PERSISTED" if options[:stored]
65
76
  end
77
+
66
78
  sql
67
79
  end
68
80
 
@@ -4,22 +4,31 @@ module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module SQLServer
6
6
  class SchemaDumper < ConnectionAdapters::SchemaDumper
7
- SQLSEVER_NO_LIMIT_TYPES = [
8
- "text",
9
- "ntext",
10
- "varchar(max)",
11
- "nvarchar(max)",
12
- "varbinary(max)"
13
- ].freeze
7
+ SQLSERVER_NO_LIMIT_TYPES = %w[text ntext varchar(max) nvarchar(max) varbinary(max)].freeze
14
8
 
15
9
  private
16
10
 
11
+ def prepare_column_options(column)
12
+ spec = super
13
+
14
+ if @connection.supports_virtual_columns? && column.virtual?
15
+ spec[:as] = extract_expression_for_virtual_column(column)
16
+ spec[:stored] = column.virtual_stored?
17
+ end
18
+
19
+ spec
20
+ end
21
+
22
+ def extract_expression_for_virtual_column(column)
23
+ column.default_function.inspect
24
+ end
25
+
17
26
  def explicit_primary_key_default?(column)
18
27
  column.type == :integer && !column.is_identity?
19
28
  end
20
29
 
21
30
  def schema_limit(column)
22
- return if SQLSEVER_NO_LIMIT_TYPES.include?(column.sql_type)
31
+ return if SQLSERVER_NO_LIMIT_TYPES.include?(column.sql_type)
23
32
 
24
33
  super
25
34
  end