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
@@ -4,10 +4,6 @@ module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module SQLServer
6
6
  module SchemaStatements
7
- def native_database_types
8
- @native_database_types ||= initialize_native_database_types.freeze
9
- end
10
-
11
7
  def create_table(table_name, **options)
12
8
  res = super
13
9
  clear_cache!
@@ -36,19 +32,24 @@ module ActiveRecord
36
32
  end
37
33
 
38
34
  def indexes(table_name)
39
- data = select("EXEC sp_helpindex #{quote(table_name)}", "SCHEMA") rescue []
35
+ data = begin
36
+ select("EXEC sp_helpindex #{quote(table_name)}", "SCHEMA")
37
+ rescue
38
+ []
39
+ end
40
40
 
41
41
  data.reduce([]) do |indexes, index|
42
- if index['index_description'].match?(/primary key/)
42
+ if index["index_description"].match?(/primary key/)
43
43
  indexes
44
44
  else
45
- name = index['index_name']
46
- unique = index['index_description'].match?(/unique/)
47
- where = select_value("SELECT [filter_definition] FROM sys.indexes WHERE name = #{quote(name)}", "SCHEMA")
48
- orders = {}
45
+ name = index["index_name"]
46
+ unique = index["index_description"].match?(/unique/)
47
+ where = select_value("SELECT [filter_definition] FROM sys.indexes WHERE name = #{quote(name)}", "SCHEMA")
48
+ include_columns = index_include_columns(table_name, name)
49
+ orders = {}
49
50
  columns = []
50
51
 
51
- index['index_keys'].split(",").each do |column|
52
+ index["index_keys"].split(",").each do |column|
52
53
  column.strip!
53
54
 
54
55
  if column.end_with?("(-)")
@@ -59,43 +60,75 @@ module ActiveRecord
59
60
  columns << column
60
61
  end
61
62
 
62
- indexes << IndexDefinition.new(table_name, name, unique, columns, where: where, orders: orders)
63
+ indexes << IndexDefinition.new(table_name, name, unique, columns, where: where, orders: orders, include: include_columns.presence)
63
64
  end
64
65
  end
65
66
  end
66
67
 
68
+ def index_include_columns(table_name, index_name)
69
+ sql = <<~SQL
70
+ SELECT
71
+ ic.index_id,
72
+ c.name AS column_name
73
+ FROM
74
+ sys.indexes i
75
+ JOIN
76
+ sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
77
+ JOIN
78
+ sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
79
+ WHERE
80
+ i.object_id = OBJECT_ID('#{table_name}')
81
+ AND i.name = '#{index_name}'
82
+ AND ic.is_included_column = 1;
83
+ SQL
84
+
85
+ select_all(sql, "SCHEMA").map { |row| row["column_name"] }
86
+ end
87
+
67
88
  def columns(table_name)
68
89
  return [] if table_name.blank?
69
90
 
70
- column_definitions(table_name).map do |ci|
71
- sqlserver_options = ci.slice :ordinal_position, :is_primary, :is_identity, :table_name
72
- sql_type_metadata = fetch_type_metadata ci[:type], sqlserver_options
73
- new_column(
74
- ci[:name],
75
- ci[:default_value],
76
- sql_type_metadata,
77
- ci[:null],
78
- ci[:default_function],
79
- ci[:collation],
80
- nil,
81
- sqlserver_options
82
- )
91
+ definitions = column_definitions(table_name)
92
+ definitions.map do |field|
93
+ new_column_from_field(table_name, field, definitions)
83
94
  end
84
95
  end
85
96
 
86
- def new_column(name, default, sql_type_metadata, null, default_function = nil, collation = nil, comment = nil, sqlserver_options = {})
97
+ def new_column_from_field(_table_name, field, _definitions)
98
+ sqlserver_options = field.slice(:ordinal_position, :is_primary, :is_identity, :table_name)
99
+ sql_type_metadata = fetch_type_metadata(field[:type], sqlserver_options)
100
+ generated_type = extract_generated_type(field)
101
+
102
+ default_function = if generated_type.present?
103
+ field[:computed_formula]
104
+ else
105
+ field[:default_function]
106
+ end
107
+
87
108
  SQLServer::Column.new(
88
- name,
89
- default,
109
+ field[:name],
110
+ lookup_cast_type(field[:type]),
111
+ field[:default_value],
90
112
  sql_type_metadata,
91
- null,
113
+ field[:null],
92
114
  default_function,
93
- collation: collation,
94
- comment: comment,
115
+ collation: field[:collation],
116
+ comment: nil,
117
+ generated_type: generated_type,
95
118
  **sqlserver_options
96
119
  )
97
120
  end
98
121
 
122
+ def extract_generated_type(field)
123
+ if field[:is_computed]
124
+ if field[:is_persisted]
125
+ :stored
126
+ else
127
+ :virtual
128
+ end
129
+ end
130
+ end
131
+
99
132
  def primary_keys(table_name)
100
133
  primaries = primary_keys_select(table_name)
101
134
  primaries.present? ? primaries : identity_columns(table_name).map(&:name)
@@ -104,8 +137,8 @@ module ActiveRecord
104
137
  def primary_keys_select(table_name)
105
138
  identifier = database_prefix_identifier(table_name)
106
139
  database = identifier.fully_qualified_database_quoted
107
- sql = %{
108
- SELECT #{lowercase_schema_reflection_sql('KCU.COLUMN_NAME')} AS [name]
140
+ sql = %(
141
+ SELECT #{lowercase_schema_reflection_sql("KCU.COLUMN_NAME")} AS [name]
109
142
  FROM #{database}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
110
143
  LEFT OUTER JOIN #{database}.INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC
111
144
  ON KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
@@ -113,11 +146,15 @@ module ActiveRecord
113
146
  AND KCU.CONSTRAINT_CATALOG = TC.CONSTRAINT_CATALOG
114
147
  AND KCU.CONSTRAINT_SCHEMA = TC.CONSTRAINT_SCHEMA
115
148
  AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY'
116
- WHERE KCU.TABLE_NAME = #{prepared_statements ? '@0' : quote(identifier.object)}
117
- AND KCU.TABLE_SCHEMA = #{identifier.schema.blank? ? 'schema_name()' : (prepared_statements ? '@1' : quote(identifier.schema))}
149
+ WHERE KCU.TABLE_NAME = #{prepared_statements ? "@0" : quote(identifier.object)}
150
+ AND KCU.TABLE_SCHEMA = #{if identifier.schema.blank?
151
+ "schema_name()"
152
+ else
153
+ (prepared_statements ? "@1" : quote(identifier.schema))
154
+ end}
118
155
  AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY'
119
156
  ORDER BY KCU.ORDINAL_POSITION ASC
120
- }.gsub(/[[:space:]]/, " ")
157
+ ).gsub(/[[:space:]]/, " ")
121
158
 
122
159
  binds = []
123
160
  nv128 = SQLServer::Type::UnicodeVarchar.new limit: 128
@@ -160,10 +197,10 @@ module ActiveRecord
160
197
  column_object = schema_cache.columns(table_name).find { |c| c.name.to_s == column_name.to_s }
161
198
  without_constraints = options.key?(:default) || options.key?(:limit)
162
199
  default = if !options.key?(:default) && column_object
163
- column_object.default
164
- else
165
- options[:default]
166
- end
200
+ column_object.default
201
+ else
202
+ options[:default]
203
+ end
167
204
 
168
205
  if without_constraints || (column_object && column_object.type != type.to_sym)
169
206
  remove_default_constraint(table_name, column_name)
@@ -213,7 +250,7 @@ module ActiveRecord
213
250
  end
214
251
 
215
252
  def rename_index(table_name, old_name, new_name)
216
- raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters" if new_name.length > index_name_length
253
+ raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long (#{new_name.length} characters); the limit is #{index_name_length} characters" if new_name.length > index_name_length
217
254
 
218
255
  identifier = SQLServer::Utils.extract_identifiers("#{table_name}.#{old_name}")
219
256
  execute_procedure :sp_rename, identifier.quoted, new_name, "INDEX"
@@ -241,7 +278,7 @@ module ActiveRecord
241
278
  identifier = SQLServer::Utils.extract_identifiers(table_name)
242
279
  fk_info = execute_procedure :sp_fkeys, nil, identifier.schema, nil, identifier.object, identifier.schema
243
280
 
244
- grouped_fk = fk_info.group_by { |row| row["FK_NAME"] }.values.each { |group| group.sort_by! { |row| row["KEY_SEQ"] } }
281
+ grouped_fk = fk_info.group_by { |row| row["FK_NAME"] }.values.each { |group| group.sort_by! { |row| row["KEY_SEQ"] } }.reverse
245
282
  grouped_fk.map do |group|
246
283
  row = group.first
247
284
  options = {
@@ -293,20 +330,20 @@ module ActiveRecord
293
330
  end
294
331
 
295
332
  def type_to_sql(type, limit: nil, precision: nil, scale: nil, **)
296
- type_limitable = %w(string integer float char nchar varchar nvarchar binary_basic).include?(type.to_s)
333
+ type_limitable = %w[string integer float char nchar varchar nvarchar binary_basic].include?(type.to_s)
297
334
  limit = nil unless type_limitable
298
335
 
299
336
  case type.to_s
300
337
  when "integer"
301
338
  case limit
302
- when 1 then "tinyint"
303
- when 2 then "smallint"
304
- when 3..4, nil then "integer"
305
- when 5..8 then "bigint"
339
+ when 1 then "tinyint"
340
+ when 2 then "smallint"
341
+ when 3..4, nil then "integer"
342
+ when 5..8 then "bigint"
306
343
  else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
307
344
  end
308
345
  when "time" # https://learn.microsoft.com/en-us/sql/t-sql/data-types/time-transact-sql
309
- column_type_sql = type.to_s
346
+ column_type_sql = type.to_s.dup
310
347
  if precision
311
348
  if (0..7) === precision
312
349
  column_type_sql << "(#{precision})"
@@ -343,18 +380,18 @@ module ActiveRecord
343
380
  # In SQL Server only the first column added should have the `ADD` keyword.
344
381
  def add_timestamps(table_name, **options)
345
382
  fragments = add_timestamps_for_alter(table_name, **options)
346
- fragments[1..].each { |fragment| fragment.sub!('ADD ', '') }
347
- execute "ALTER TABLE #{quote_table_name(table_name)} #{fragments.join(', ')}"
383
+ fragments[1..].each { |fragment| fragment.sub!("ADD ", "") }
384
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{fragments.join(", ")}"
348
385
  end
349
386
 
350
387
  def columns_for_distinct(columns, orders)
351
388
  order_columns = orders.reject(&:blank?).map { |s|
352
- s = visitor.compile(s) unless s.is_a?(String)
353
- s.gsub(/\s+(?:ASC|DESC)\b/i, "")
354
- .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
355
- }
356
- .reject(&:blank?)
357
- .reject { |s| columns.include?(s) }
389
+ s = visitor.compile(s) unless s.is_a?(String)
390
+ s.gsub(/\s+(?:ASC|DESC)\b/i, "")
391
+ .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
392
+ }
393
+ .reject(&:blank?)
394
+ .reject { |s| columns.include?(s) }
358
395
 
359
396
  order_columns_aliased = order_columns.map.with_index { |column, i| "#{column} AS alias_#{i}" }
360
397
 
@@ -402,24 +439,33 @@ module ActiveRecord
402
439
  # Returns an array of schema names.
403
440
  def schema_names
404
441
  sql = <<~SQL.squish
405
- SELECT name
406
- FROM sys.schemas
407
- WHERE
408
- name NOT LIKE 'db_%' AND
409
- name NOT IN ('INFORMATION_SCHEMA', 'sys', 'guest')
442
+ SELECT name
443
+ FROM sys.schemas
444
+ WHERE
445
+ name NOT LIKE 'db_%' AND
446
+ name NOT IN ('INFORMATION_SCHEMA', 'sys', 'guest')
410
447
  SQL
411
448
 
412
449
  query_values(sql, "SCHEMA")
413
450
  end
414
451
 
452
+ def quoted_include_columns_for_index(column_names) # :nodoc:
453
+ return quote_column_name(column_names) if column_names.is_a?(Symbol)
454
+
455
+ quoted_columns = column_names.each_with_object({}) do |name, result|
456
+ result[name.to_sym] = quote_column_name(name).dup
457
+ end
458
+ add_options_for_index_columns(quoted_columns).values.join(", ")
459
+ end
460
+
415
461
  private
416
462
 
417
463
  def data_source_sql(name = nil, type: nil)
418
464
  scope = quoted_scope(name, type: type)
419
465
 
420
- table_schema = lowercase_schema_reflection_sql('TABLE_SCHEMA')
421
- table_name = lowercase_schema_reflection_sql('TABLE_NAME')
422
- database = scope[:database].present? ? "#{scope[:database]}." : ""
466
+ table_schema = lowercase_schema_reflection_sql("TABLE_SCHEMA")
467
+ table_name = lowercase_schema_reflection_sql("TABLE_NAME")
468
+ database = scope[:database].present? ? "#{scope[:database]}." : ""
423
469
  table_catalog = scope[:database].present? ? quote(scope[:database]) : "DB_NAME()"
424
470
 
425
471
  sql = "SELECT "
@@ -449,46 +495,10 @@ module ActiveRecord
449
495
 
450
496
  # === SQLServer Specific ======================================== #
451
497
 
452
- def initialize_native_database_types
453
- {
454
- primary_key: "bigint NOT NULL IDENTITY(1,1) PRIMARY KEY",
455
- primary_key_nonclustered: "bigint NOT NULL IDENTITY(1,1) PRIMARY KEY NONCLUSTERED",
456
- integer: { name: "int", limit: 4 },
457
- bigint: { name: "bigint" },
458
- boolean: { name: "bit" },
459
- decimal: { name: "decimal" },
460
- money: { name: "money" },
461
- smallmoney: { name: "smallmoney" },
462
- float: { name: "float" },
463
- real: { name: "real" },
464
- date: { name: "date" },
465
- datetime: { name: "datetime" },
466
- datetime2: { name: "datetime2" },
467
- datetimeoffset: { name: "datetimeoffset" },
468
- smalldatetime: { name: "smalldatetime" },
469
- timestamp: { name: "datetime2(6)" },
470
- time: { name: "time" },
471
- char: { name: "char" },
472
- varchar: { name: "varchar", limit: 8000 },
473
- varchar_max: { name: "varchar(max)" },
474
- text_basic: { name: "text" },
475
- nchar: { name: "nchar" },
476
- string: { name: "nvarchar", limit: 4000 },
477
- text: { name: "nvarchar(max)" },
478
- ntext: { name: "ntext" },
479
- binary_basic: { name: "binary" },
480
- varbinary: { name: "varbinary", limit: 8000 },
481
- binary: { name: "varbinary(max)" },
482
- uuid: { name: "uniqueidentifier" },
483
- ss_timestamp: { name: "timestamp" },
484
- json: { name: "nvarchar(max)" }
485
- }
486
- end
487
-
488
498
  def column_definitions(table_name)
489
- identifier = database_prefix_identifier(table_name)
490
- database = identifier.fully_qualified_database_quoted
491
- view_exists = view_exists?(table_name)
499
+ identifier = database_prefix_identifier(table_name)
500
+ database = identifier.fully_qualified_database_quoted
501
+ view_exists = view_exists?(table_name)
492
502
 
493
503
  if view_exists
494
504
  sql = <<~SQL
@@ -510,40 +520,30 @@ module ActiveRecord
510
520
  results = internal_exec_query(sql, "SCHEMA", binds)
511
521
  raise ActiveRecord::StatementInvalid, "Table '#{table_name}' doesn't exist" if results.empty?
512
522
 
513
- columns = results.map do |ci|
514
- col = {
515
- name: ci["name"],
516
- numeric_scale: ci["numeric_scale"],
517
- numeric_precision: ci["numeric_precision"],
518
- datetime_precision: ci["datetime_precision"],
519
- collation: ci["collation"],
520
- ordinal_position: ci["ordinal_position"],
521
- length: ci["length"]
522
- }
523
+ results.map do |ci|
524
+ col = ci.slice("name", "numeric_scale", "numeric_precision", "datetime_precision", "collation", "ordinal_position", "length", "is_computed", "is_persisted", "computed_formula").symbolize_keys
523
525
 
524
526
  col[:table_name] = view_exists ? view_table_name(table_name) : table_name
525
527
  col[:type] = column_type(ci: ci)
526
- col[:default_value], col[:default_function] = default_value_and_function(default: ci['default_value'],
527
- name: ci['name'],
528
- type: col[:type],
529
- original_type: ci['type'],
530
- view_exists: view_exists,
531
- table_name: table_name,
532
- default_functions: default_functions)
533
-
534
- col[:null] = ci['is_nullable'].to_i == 1
535
- col[:is_primary] = ci['is_primary'].to_i == 1
536
-
537
- if [true, false].include?(ci['is_identity'])
538
- col[:is_identity] = ci['is_identity']
528
+ col[:default_value], col[:default_function] = default_value_and_function(default: ci["default_value"],
529
+ name: ci["name"],
530
+ type: col[:type],
531
+ original_type: ci["type"],
532
+ view_exists: view_exists,
533
+ table_name: table_name,
534
+ default_functions: default_functions)
535
+
536
+ col[:null] = ci["is_nullable"].to_i == 1
537
+ col[:is_primary] = ci["is_primary"].to_i == 1
538
+
539
+ col[:is_identity] = if [true, false].include?(ci["is_identity"])
540
+ ci["is_identity"]
539
541
  else
540
- col[:is_identity] = ci['is_identity'].to_i == 1
542
+ ci["is_identity"].to_i == 1
541
543
  end
542
544
 
543
545
  col
544
546
  end
545
-
546
- columns
547
547
  end
548
548
 
549
549
  def default_value_and_function(default:, name:, type:, original_type:, view_exists:, table_name:, default_functions:)
@@ -565,9 +565,9 @@ module ActiveRecord
565
565
  [nil, nil]
566
566
  else
567
567
  type = case type
568
- when /smallint|int|bigint/ then original_type
569
- else type
570
- end
568
+ when /smallint|int|bigint/ then original_type
569
+ else type
570
+ end
571
571
  value = default.match(/\A\((.*)\)\Z/m)[1]
572
572
  value = select_value("SELECT CAST(#{value} AS #{type}) AS value", "SCHEMA")
573
573
  [value, nil]
@@ -575,21 +575,21 @@ module ActiveRecord
575
575
  end
576
576
 
577
577
  def column_type(ci:)
578
- case ci['type']
578
+ case ci["type"]
579
579
  when /^bit|image|text|ntext|datetime$/
580
- ci['type']
580
+ ci["type"]
581
581
  when /^datetime2|datetimeoffset$/i
582
- "#{ci['type']}(#{ci['datetime_precision']})"
582
+ "#{ci["type"]}(#{ci["datetime_precision"]})"
583
583
  when /^time$/i
584
- "#{ci['type']}(#{ci['datetime_precision']})"
584
+ "#{ci["type"]}(#{ci["datetime_precision"]})"
585
585
  when /^numeric|decimal$/i
586
- "#{ci['type']}(#{ci['numeric_precision']},#{ci['numeric_scale']})"
586
+ "#{ci["type"]}(#{ci["numeric_precision"]},#{ci["numeric_scale"]})"
587
587
  when /^float|real$/i
588
- "#{ci['type']}"
588
+ ci["type"]
589
589
  when /^char|nchar|varchar|nvarchar|binary|varbinary|bigint|int|smallint$/
590
- ci['length'].to_i == -1 ? "#{ci['type']}(max)" : "#{ci['type']}(#{ci['length']})"
590
+ (ci["length"].to_i == -1) ? "#{ci["type"]}(max)" : "#{ci["type"]}(#{ci["length"]})"
591
591
  else
592
- ci['type']
592
+ ci["type"]
593
593
  end
594
594
  end
595
595
 
@@ -610,8 +610,8 @@ module ActiveRecord
610
610
 
611
611
  %{
612
612
  SELECT
613
- #{lowercase_schema_reflection_sql('o.name')} AS [table_name],
614
- #{lowercase_schema_reflection_sql('c.name')} AS [name],
613
+ #{lowercase_schema_reflection_sql("o.name")} AS [table_name],
614
+ #{lowercase_schema_reflection_sql("c.name")} AS [name],
615
615
  t.name AS [type],
616
616
  d.definition AS [default_value],
617
617
  CASE
@@ -641,7 +641,10 @@ module ActiveRecord
641
641
  WHEN ic.object_id IS NOT NULL
642
642
  THEN 1
643
643
  END AS [is_primary],
644
- c.is_identity AS [is_identity]
644
+ c.is_identity AS [is_identity],
645
+ c.is_computed AS [is_computed],
646
+ cc.is_persisted AS [is_persisted],
647
+ cc.definition AS [computed_formula]
645
648
  FROM #{database}.sys.columns c
646
649
  INNER JOIN #{database}.sys.objects o
647
650
  ON c.object_id = o.object_id
@@ -660,6 +663,9 @@ module ActiveRecord
660
663
  ON k.parent_object_id = ic.object_id
661
664
  AND k.unique_index_id = ic.index_id
662
665
  AND c.column_id = ic.column_id
666
+ LEFT OUTER JOIN #{database}.sys.computed_columns cc
667
+ ON c.object_id = cc.object_id
668
+ AND c.column_id = cc.column_id
663
669
  WHERE
664
670
  o.Object_ID = Object_ID(#{object_id_arg})
665
671
  AND s.name = #{schema_name}
@@ -687,7 +693,7 @@ module ActiveRecord
687
693
  execute_procedure(:sp_helpconstraint, table_name, "nomsg").flatten.select do |row|
688
694
  row["constraint_type"] == "DEFAULT on column #{column_name}"
689
695
  end.each do |row|
690
- execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{row['constraint_name']}"
696
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{row["constraint_name"]}"
691
697
  end
692
698
  end
693
699
 
@@ -718,11 +724,11 @@ module ActiveRecord
718
724
  .split(/\bSELECT\b(?![^\[]*\])/i)[0]
719
725
  .match(/\s*([^(]*)/i)[0]
720
726
  elsif s.match?(/^\s*UPDATE\s+.*/i)
721
- s.match(/UPDATE\s+([^\(\s]+)\s*/i)[1]
727
+ s.match(/UPDATE\s+([^(\s]+)\s*/i)[1]
722
728
  elsif s.match?(/^\s*MERGE INTO.*/i)
723
729
  s.match(/^\s*MERGE\s+INTO\s+(\[?[a-z0-9_ -]+\]?\.?\[?[a-z0-9_ -]+\]?)\s+(AS|WITH|USING)/i)[1]
724
730
  else
725
- s.match(/FROM[\s|\(]+((\[[^\(\]]+\])|[^\(\s]+)\s*/i)[1]
731
+ s.match(/FROM[\s|(]+((\[[^(\]]+\])|[^(\s]+)\s*/i)[1]
726
732
  end.strip
727
733
  end
728
734
 
@@ -746,18 +752,18 @@ module ActiveRecord
746
752
 
747
753
  @view_information[table_name] ||= begin
748
754
  identifier = SQLServer::Utils.extract_identifiers(table_name)
749
- information_query_table = identifier.database.present? ? "[#{identifier.database}].[INFORMATION_SCHEMA].[VIEWS]" : "[INFORMATION_SCHEMA].[VIEWS]"
755
+ information_query_table = identifier.database.present? ? "[#{identifier.database}].[INFORMATION_SCHEMA].[VIEWS]" : "[INFORMATION_SCHEMA].[VIEWS]"
750
756
 
751
757
  view_info = select_one("SELECT * FROM #{information_query_table} WITH (NOLOCK) WHERE TABLE_NAME = #{quote(identifier.object)}", "SCHEMA").to_h
752
758
 
753
759
  if view_info.present?
754
- if view_info['VIEW_DEFINITION'].blank? || view_info['VIEW_DEFINITION'].length == 4000
755
- view_info['VIEW_DEFINITION'] = begin
756
- select_values("EXEC sp_helptext #{identifier.object_quoted}", "SCHEMA").join
757
- rescue
758
- warn "No view definition found, possible permissions problem.\nPlease run GRANT VIEW DEFINITION TO your_user;"
759
- nil
760
- end
760
+ if view_info["VIEW_DEFINITION"].blank? || view_info["VIEW_DEFINITION"].length == 4000
761
+ view_info["VIEW_DEFINITION"] = begin
762
+ select_values("EXEC sp_helptext #{identifier.object_quoted}", "SCHEMA").join
763
+ rescue
764
+ warn "No view definition found, possible permissions problem.\nPlease run GRANT VIEW DEFINITION TO your_user;"
765
+ nil
766
+ end
761
767
  end
762
768
  end
763
769
 
@@ -766,11 +772,11 @@ module ActiveRecord
766
772
  end
767
773
 
768
774
  def views_real_column_name(table_name, column_name)
769
- view_definition = view_information(table_name)['VIEW_DEFINITION']
775
+ view_definition = view_information(table_name)["VIEW_DEFINITION"]
770
776
  return column_name if view_definition.blank?
771
777
 
772
778
  # Remove "CREATE VIEW ... AS SELECT ..." and then match the column name.
773
- match_data = view_definition.sub(/CREATE\s+VIEW.*AS\s+SELECT\s/, '').match(/([\w-]*)\s+AS\s+#{column_name}\W/im)
779
+ match_data = view_definition.sub(/CREATE\s+VIEW.*AS\s+SELECT\s/, "").match(/([\w-]*)\s+AS\s+#{column_name}\W/im)
774
780
  match_data ? match_data[1] : column_name
775
781
  end
776
782
 
@@ -36,7 +36,7 @@ module ActiveRecord
36
36
  result.columns.each_with_index do |column, i|
37
37
  cells_in_column = [column] + result.rows.map { |r| cast_item(r[i]) }
38
38
  computed_width = cells_in_column.map(&:length).max
39
- final_width = computed_width > max_column_width ? max_column_width : computed_width
39
+ final_width = (computed_width > max_column_width) ? max_column_width : computed_width
40
40
  computed_widths << final_width
41
41
  end
42
42
  end
@@ -51,7 +51,7 @@ module ActiveRecord
51
51
  items.each_with_index do |item, i|
52
52
  cells << cast_item(item).ljust(@widths[i])
53
53
  end
54
- "| #{cells.join(' | ')} |"
54
+ "| #{cells.join(" | ")} |"
55
55
  end
56
56
 
57
57
  def cast_item(item)
@@ -7,9 +7,9 @@ module ActiveRecord
7
7
  module ConnectionAdapters
8
8
  module SQLServer
9
9
  module Showplan
10
- OPTION_ALL = "SHOWPLAN_ALL"
10
+ OPTION_ALL = "SHOWPLAN_ALL"
11
11
  OPTION_TEXT = "SHOWPLAN_TEXT"
12
- OPTION_XML = "SHOWPLAN_XML"
12
+ OPTION_XML = "SHOWPLAN_XML"
13
13
  OPTIONS = [OPTION_ALL, OPTION_TEXT, OPTION_XML]
14
14
 
15
15
  def explain(arel, binds = [], options = [])
@@ -30,10 +30,10 @@ module ActiveRecord
30
30
  end
31
31
 
32
32
  def set_showplan_option(enable = true)
33
- sql = "SET #{showplan_option} #{enable ? 'ON' : 'OFF'}"
33
+ sql = "SET #{showplan_option} #{enable ? "ON" : "OFF"}"
34
34
  raw_execute(sql, "SCHEMA")
35
- rescue Exception
36
- raise ActiveRecordError, "#{showplan_option} could not be turned #{enable ? 'ON' : 'OFF'}, perhaps you do not have SHOWPLAN permissions?"
35
+ rescue
36
+ raise ActiveRecordError, "#{showplan_option} could not be turned #{enable ? "ON" : "OFF"}, perhaps you do not have SHOWPLAN permissions?"
37
37
  end
38
38
 
39
39
  def showplan_option
@@ -26,15 +26,10 @@ module ActiveRecord
26
26
  table_name == other.table_name &&
27
27
  ordinal_position == other.ordinal_position
28
28
  end
29
- alias eql? ==
29
+ alias_method :eql?, :==
30
30
 
31
31
  def hash
32
- TypeMetadata.hash ^
33
- __getobj__.hash ^
34
- is_identity.hash ^
35
- is_primary.hash ^
36
- table_name.hash ^
37
- ordinal_position.hash
32
+ [TypeMetadata, __getobj__, is_identity, is_primary, table_name, ordinal_position].hash
38
33
  end
39
34
 
40
35
  private
@@ -109,6 +109,8 @@ module ActiveRecord
109
109
  type = :datetime2 unless options[:precision].nil?
110
110
  when :primary_key
111
111
  options[:is_identity] = true
112
+ when :virtual
113
+ type = options[:type]
112
114
  end
113
115
 
114
116
  super
@@ -117,7 +119,7 @@ module ActiveRecord
117
119
  private
118
120
 
119
121
  def valid_column_definition_options
120
- super + [:is_identity]
122
+ super + [:is_identity, :as, :stored]
121
123
  end
122
124
  end
123
125
 
@@ -35,16 +35,16 @@ module ActiveRecord
35
35
 
36
36
  self.class == other.class && value == other.value
37
37
  end
38
- alias :== :eql?
38
+ alias_method :==, :eql?
39
39
 
40
40
  def self.from_msgpack_ext(string)
41
- type, value = string.chomp!("msgpack_ext").split(',')
41
+ type, value = string.chomp!("msgpack_ext").split(",")
42
42
 
43
43
  Data.new(value, type.constantize)
44
44
  end
45
45
 
46
46
  def to_msgpack_ext
47
- [type.class.to_s, value].join(',') + "msgpack_ext"
47
+ [type.class.to_s, value].join(",") + "msgpack_ext"
48
48
  end
49
49
  end
50
50
  end
@@ -9,12 +9,12 @@ module ActiveRecord
9
9
  "date"
10
10
  end
11
11
 
12
- def serialize(value)
12
+ def serialize(_value)
13
13
  value = super
14
14
  return value unless value.acts_like?(:date)
15
15
 
16
- date = super(value).to_formatted_s(:_sqlserver_dateformat)
17
- Data.new date, self
16
+ date = super.to_formatted_s(:_sqlserver_dateformat)
17
+ Data.new(date, self)
18
18
  end
19
19
 
20
20
  def deserialize(value)