activerecord-sqlserver-adapter 8.0.12 → 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 (72) 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 +13 -84
  5. data/Dockerfile.ci +1 -1
  6. data/Gemfile +9 -12
  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 +170 -172
  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 +119 -67
  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 -61
  41. data/test/cases/change_column_null_test_sqlserver.rb +16 -46
  42. data/test/cases/coerced_tests.rb +365 -161
  43. data/test/cases/column_test_sqlserver.rb +328 -316
  44. data/test/cases/connection_test_sqlserver.rb +15 -11
  45. data/test/cases/enum_test_sqlserver.rb +8 -9
  46. data/test/cases/execute_procedure_test_sqlserver.rb +1 -1
  47. data/test/cases/fetch_test_sqlserver.rb +1 -1
  48. data/test/cases/helper_sqlserver.rb +8 -3
  49. data/test/cases/index_test_sqlserver.rb +8 -6
  50. data/test/cases/insert_all_test_sqlserver.rb +3 -28
  51. data/test/cases/json_test_sqlserver.rb +8 -8
  52. data/test/cases/lateral_test_sqlserver.rb +2 -2
  53. data/test/cases/migration_test_sqlserver.rb +12 -12
  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 -39
  59. data/test/cases/transaction_test_sqlserver.rb +6 -8
  60. data/test/cases/trigger_test_sqlserver.rb +1 -1
  61. data/test/cases/utils_test_sqlserver.rb +3 -3
  62. data/test/cases/view_test_sqlserver.rb +12 -8
  63. data/test/cases/virtual_column_test_sqlserver.rb +113 -0
  64. data/test/migrations/create_clients_and_change_column_collation.rb +2 -2
  65. data/test/models/sqlserver/edge_schema.rb +2 -2
  66. data/test/schema/sqlserver_specific_schema.rb +49 -37
  67. data/test/support/coerceable_test_sqlserver.rb +10 -10
  68. data/test/support/connection_reflection.rb +0 -5
  69. data/test/support/core_ext/backtrace_cleaner.rb +36 -0
  70. data/test/support/query_assertions.rb +6 -6
  71. data/test/support/rake_helpers.rb +6 -10
  72. metadata +13 -108
@@ -4,13 +4,6 @@ module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module SQLServer
6
6
  module SchemaStatements
7
- MERGE_TARGET_IDENTIFIER = /(?:\[[^\]]+\]|[a-z0-9_-]+)/i # :nodoc:
8
- MERGE_TARGET_TABLE_NAME = /\A\s*MERGE\s+INTO\s+(#{MERGE_TARGET_IDENTIFIER}(?:\s*\.#{MERGE_TARGET_IDENTIFIER}){0,2})\s+(?:AS|WITH|USING)/i # :nodoc:
9
-
10
- def native_database_types
11
- @native_database_types ||= initialize_native_database_types.freeze
12
- end
13
-
14
7
  def create_table(table_name, **options)
15
8
  res = super
16
9
  clear_cache!
@@ -39,19 +32,24 @@ module ActiveRecord
39
32
  end
40
33
 
41
34
  def indexes(table_name)
42
- 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
43
40
 
44
41
  data.reduce([]) do |indexes, index|
45
- if index['index_description'].match?(/primary key/)
42
+ if index["index_description"].match?(/primary key/)
46
43
  indexes
47
44
  else
48
- name = index['index_name']
49
- unique = index['index_description'].match?(/unique/)
50
- where = select_value("SELECT [filter_definition] FROM sys.indexes WHERE name = #{quote(name)}", "SCHEMA")
51
- 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 = {}
52
50
  columns = []
53
51
 
54
- index['index_keys'].split(",").each do |column|
52
+ index["index_keys"].split(",").each do |column|
55
53
  column.strip!
56
54
 
57
55
  if column.end_with?("(-)")
@@ -62,43 +60,75 @@ module ActiveRecord
62
60
  columns << column
63
61
  end
64
62
 
65
- 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)
66
64
  end
67
65
  end
68
66
  end
69
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
+
70
88
  def columns(table_name)
71
89
  return [] if table_name.blank?
72
90
 
73
- column_definitions(table_name).map do |ci|
74
- sqlserver_options = ci.slice :ordinal_position, :is_primary, :is_identity, :table_name
75
- sql_type_metadata = fetch_type_metadata ci[:type], sqlserver_options
76
- new_column(
77
- ci[:name],
78
- ci[:default_value],
79
- sql_type_metadata,
80
- ci[:null],
81
- ci[:default_function],
82
- ci[:collation],
83
- nil,
84
- sqlserver_options
85
- )
91
+ definitions = column_definitions(table_name)
92
+ definitions.map do |field|
93
+ new_column_from_field(table_name, field, definitions)
86
94
  end
87
95
  end
88
96
 
89
- 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
+
90
108
  SQLServer::Column.new(
91
- name,
92
- default,
109
+ field[:name],
110
+ lookup_cast_type(field[:type]),
111
+ field[:default_value],
93
112
  sql_type_metadata,
94
- null,
113
+ field[:null],
95
114
  default_function,
96
- collation: collation,
97
- comment: comment,
115
+ collation: field[:collation],
116
+ comment: nil,
117
+ generated_type: generated_type,
98
118
  **sqlserver_options
99
119
  )
100
120
  end
101
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
+
102
132
  def primary_keys(table_name)
103
133
  primaries = primary_keys_select(table_name)
104
134
  primaries.present? ? primaries : identity_columns(table_name).map(&:name)
@@ -107,8 +137,8 @@ module ActiveRecord
107
137
  def primary_keys_select(table_name)
108
138
  identifier = database_prefix_identifier(table_name)
109
139
  database = identifier.fully_qualified_database_quoted
110
- sql = %{
111
- SELECT #{lowercase_schema_reflection_sql('KCU.COLUMN_NAME')} AS [name]
140
+ sql = %(
141
+ SELECT #{lowercase_schema_reflection_sql("KCU.COLUMN_NAME")} AS [name]
112
142
  FROM #{database}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
113
143
  LEFT OUTER JOIN #{database}.INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC
114
144
  ON KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
@@ -116,11 +146,15 @@ module ActiveRecord
116
146
  AND KCU.CONSTRAINT_CATALOG = TC.CONSTRAINT_CATALOG
117
147
  AND KCU.CONSTRAINT_SCHEMA = TC.CONSTRAINT_SCHEMA
118
148
  AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY'
119
- WHERE KCU.TABLE_NAME = #{prepared_statements ? '@0' : quote(identifier.object)}
120
- 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}
121
155
  AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY'
122
156
  ORDER BY KCU.ORDINAL_POSITION ASC
123
- }.gsub(/[[:space:]]/, " ")
157
+ ).gsub(/[[:space:]]/, " ")
124
158
 
125
159
  binds = []
126
160
  nv128 = SQLServer::Type::UnicodeVarchar.new limit: 128
@@ -163,10 +197,10 @@ module ActiveRecord
163
197
  column_object = schema_cache.columns(table_name).find { |c| c.name.to_s == column_name.to_s }
164
198
  without_constraints = options.key?(:default) || options.key?(:limit)
165
199
  default = if !options.key?(:default) && column_object
166
- column_object.default
167
- else
168
- options[:default]
169
- end
200
+ column_object.default
201
+ else
202
+ options[:default]
203
+ end
170
204
 
171
205
  if without_constraints || (column_object && column_object.type != type.to_sym)
172
206
  remove_default_constraint(table_name, column_name)
@@ -216,7 +250,7 @@ module ActiveRecord
216
250
  end
217
251
 
218
252
  def rename_index(table_name, old_name, new_name)
219
- 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
220
254
 
221
255
  identifier = SQLServer::Utils.extract_identifiers("#{table_name}.#{old_name}")
222
256
  execute_procedure :sp_rename, identifier.quoted, new_name, "INDEX"
@@ -244,7 +278,7 @@ module ActiveRecord
244
278
  identifier = SQLServer::Utils.extract_identifiers(table_name)
245
279
  fk_info = execute_procedure :sp_fkeys, nil, identifier.schema, nil, identifier.object, identifier.schema
246
280
 
247
- 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
248
282
  grouped_fk.map do |group|
249
283
  row = group.first
250
284
  options = {
@@ -296,20 +330,20 @@ module ActiveRecord
296
330
  end
297
331
 
298
332
  def type_to_sql(type, limit: nil, precision: nil, scale: nil, **)
299
- 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)
300
334
  limit = nil unless type_limitable
301
335
 
302
336
  case type.to_s
303
337
  when "integer"
304
338
  case limit
305
- when 1 then "tinyint"
306
- when 2 then "smallint"
307
- when 3..4, nil then "integer"
308
- 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"
309
343
  else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
310
344
  end
311
345
  when "time" # https://learn.microsoft.com/en-us/sql/t-sql/data-types/time-transact-sql
312
- column_type_sql = type.to_s
346
+ column_type_sql = type.to_s.dup
313
347
  if precision
314
348
  if (0..7) === precision
315
349
  column_type_sql << "(#{precision})"
@@ -346,18 +380,18 @@ module ActiveRecord
346
380
  # In SQL Server only the first column added should have the `ADD` keyword.
347
381
  def add_timestamps(table_name, **options)
348
382
  fragments = add_timestamps_for_alter(table_name, **options)
349
- fragments[1..].each { |fragment| fragment.sub!('ADD ', '') }
350
- 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(", ")}"
351
385
  end
352
386
 
353
387
  def columns_for_distinct(columns, orders)
354
388
  order_columns = orders.reject(&:blank?).map { |s|
355
389
  s = visitor.compile(s) unless s.is_a?(String)
356
390
  s.gsub(/\s+(?:ASC|DESC)\b/i, "")
357
- .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
391
+ .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
358
392
  }
359
- .reject(&:blank?)
360
- .reject { |s| columns.include?(s) }
393
+ .reject(&:blank?)
394
+ .reject { |s| columns.include?(s) }
361
395
 
362
396
  order_columns_aliased = order_columns.map.with_index { |column, i| "#{column} AS alias_#{i}" }
363
397
 
@@ -377,12 +411,7 @@ module ActiveRecord
377
411
  if !null.nil? && null == false && !default.nil?
378
412
  execute("UPDATE #{table_id} SET #{column_id}=#{quote(default)} WHERE #{column_id} IS NULL")
379
413
  end
380
-
381
- # If datetime column has precision then it should be datetime2 column.
382
- column_type = column.type
383
- column_type = :datetime2 if column.type.to_s == "datetime" && !column.precision.nil?
384
-
385
- sql = "ALTER TABLE #{table_id} ALTER COLUMN #{column_id} #{type_to_sql column_type, limit: column.limit, precision: column.precision, scale: column.scale}"
414
+ sql = "ALTER TABLE #{table_id} ALTER COLUMN #{column_id} #{type_to_sql column.type, limit: column.limit, precision: column.precision, scale: column.scale}"
386
415
  sql += " NOT NULL" if !null.nil? && null == false
387
416
 
388
417
  execute sql
@@ -410,24 +439,33 @@ module ActiveRecord
410
439
  # Returns an array of schema names.
411
440
  def schema_names
412
441
  sql = <<~SQL.squish
413
- SELECT name
414
- FROM sys.schemas
415
- WHERE
416
- name NOT LIKE 'db_%' AND
417
- 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')
418
447
  SQL
419
448
 
420
449
  query_values(sql, "SCHEMA")
421
450
  end
422
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
+
423
461
  private
424
462
 
425
463
  def data_source_sql(name = nil, type: nil)
426
464
  scope = quoted_scope(name, type: type)
427
465
 
428
- table_schema = lowercase_schema_reflection_sql('TABLE_SCHEMA')
429
- table_name = lowercase_schema_reflection_sql('TABLE_NAME')
430
- 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]}." : ""
431
469
  table_catalog = scope[:database].present? ? quote(scope[:database]) : "DB_NAME()"
432
470
 
433
471
  sql = "SELECT "
@@ -457,46 +495,10 @@ module ActiveRecord
457
495
 
458
496
  # === SQLServer Specific ======================================== #
459
497
 
460
- def initialize_native_database_types
461
- {
462
- primary_key: "bigint NOT NULL IDENTITY(1,1) PRIMARY KEY",
463
- primary_key_nonclustered: "bigint NOT NULL IDENTITY(1,1) PRIMARY KEY NONCLUSTERED",
464
- integer: { name: "int", limit: 4 },
465
- bigint: { name: "bigint" },
466
- boolean: { name: "bit" },
467
- decimal: { name: "decimal" },
468
- money: { name: "money" },
469
- smallmoney: { name: "smallmoney" },
470
- float: { name: "float" },
471
- real: { name: "real" },
472
- date: { name: "date" },
473
- datetime: { name: "datetime" },
474
- datetime2: { name: "datetime2" },
475
- datetimeoffset: { name: "datetimeoffset" },
476
- smalldatetime: { name: "smalldatetime" },
477
- timestamp: { name: "datetime2(6)" },
478
- time: { name: "time" },
479
- char: { name: "char" },
480
- varchar: { name: "varchar", limit: 8000 },
481
- varchar_max: { name: "varchar(max)" },
482
- text_basic: { name: "text" },
483
- nchar: { name: "nchar" },
484
- string: { name: "nvarchar", limit: 4000 },
485
- text: { name: "nvarchar(max)" },
486
- ntext: { name: "ntext" },
487
- binary_basic: { name: "binary" },
488
- varbinary: { name: "varbinary", limit: 8000 },
489
- binary: { name: "varbinary(max)" },
490
- uuid: { name: "uniqueidentifier" },
491
- ss_timestamp: { name: "timestamp" },
492
- json: { name: "nvarchar(max)" }
493
- }
494
- end
495
-
496
498
  def column_definitions(table_name)
497
- identifier = database_prefix_identifier(table_name)
498
- database = identifier.fully_qualified_database_quoted
499
- 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)
500
502
 
501
503
  if view_exists
502
504
  sql = <<~SQL
@@ -518,40 +520,30 @@ module ActiveRecord
518
520
  results = internal_exec_query(sql, "SCHEMA", binds)
519
521
  raise ActiveRecord::StatementInvalid, "Table '#{table_name}' doesn't exist" if results.empty?
520
522
 
521
- columns = results.map do |ci|
522
- col = {
523
- name: ci["name"],
524
- numeric_scale: ci["numeric_scale"],
525
- numeric_precision: ci["numeric_precision"],
526
- datetime_precision: ci["datetime_precision"],
527
- collation: ci["collation"],
528
- ordinal_position: ci["ordinal_position"],
529
- length: ci["length"]
530
- }
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
531
525
 
532
526
  col[:table_name] = view_exists ? view_table_name(table_name) : table_name
533
527
  col[:type] = column_type(ci: ci)
534
- col[:default_value], col[:default_function] = default_value_and_function(default: ci['default_value'],
535
- name: ci['name'],
536
- type: col[:type],
537
- original_type: ci['type'],
538
- view_exists: view_exists,
539
- table_name: table_name,
540
- default_functions: default_functions)
541
-
542
- col[:null] = ci['is_nullable'].to_i == 1
543
- col[:is_primary] = ci['is_primary'].to_i == 1
544
-
545
- if [true, false].include?(ci['is_identity'])
546
- 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"]
547
541
  else
548
- col[:is_identity] = ci['is_identity'].to_i == 1
542
+ ci["is_identity"].to_i == 1
549
543
  end
550
544
 
551
545
  col
552
546
  end
553
-
554
- columns
555
547
  end
556
548
 
557
549
  def default_value_and_function(default:, name:, type:, original_type:, view_exists:, table_name:, default_functions:)
@@ -573,9 +565,9 @@ module ActiveRecord
573
565
  [nil, nil]
574
566
  else
575
567
  type = case type
576
- when /smallint|int|bigint/ then original_type
577
- else type
578
- end
568
+ when /smallint|int|bigint/ then original_type
569
+ else type
570
+ end
579
571
  value = default.match(/\A\((.*)\)\Z/m)[1]
580
572
  value = select_value("SELECT CAST(#{value} AS #{type}) AS value", "SCHEMA")
581
573
  [value, nil]
@@ -583,21 +575,21 @@ module ActiveRecord
583
575
  end
584
576
 
585
577
  def column_type(ci:)
586
- case ci['type']
578
+ case ci["type"]
587
579
  when /^bit|image|text|ntext|datetime$/
588
- ci['type']
580
+ ci["type"]
589
581
  when /^datetime2|datetimeoffset$/i
590
- "#{ci['type']}(#{ci['datetime_precision']})"
582
+ "#{ci["type"]}(#{ci["datetime_precision"]})"
591
583
  when /^time$/i
592
- "#{ci['type']}(#{ci['datetime_precision']})"
584
+ "#{ci["type"]}(#{ci["datetime_precision"]})"
593
585
  when /^numeric|decimal$/i
594
- "#{ci['type']}(#{ci['numeric_precision']},#{ci['numeric_scale']})"
586
+ "#{ci["type"]}(#{ci["numeric_precision"]},#{ci["numeric_scale"]})"
595
587
  when /^float|real$/i
596
- "#{ci['type']}"
588
+ ci["type"]
597
589
  when /^char|nchar|varchar|nvarchar|binary|varbinary|bigint|int|smallint$/
598
- 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"]})"
599
591
  else
600
- ci['type']
592
+ ci["type"]
601
593
  end
602
594
  end
603
595
 
@@ -618,8 +610,8 @@ module ActiveRecord
618
610
 
619
611
  %{
620
612
  SELECT
621
- #{lowercase_schema_reflection_sql('o.name')} AS [table_name],
622
- #{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],
623
615
  t.name AS [type],
624
616
  d.definition AS [default_value],
625
617
  CASE
@@ -649,7 +641,10 @@ module ActiveRecord
649
641
  WHEN ic.object_id IS NOT NULL
650
642
  THEN 1
651
643
  END AS [is_primary],
652
- 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]
653
648
  FROM #{database}.sys.columns c
654
649
  INNER JOIN #{database}.sys.objects o
655
650
  ON c.object_id = o.object_id
@@ -668,6 +663,9 @@ module ActiveRecord
668
663
  ON k.parent_object_id = ic.object_id
669
664
  AND k.unique_index_id = ic.index_id
670
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
671
669
  WHERE
672
670
  o.Object_ID = Object_ID(#{object_id_arg})
673
671
  AND s.name = #{schema_name}
@@ -695,7 +693,7 @@ module ActiveRecord
695
693
  execute_procedure(:sp_helpconstraint, table_name, "nomsg").flatten.select do |row|
696
694
  row["constraint_type"] == "DEFAULT on column #{column_name}"
697
695
  end.each do |row|
698
- 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"]}"
699
697
  end
700
698
  end
701
699
 
@@ -726,11 +724,11 @@ module ActiveRecord
726
724
  .split(/\bSELECT\b(?![^\[]*\])/i)[0]
727
725
  .match(/\s*([^(]*)/i)[0]
728
726
  elsif s.match?(/^\s*UPDATE\s+.*/i)
729
- s.match(/UPDATE\s+([^\(\s]+)\s*/i)[1]
727
+ s.match(/UPDATE\s+([^(\s]+)\s*/i)[1]
730
728
  elsif s.match?(/^\s*MERGE INTO.*/i)
731
- s.match(MERGE_TARGET_TABLE_NAME)[1]
729
+ s.match(/^\s*MERGE\s+INTO\s+(\[?[a-z0-9_ -]+\]?\.?\[?[a-z0-9_ -]+\]?)\s+(AS|WITH|USING)/i)[1]
732
730
  else
733
- s.match(/FROM[\s|\(]+((\[[^\(\]]+\])|[^\(\s]+)\s*/i)[1]
731
+ s.match(/FROM[\s|(]+((\[[^(\]]+\])|[^(\s]+)\s*/i)[1]
734
732
  end.strip
735
733
  end
736
734
 
@@ -753,32 +751,32 @@ module ActiveRecord
753
751
  @view_information ||= {}
754
752
 
755
753
  @view_information[table_name] ||= begin
756
- identifier = SQLServer::Utils.extract_identifiers(table_name)
757
- information_query_table = identifier.database.present? ? "[#{identifier.database}].[INFORMATION_SCHEMA].[VIEWS]" : "[INFORMATION_SCHEMA].[VIEWS]"
758
-
759
- view_info = select_one("SELECT * FROM #{information_query_table} WITH (NOLOCK) WHERE TABLE_NAME = #{quote(identifier.object)}", "SCHEMA").to_h
760
-
761
- if view_info.present?
762
- if view_info['VIEW_DEFINITION'].blank? || view_info['VIEW_DEFINITION'].length == 4000
763
- view_info['VIEW_DEFINITION'] = begin
764
- select_values("EXEC sp_helptext #{identifier.object_quoted}", "SCHEMA").join
765
- rescue
766
- warn "No view definition found, possible permissions problem.\nPlease run GRANT VIEW DEFINITION TO your_user;"
767
- nil
768
- end
769
- end
770
- end
754
+ identifier = SQLServer::Utils.extract_identifiers(table_name)
755
+ information_query_table = identifier.database.present? ? "[#{identifier.database}].[INFORMATION_SCHEMA].[VIEWS]" : "[INFORMATION_SCHEMA].[VIEWS]"
756
+
757
+ view_info = select_one("SELECT * FROM #{information_query_table} WITH (NOLOCK) WHERE TABLE_NAME = #{quote(identifier.object)}", "SCHEMA").to_h
758
+
759
+ if view_info.present?
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
767
+ end
768
+ end
771
769
 
772
- view_info
773
- end
770
+ view_info
771
+ end
774
772
  end
775
773
 
776
774
  def views_real_column_name(table_name, column_name)
777
- view_definition = view_information(table_name)['VIEW_DEFINITION']
775
+ view_definition = view_information(table_name)["VIEW_DEFINITION"]
778
776
  return column_name if view_definition.blank?
779
777
 
780
778
  # Remove "CREATE VIEW ... AS SELECT ..." and then match the column name.
781
- 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)
782
780
  match_data ? match_data[1] : column_name
783
781
  end
784
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