activerecord-sqlserver-adapter 8.0.9 → 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 (74) 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 -62
  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 +121 -90
  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 +396 -343
  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/optimizer_hints_test_sqlserver.rb +1 -1
  54. data/test/cases/pessimistic_locking_test_sqlserver.rb +6 -6
  55. data/test/cases/primary_keys_test_sqlserver.rb +4 -4
  56. data/test/cases/rake_test_sqlserver.rb +15 -7
  57. data/test/cases/schema_dumper_test_sqlserver.rb +109 -113
  58. data/test/cases/schema_test_sqlserver.rb +7 -7
  59. data/test/cases/showplan_test_sqlserver.rb +2 -2
  60. data/test/cases/specific_schema_test_sqlserver.rb +6 -6
  61. data/test/cases/transaction_test_sqlserver.rb +6 -8
  62. data/test/cases/trigger_test_sqlserver.rb +1 -1
  63. data/test/cases/utils_test_sqlserver.rb +3 -3
  64. data/test/cases/view_test_sqlserver.rb +12 -8
  65. data/test/cases/virtual_column_test_sqlserver.rb +113 -0
  66. data/test/migrations/create_clients_and_change_column_collation.rb +2 -2
  67. data/test/models/sqlserver/edge_schema.rb +2 -2
  68. data/test/schema/sqlserver_specific_schema.rb +49 -37
  69. data/test/support/coerceable_test_sqlserver.rb +10 -10
  70. data/test/support/connection_reflection.rb +0 -5
  71. data/test/support/core_ext/backtrace_cleaner.rb +36 -0
  72. data/test/support/query_assertions.rb +25 -3
  73. data/test/support/rake_helpers.rb +6 -10
  74. metadata +12 -107
@@ -14,56 +14,62 @@ module ActiveRecord
14
14
  end
15
15
 
16
16
  def perform_query(raw_connection, sql, binds, type_casted_binds, prepare:, notification_payload:, batch:)
17
- result = if id_insert_table_name = query_requires_identity_insert?(sql)
18
- with_identity_insert_enabled(id_insert_table_name, raw_connection) do
19
- internal_exec_sql_query(sql, raw_connection)
20
- end
21
- else
22
- internal_exec_sql_query(sql, raw_connection)
23
- end
17
+ unless binds.nil? || binds.empty?
18
+ types, params = sp_executesql_types_and_parameters(binds)
19
+ sql = sp_executesql_sql(sql, types, params, notification_payload[:name])
20
+ end
21
+
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
24
31
 
25
32
  verified!
33
+ notification_payload[:affected_rows] = affected_rows
26
34
  notification_payload[:row_count] = result.count
27
35
  result
28
36
  end
29
37
 
30
- def cast_result(raw_result)
31
- if raw_result.columns.empty?
32
- ActiveRecord::Result.empty
33
- else
34
- ActiveRecord::Result.new(raw_result.columns, raw_result.rows)
35
- 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
36
43
  end
37
44
 
45
+ # Returns the affected rows from results.
38
46
  def affected_rows(raw_result)
39
- column_name = lowercase_schema_reflection ? 'affectedrows' : 'AffectedRows'
40
- raw_result.first[column_name]
47
+ column_name = lowercase_schema_reflection ? "affectedrows" : "AffectedRows"
48
+ raw_result&.first&.fetch(column_name, nil)
41
49
  end
42
50
 
43
- def raw_execute(sql, name = nil, binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true, batch: false)
44
- unless binds.nil? || binds.empty?
45
- types, params = sp_executesql_types_and_parameters(binds)
46
- sql = sp_executesql_sql(sql, types, params, name)
47
- end
48
-
49
- super
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
50
54
  end
51
55
 
52
56
  def internal_exec_sql_query(sql, conn)
53
57
  handle = internal_raw_execute(sql, conn)
54
- 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)]
55
61
  ensure
56
62
  finish_statement_handle(handle)
57
63
  end
58
64
 
59
65
  def exec_delete(sql, name = nil, binds = [])
60
66
  sql = sql.dup << "; SELECT @@ROWCOUNT AS AffectedRows"
61
- super(sql, name, binds)
67
+ super
62
68
  end
63
69
 
64
70
  def exec_update(sql, name = nil, binds = [])
65
71
  sql = sql.dup << "; SELECT @@ROWCOUNT AS AffectedRows"
66
- super(sql, name, binds)
72
+ super
67
73
  end
68
74
 
69
75
  def begin_db_transaction
@@ -154,7 +160,6 @@ module ActiveRecord
154
160
  end
155
161
  end
156
162
 
157
-
158
163
  def build_sql_for_merge_insert(insert:, insert_all:, columns_with_uniqueness_constraints:) # :nodoc:
159
164
  insert_all.inserts.reverse! if insert.update_duplicates?
160
165
 
@@ -164,7 +169,7 @@ module ActiveRecord
164
169
  SELECT *
165
170
  FROM (
166
171
  SELECT #{insert.send(:columns_list)}, #{partition_by_columns_with_uniqueness_constraints(columns_with_uniqueness_constraints:)}
167
- FROM (#{insert.values_list})
172
+ FROM (#{merge_insert_values_list(insert:, insert_all:)})
168
173
  AS t1 (#{insert.send(:columns_list)})
169
174
  ) AS ranked_source
170
175
  WHERE #{is_first_record_across_all_uniqueness_constraints(columns_with_uniqueness_constraints:)}
@@ -193,21 +198,50 @@ module ActiveRecord
193
198
  sql
194
199
  end
195
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
+
196
230
  # === SQLServer Specific ======================================== #
197
231
 
198
232
  def execute_procedure(proc_name, *variables)
199
233
  vars = if variables.any? && variables.first.is_a?(Hash)
200
- variables.first.map { |k, v| "@#{k} = #{quote(v)}" }
201
- else
202
- variables.map { |v| quote(v) }
203
- end.join(", ")
234
+ variables.first.map { |k, v| "@#{k} = #{quote(v)}" }
235
+ else
236
+ variables.map { |v| quote(v) }
237
+ end.join(", ")
204
238
  sql = "EXEC #{proc_name} #{vars}".strip
205
239
 
206
240
  log(sql, "Execute Procedure") do |notification_payload|
207
241
  with_raw_connection do |conn|
208
242
  result = internal_raw_execute(sql, conn)
209
243
  verified!
210
- options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc }
244
+ options = {as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc}
211
245
 
212
246
  result.each(options) do |row|
213
247
  r = row.with_indifferent_access
@@ -244,7 +278,7 @@ module ActiveRecord
244
278
 
245
279
  rows = select_rows("DBCC USEROPTIONS WITH NO_INFOMSGS", "SCHEMA")
246
280
  rows = rows.first if rows.size == 2 && rows.last.empty?
247
- rows.reduce(HashWithIndifferentAccess.new) do |values, row|
281
+ rows.each_with_object(HashWithIndifferentAccess.new) do |row, values|
248
282
  if row.instance_of? Hash
249
283
  set_option = row.values[0].gsub(/\s+/, "_")
250
284
  user_value = row.values[1]
@@ -253,7 +287,6 @@ module ActiveRecord
253
287
  user_value = row[1]
254
288
  end
255
289
  values[set_option] = user_value
256
- values
257
290
  end
258
291
  end
259
292
 
@@ -307,35 +340,35 @@ module ActiveRecord
307
340
  end
308
341
 
309
342
  sql = if pk && use_output_inserted? && !database_prefix_remote_server?
310
- table_name ||= get_table_name(sql)
311
- exclude_output_inserted = exclude_output_inserted_table_name?(table_name, sql)
312
-
313
- if exclude_output_inserted
314
- pk_and_types = Array(pk).map do |subkey|
315
- {
316
- quoted: SQLServer::Utils.extract_identifiers(subkey).quoted,
317
- id_sql_type: exclude_output_inserted_id_sql_type(subkey, exclude_output_inserted)
318
- }
319
- end
320
-
321
- <<~SQL.squish
322
- DECLARE @ssaIdInsertTable table (#{pk_and_types.map { |pk_and_type| "#{pk_and_type[:quoted]} #{pk_and_type[:id_sql_type]}"}.join(", ") });
323
- #{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT #{ pk_and_types.map { |pk_and_type| "INSERTED.#{pk_and_type[:quoted]}" }.join(", ") } INTO @ssaIdInsertTable"}
324
- 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
325
- SQL
326
- else
327
- returning_columns = returning || Array(pk)
328
-
329
- if returning_columns.any?
330
- returning_columns_statements = returning_columns.map { |c| " INSERTED.#{SQLServer::Utils.extract_identifiers(c).quoted}" }
331
- sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT" + returning_columns_statements.join(",")
332
- else
333
- sql
334
- end
335
- end
336
- else
337
- "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
338
- 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
339
372
 
340
373
  [sql, binds]
341
374
  end
@@ -343,9 +376,9 @@ module ActiveRecord
343
376
  # === SQLServer Specific ======================================== #
344
377
 
345
378
  def set_identity_insert(table_name, conn, enable)
346
- internal_raw_execute("SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}", conn , perform_do: true)
347
- rescue Exception
348
- 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}"
349
382
  end
350
383
 
351
384
  # === SQLServer Specific (Executing) ============================ #
@@ -363,7 +396,7 @@ module ActiveRecord
363
396
 
364
397
  def sp_executesql_sql_type(attr)
365
398
  if attr.respond_to?(:type)
366
- 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
367
400
  type = type.subtype if type.serialized?
368
401
 
369
402
  return type.sqlserver_type if type.respond_to?(:sqlserver_type)
@@ -376,9 +409,9 @@ module ActiveRecord
376
409
  value = active_model_attribute?(attr) ? attr.value_for_database : attr
377
410
 
378
411
  if value.is_a?(Numeric)
379
- value > 2_147_483_647 ? "bigint".freeze : "int".freeze
412
+ (value > 2_147_483_647) ? "bigint" : "int"
380
413
  else
381
- "nvarchar(max)".freeze
414
+ "nvarchar(max)"
382
415
  end
383
416
  end
384
417
 
@@ -478,26 +511,24 @@ module ActiveRecord
478
511
  end
479
512
  results = handle.each(query_options)
480
513
 
481
- columns = handle.fields
482
- # If query returns multiple result sets, only return the columns of the last one.
483
- columns = columns.last if columns.any? && columns.all? { |e| e.is_a?(Array) }
484
- 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
485
518
 
486
- 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
487
523
  end
488
524
 
489
525
  def finish_statement_handle(handle)
490
- handle.cancel if handle
526
+ handle&.cancel
491
527
  handle
492
528
  end
493
529
 
494
- # TinyTDS returns false instead of raising an exception if connection fails.
495
- # Getting around this by raising an exception ourselves while PR
496
- # https://github.com/rails-sqlserver/tiny_tds/pull/469 is not released.
497
530
  def internal_raw_execute(sql, raw_connection, perform_do: false)
498
531
  result = raw_connection.execute(sql)
499
- raise TinyTds::Error, "failed to execute statement" if result.is_a?(FalseClass)
500
-
501
532
  perform_do ? result.do : result
502
533
  end
503
534
 
@@ -506,16 +537,16 @@ module ActiveRecord
506
537
  return "" unless insert_all.returning
507
538
 
508
539
  returning_values_sql = if insert_all.returning.is_a?(String)
509
- insert_all.returning
510
- else
511
- Array(insert_all.returning).map do |attribute|
512
- if insert.model.attribute_alias?(attribute)
513
- "INSERTED.#{quote_column_name(insert.model.attribute_alias(attribute))} AS #{quote_column_name(attribute)}"
514
- else
515
- "INSERTED.#{quote_column_name(attribute)}"
516
- end
517
- end.join(",")
518
- 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
519
550
 
520
551
  " OUTPUT #{returning_values_sql}"
521
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