activerecord-sqlserver-adapter 7.2.1 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73bbb76f7c1a620edbaf0aa6e69c5a8fcb9ba2f7c7c14716bb2663f776b8cb0d
4
- data.tar.gz: a6d9ac56dbcddd486bd0111da5db2e2d0cc6c2bbc3519041a73fffa5cfc3fddb
3
+ metadata.gz: 926f65915d98bb3c271370883fa89891fc575b5c6801cd8c235386c078d34bf5
4
+ data.tar.gz: 0a932a9d5edf0d3932e3f0ba44328ed77a033a1223f05699e8f4caa79116ac56
5
5
  SHA512:
6
- metadata.gz: 921bd35d9de85b363190dee9b001373fbae60b0c1e99755ff4b172c9f51510d7353b49fb39d636b4acc015755067846bfed6ca4f984eb75d91def007679154da
7
- data.tar.gz: 384e76d966178e7c8bdc4f84cc03e3739bcfec1291fd42e57d8eb4804b7df37e4311f94ee659efa4d6b76a82ae833f8cd741f66bb667abf7a1e0c2f182107be9
6
+ metadata.gz: ee45540a8d2ae57dc69a35048f4ccfbf01bcfc91e177e57405b19ccb8a3279eef7b752c22dee20c4171c8bb4e2cb0fcaadb99cd6c25d139fd2ef53577a5cb23a
7
+ data.tar.gz: 9184a7041e5de445103bcd47832352614897865f0ef49e83067b553786d089be73eb73c34dfaf3f102e02804ee7d8d40803d5582430ebfeecbe95f568b0b7cb7
@@ -1,6 +1,12 @@
1
1
  name: CI
2
2
 
3
- on: [push, pull_request]
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+ schedule:
9
+ - cron: '0 4 * * 1'
4
10
 
5
11
  jobs:
6
12
  test:
@@ -14,9 +20,8 @@ jobs:
14
20
  fail-fast: false
15
21
  matrix:
16
22
  ruby:
17
- - 3.1.6
18
- - 3.2.4
19
- - 3.3.2
23
+ - 3.3.4
24
+ - 3.2.5
20
25
 
21
26
  steps:
22
27
  - name: Checkout code
data/CHANGELOG.md CHANGED
@@ -1,19 +1,12 @@
1
- ## v7.2.1
2
-
3
- #### Fixed
4
-
5
- - [#1231](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1231) Enable identity insert on view's base table
6
-
7
- ## v7.2.0
8
-
9
- #### Added
10
-
11
- - [#1178](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1178) Support encrypting binary columns
1
+ ## v8.0.0
12
2
 
13
3
  #### Changed
14
4
 
15
- - [#1153](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1153) Only support Ruby v3.1+
16
- - [#1196](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1196) Use default inspect for database adapter
5
+ - [#1216](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1216) Refactor adapter interface to match abstract adapter
6
+ - [#1225](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1225) Drop support to Ruby 3.1
7
+
8
+ #### Fixed
17
9
 
10
+ - [#1215](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1215) Fix mismatched foreign key errors
18
11
 
19
- Please check [7-1-stable](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/blob/7-1-stable/CHANGELOG.md) for previous changes.
12
+ Please check [7-2-stable](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/blob/7-2-stable/CHANGELOG.md) for previous changes.
data/Dockerfile.ci CHANGED
@@ -9,6 +9,6 @@ WORKDIR $WORKDIR
9
9
 
10
10
  COPY . $WORKDIR
11
11
 
12
- RUN RAILS_BRANCH=7-2-stable bundle install --jobs `expr $(cat /proc/cpuinfo | grep -c "cpu cores") - 1` --retry 3
12
+ RUN bundle install --jobs `expr $(cat /proc/cpuinfo | grep -c "cpu cores") - 1` --retry 3
13
13
 
14
14
  CMD ["sh"]
data/Gemfile CHANGED
@@ -18,6 +18,8 @@ if ENV["RAILS_SOURCE"]
18
18
  gemspec path: ENV["RAILS_SOURCE"]
19
19
  elsif ENV["RAILS_BRANCH"]
20
20
  gem "rails", github: "rails/rails", branch: ENV["RAILS_BRANCH"]
21
+ elsif ENV["RAILS_COMMIT"]
22
+ gem "rails", github: "rails/rails", ref: ENV["RAILS_COMMIT"]
21
23
  else
22
24
  # Need to get rails source because the gem doesn't include tests
23
25
  version = ENV["RAILS_VERSION"] || begin
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # ActiveRecord SQL Server Adapter. For SQL Server 2012 And Higher.
1
+ # ActiveRecord SQL Server Adapter
2
2
 
3
3
  * [![CI](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/actions/workflows/ci.yml/badge.svg)](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/actions/workflows/ci.yml) - CI
4
4
  * [![Gem Version](http://img.shields.io/gem/v/activerecord-sqlserver-adapter.svg)](https://rubygems.org/gems/activerecord-sqlserver-adapter) - Gem Version
@@ -8,15 +8,18 @@
8
8
 
9
9
  The SQL Server adapter for ActiveRecord using SQL Server 2012 or higher.
10
10
 
11
- Interested in older versions? We follow a rational versioning policy that tracks Rails. That means that our 7.x version
12
- of the adapter is only for the latest 7.x version of Rails. If you need the adapter for SQL Server 2008 or 2005, you
13
- are still in the right spot. Just install the latest 3.2.x to 4.1.x version of the adapter that matches your Rails
14
- version. We also have stable branches for each major/minor release of ActiveRecord. For older versions, please check
15
- their stable branches.
11
+ We follow a rational versioning policy that tracks Rails. That means that our 7.x version of the adapter is only
12
+ for the latest 7.x version of Rails. We also have stable branches for each major/minor release of ActiveRecord.
13
+
14
+ We support the versions of the adapter that are in the Rails [Bug Fixes](https://rubyonrails.org/maintenance)
15
+ maintenance group.
16
+
17
+ See [Rubygems](https://rubygems.org/gems/activerecord-sqlserver-adapter/versions) for the latest version of the adapter for each Rails release.
16
18
 
17
19
  | Adapter Version | Rails Version | Support | Branch |
18
20
  |-----------------|---------------|----------------|-------------------------------------------------------------------------------------------------|
19
- | `7.2.x` | `7.2.x` | Active | [main](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/main) |
21
+ | `8.0.0` | `8.0.x` | Active | [main](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/main) |
22
+ | `7.2.x` | `7.2.x` | Active | [7-2-stable](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/7-2-stable) |
20
23
  | `7.1.x` | `7.1.x` | Active | [7-1-stable](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/7-1-stable) |
21
24
  | `7.0.x` | `7.0.x` | Ended | [7-0-stable](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/7-0-stable) |
22
25
  | `6.1.x` | `6.1.x` | Ended | [6-1-stable](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/6-1-stable) |
@@ -26,7 +29,6 @@ their stable branches.
26
29
  | `4.2.x` | `4.2.x` | Ended | [4-2-stable](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/4-2-stable) |
27
30
  | `4.1.x` | `4.1.x` | Ended | [4-1-stable](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/4-1-stable) |
28
31
 
29
- See [Rubygems](https://rubygems.org/gems/activerecord-sqlserver-adapter/versions) for the latest version of the adapter for each Rails release.
30
32
 
31
33
  #### Native Data Type Support
32
34
 
@@ -103,16 +105,14 @@ configuration then implement the `configure_connection` method in an initializer
103
105
  example we are setting the `TEXTSIZE` to 64 megabytes.
104
106
 
105
107
  ```ruby
106
- module ActiveRecord
107
- module ConnectionAdapters
108
- class SQLServerAdapter < AbstractAdapter
109
- def configure_connection
110
- super
111
- @raw_connection.execute("SET TEXTSIZE #{64.megabytes}").do
112
- end
108
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.prepend(
109
+ Module.new do
110
+ def configure_connection
111
+ super
112
+ @raw_connection.execute("SET TEXTSIZE #{64.megabytes}").do
113
113
  end
114
114
  end
115
- end
115
+ )
116
116
  ```
117
117
 
118
118
  #### Configure Application Name
@@ -262,7 +262,6 @@ Many many people have contributed. If you do not see your name here and it shoul
262
262
 
263
263
  You can see an up-to-date list of contributors here: http://github.com/rails-sqlserver/activerecord-sqlserver-adapter/contributors
264
264
 
265
-
266
265
  ## License
267
266
 
268
267
  ActiveRecord SQL Server Adapter is released under the [MIT License](https://opensource.org/licenses/MIT).
data/VERSION CHANGED
@@ -1 +1 @@
1
- 7.2.1
1
+ 8.0.0
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
7
7
  spec.platform = Gem::Platform::RUBY
8
8
  spec.version = version
9
9
 
10
- spec.required_ruby_version = ">= 3.1.0"
10
+ spec.required_ruby_version = ">= 3.2.0"
11
11
 
12
12
  spec.license = "MIT"
13
13
  spec.authors = ["Ken Collins", "Anna Carey", "Will Bond", "Murray Steele", "Shawn Balestracci", "Joe Rafaniello", "Tom Ward", "Aidan Haran"]
@@ -27,6 +27,6 @@ Gem::Specification.new do |spec|
27
27
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
28
28
  spec.require_paths = ["lib"]
29
29
 
30
- spec.add_dependency "activerecord", "~> 7.2.0"
30
+ spec.add_dependency "activerecord", "~> 8.0.0"
31
31
  spec.add_dependency "tiny_tds"
32
32
  end
@@ -5,7 +5,6 @@ services:
5
5
  ci:
6
6
  environment:
7
7
  - ACTIVERECORD_UNITTEST_HOST=sqlserver
8
- - RAILS_BRANCH=7-2-stable
9
8
  build:
10
9
  context: .
11
10
  dockerfile: Dockerfile.ci
@@ -11,7 +11,7 @@ module ActiveRecord
11
11
  private
12
12
 
13
13
  def construct_relation_for_exists(conditions)
14
- klass.with_connection do |connection|
14
+ model.with_connection do |connection|
15
15
  if connection.sqlserver?
16
16
  _construct_relation_for_exists(conditions)
17
17
  else
@@ -13,50 +13,42 @@ module ActiveRecord
13
13
  !READ_QUERY.match?(sql.b)
14
14
  end
15
15
 
16
- def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
17
- log(sql, name, async: async) do |notification_payload|
18
- with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
19
- result = if id_insert_table_name = query_requires_identity_insert?(sql)
20
- with_identity_insert_enabled(id_insert_table_name, conn) { internal_raw_execute(sql, conn, perform_do: true) }
21
- else
22
- internal_raw_execute(sql, conn, perform_do: true)
23
- end
24
- verified!
25
- notification_payload[:row_count] = result
26
- result
27
- end
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
+ # If the table name is a view, we need to get the base table name for enabling identity insert.
19
+ id_insert_table_name = view_table_name(id_insert_table_name) if view_exists?(id_insert_table_name)
20
+
21
+ with_identity_insert_enabled(id_insert_table_name, raw_connection) do
22
+ internal_exec_sql_query(sql, raw_connection)
23
+ end
24
+ else
25
+ internal_exec_sql_query(sql, raw_connection)
26
+ end
27
+
28
+ verified!
29
+ notification_payload[:row_count] = result.count
30
+ result
31
+ end
32
+
33
+ def cast_result(raw_result)
34
+ if raw_result.columns.empty?
35
+ ActiveRecord::Result.empty
36
+ else
37
+ ActiveRecord::Result.new(raw_result.columns, raw_result.rows)
28
38
  end
29
39
  end
30
40
 
31
- def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false)
32
- sql = transform_query(sql)
33
-
34
- check_if_write_query(sql)
35
- mark_transaction_written_if_write(sql)
41
+ def affected_rows(raw_result)
42
+ raw_result.first['AffectedRows']
43
+ end
36
44
 
37
- unless without_prepared_statement?(binds)
45
+ def raw_execute(sql, name = nil, binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true, batch: false)
46
+ unless binds.nil? || binds.empty?
38
47
  types, params = sp_executesql_types_and_parameters(binds)
39
48
  sql = sp_executesql_sql(sql, types, params, name)
40
49
  end
41
50
 
42
- log(sql, name, binds, async: async) do |notification_payload|
43
- with_raw_connection do |conn|
44
- result = if id_insert_table_name = query_requires_identity_insert?(sql)
45
- # If the table name is a view, we need to get the base table name for enabling identity insert.
46
- id_insert_table_name = view_table_name(id_insert_table_name) if view_exists?(id_insert_table_name)
47
-
48
- with_identity_insert_enabled(id_insert_table_name, conn) do
49
- internal_exec_sql_query(sql, conn)
50
- end
51
- else
52
- internal_exec_sql_query(sql, conn)
53
- end
54
-
55
- verified!
56
- notification_payload[:row_count] = result.count
57
- result
58
- end
59
- end
51
+ super
60
52
  end
61
53
 
62
54
  def internal_exec_sql_query(sql, conn)
@@ -66,14 +58,14 @@ module ActiveRecord
66
58
  finish_statement_handle(handle)
67
59
  end
68
60
 
69
- def exec_delete(sql, name, binds)
61
+ def exec_delete(sql, name = nil, binds = [])
70
62
  sql = sql.dup << "; SELECT @@ROWCOUNT AS AffectedRows"
71
- super(sql, name, binds).rows.first.first
63
+ super(sql, name, binds)
72
64
  end
73
65
 
74
- def exec_update(sql, name, binds)
66
+ def exec_update(sql, name = nil, binds = [])
75
67
  sql = sql.dup << "; SELECT @@ROWCOUNT AS AffectedRows"
76
- super(sql, name, binds).rows.first.first
68
+ super(sql, name, binds)
77
69
  end
78
70
 
79
71
  def begin_db_transaction
@@ -156,7 +148,7 @@ module ActiveRecord
156
148
  returning_sql = if returning.is_a?(String)
157
149
  returning
158
150
  else
159
- returning.map { |column| "INSERTED.#{quote_column_name(column)}" }.join(", ")
151
+ Array(returning).map { |column| "INSERTED.#{quote_column_name(column)}" }.join(", ")
160
152
  end
161
153
  sql << " OUTPUT #{returning_sql}"
162
154
  end
@@ -332,14 +324,17 @@ module ActiveRecord
332
324
 
333
325
  def sp_executesql_sql_type(attr)
334
326
  if attr.respond_to?(:type)
335
- return attr.type.sqlserver_type if attr.type.respond_to?(:sqlserver_type)
327
+ type = attr.type.is_a?(ActiveRecord::Normalization::NormalizedValueType) ? attr.type.cast_type : attr.type
328
+ type = type.subtype if type.serialized?
329
+
330
+ return type.sqlserver_type if type.respond_to?(:sqlserver_type)
336
331
 
337
- if attr.type.is_a?(ActiveRecord::Encryption::EncryptedAttributeType) && attr.type.instance_variable_get(:@cast_type).respond_to?(:sqlserver_type)
338
- return attr.type.instance_variable_get(:@cast_type).sqlserver_type
332
+ if type.is_a?(ActiveRecord::Encryption::EncryptedAttributeType) && type.instance_variable_get(:@cast_type).respond_to?(:sqlserver_type)
333
+ return type.instance_variable_get(:@cast_type).sqlserver_type
339
334
  end
340
335
  end
341
336
 
342
- value = basic_attribute_type?(attr) ? attr : attr.value_for_database
337
+ value = active_model_attribute?(attr) ? attr.value_for_database : attr
343
338
 
344
339
  if value.is_a?(Numeric)
345
340
  value > 2_147_483_647 ? "bigint".freeze : "int".freeze
@@ -349,7 +344,7 @@ module ActiveRecord
349
344
  end
350
345
 
351
346
  def sp_executesql_sql_param(attr)
352
- return quote(attr) if basic_attribute_type?(attr)
347
+ return quote(attr) unless active_model_attribute?(attr)
353
348
 
354
349
  case value = attr.value_for_database
355
350
  when Type::Binary::Data, ActiveRecord::Type::SQLServer::Data
@@ -359,14 +354,8 @@ module ActiveRecord
359
354
  end
360
355
  end
361
356
 
362
- def basic_attribute_type?(type)
363
- type.is_a?(Symbol) ||
364
- type.is_a?(String) ||
365
- type.is_a?(Numeric) ||
366
- type.is_a?(Time) ||
367
- type.is_a?(TrueClass) ||
368
- type.is_a?(FalseClass) ||
369
- type.is_a?(NilClass)
357
+ def active_model_attribute?(type)
358
+ type.is_a?(::ActiveModel::Attribute)
370
359
  end
371
360
 
372
361
  def sp_executesql_sql(sql, types, params, name)
@@ -381,6 +370,7 @@ module ActiveRecord
381
370
  sql = "EXEC sp_executesql #{quote(sql)}"
382
371
  sql += ", #{types}, #{params}" unless params.empty?
383
372
  end
373
+
384
374
  sql.freeze
385
375
  end
386
376
 
@@ -458,10 +448,9 @@ module ActiveRecord
458
448
  # TinyTDS returns false instead of raising an exception if connection fails.
459
449
  # Getting around this by raising an exception ourselves while PR
460
450
  # https://github.com/rails-sqlserver/tiny_tds/pull/469 is not released.
461
- def internal_raw_execute(sql, conn, perform_do: false)
462
- result = conn.execute(sql).tap do |_result|
463
- raise TinyTds::Error, "failed to execute statement" if _result.is_a?(FalseClass)
464
- end
451
+ def internal_raw_execute(sql, raw_connection, perform_do: false)
452
+ result = raw_connection.execute(sql)
453
+ raise TinyTds::Error, "failed to execute statement" if result.is_a?(FalseClass)
465
454
 
466
455
  perform_do ? result.do : result
467
456
  end
@@ -14,22 +14,24 @@ module ActiveRecord
14
14
  res
15
15
  end
16
16
 
17
- def drop_table(table_name, **options)
18
- # Mimic CASCADE option as best we can.
19
- if options[:force] == :cascade
20
- execute_procedure(:sp_fkeys, pktable_name: table_name).each do |fkdata|
21
- fktable = fkdata["FKTABLE_NAME"]
22
- fkcolmn = fkdata["FKCOLUMN_NAME"]
23
- pktable = fkdata["PKTABLE_NAME"]
24
- pkcolmn = fkdata["PKCOLUMN_NAME"]
25
- remove_foreign_key fktable, name: fkdata["FK_NAME"]
26
- execute "DELETE FROM #{quote_table_name(fktable)} WHERE #{quote_column_name(fkcolmn)} IN ( SELECT #{quote_column_name(pkcolmn)} FROM #{quote_table_name(pktable)} )"
17
+ def drop_table(*table_names, **options)
18
+ table_names.each do |table_name|
19
+ # Mimic CASCADE option as best we can.
20
+ if options[:force] == :cascade
21
+ execute_procedure(:sp_fkeys, pktable_name: table_name).each do |fkdata|
22
+ fktable = fkdata["FKTABLE_NAME"]
23
+ fkcolmn = fkdata["FKCOLUMN_NAME"]
24
+ pktable = fkdata["PKTABLE_NAME"]
25
+ pkcolmn = fkdata["PKCOLUMN_NAME"]
26
+ remove_foreign_key fktable, name: fkdata["FK_NAME"]
27
+ execute "DELETE FROM #{quote_table_name(fktable)} WHERE #{quote_column_name(fkcolmn)} IN ( SELECT #{quote_column_name(pkcolmn)} FROM #{quote_table_name(pktable)} )"
28
+ end
29
+ end
30
+ if options[:if_exists] && version_year < 2016
31
+ execute "IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = #{quote(table_name)}) DROP TABLE #{quote_table_name(table_name)}", "SCHEMA"
32
+ else
33
+ super
27
34
  end
28
- end
29
- if options[:if_exists] && version_year < 2016
30
- execute "IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = #{quote(table_name)}) DROP TABLE #{quote_table_name(table_name)}", "SCHEMA"
31
- else
32
- super
33
35
  end
34
36
  end
35
37
 
@@ -37,18 +39,16 @@ module ActiveRecord
37
39
  data = select("EXEC sp_helpindex #{quote(table_name)}", "SCHEMA") rescue []
38
40
 
39
41
  data.reduce([]) do |indexes, index|
40
- index = index.with_indifferent_access
41
-
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/)
45
+ name = index['index_name']
46
+ unique = index['index_description'].match?(/unique/)
47
47
  where = select_value("SELECT [filter_definition] FROM sys.indexes WHERE name = #{quote(name)}", "SCHEMA")
48
48
  orders = {}
49
49
  columns = []
50
50
 
51
- index[:index_keys].split(",").each do |column|
51
+ index['index_keys'].split(",").each do |column|
52
52
  column.strip!
53
53
 
54
54
  if column.end_with?("(-)")
@@ -291,7 +291,7 @@ module ActiveRecord
291
291
  end
292
292
 
293
293
  def type_to_sql(type, limit: nil, precision: nil, scale: nil, **)
294
- type_limitable = %w(string integer float char nchar varchar nvarchar).include?(type.to_s)
294
+ type_limitable = %w(string integer float char nchar varchar nvarchar binary_basic).include?(type.to_s)
295
295
  limit = nil unless type_limitable
296
296
 
297
297
  case type.to_s
@@ -480,16 +480,15 @@ module ActiveRecord
480
480
  end
481
481
 
482
482
  def column_definitions(table_name)
483
- identifier = database_prefix_identifier(table_name)
484
- database = identifier.fully_qualified_database_quoted
485
- view_exists = view_exists?(table_name)
486
- view_tblnm = view_table_name(table_name) if view_exists
483
+ identifier = database_prefix_identifier(table_name)
484
+ database = identifier.fully_qualified_database_quoted
485
+ view_exists = view_exists?(table_name)
487
486
 
488
487
  if view_exists
489
488
  sql = <<~SQL
490
489
  SELECT LOWER(c.COLUMN_NAME) AS [name], c.COLUMN_DEFAULT AS [default]
491
490
  FROM #{database}.INFORMATION_SCHEMA.COLUMNS c
492
- WHERE c.TABLE_NAME = #{quote(view_tblnm)}
491
+ WHERE c.TABLE_NAME = #{quote(view_table_name(table_name))}
493
492
  SQL
494
493
  results = internal_exec_query(sql, "SCHEMA")
495
494
  default_functions = results.each.with_object({}) { |row, out| out[row["name"]] = row["default"] }.compact
@@ -498,71 +497,93 @@ module ActiveRecord
498
497
  sql = column_definitions_sql(database, identifier)
499
498
 
500
499
  binds = []
501
- nv128 = SQLServer::Type::UnicodeVarchar.new limit: 128
500
+ nv128 = SQLServer::Type::UnicodeVarchar.new(limit: 128)
502
501
  binds << Relation::QueryAttribute.new("TABLE_NAME", identifier.object, nv128)
503
502
  binds << Relation::QueryAttribute.new("TABLE_SCHEMA", identifier.schema, nv128) unless identifier.schema.blank?
503
+
504
504
  results = internal_exec_query(sql, "SCHEMA", binds)
505
+ raise ActiveRecord::StatementInvalid, "Table '#{table_name}' doesn't exist" if results.empty?
505
506
 
506
507
  columns = results.map do |ci|
507
- ci = ci.symbolize_keys
508
- ci[:_type] = ci[:type]
509
- ci[:table_name] = view_tblnm || table_name
510
- ci[:type] = case ci[:type]
511
- when /^bit|image|text|ntext|datetime$/
512
- ci[:type]
513
- when /^datetime2|datetimeoffset$/i
514
- "#{ci[:type]}(#{ci[:datetime_precision]})"
515
- when /^time$/i
516
- "#{ci[:type]}(#{ci[:datetime_precision]})"
517
- when /^numeric|decimal$/i
518
- "#{ci[:type]}(#{ci[:numeric_precision]},#{ci[:numeric_scale]})"
519
- when /^float|real$/i
520
- "#{ci[:type]}"
521
- when /^char|nchar|varchar|nvarchar|binary|varbinary|bigint|int|smallint$/
522
- ci[:length].to_i == -1 ? "#{ci[:type]}(max)" : "#{ci[:type]}(#{ci[:length]})"
523
- else
524
- ci[:type]
525
- end
526
- ci[:default_value],
527
- ci[:default_function] = begin
528
- default = ci[:default_value]
529
- if default.nil? && view_exists
530
- view_column = views_real_column_name(table_name, ci[:name]).downcase
531
- default = default_functions[view_column] if view_column.present?
532
- end
533
- case default
534
- when nil
535
- [nil, nil]
536
- when /\A\((\w+\(\))\)\Z/
537
- default_function = Regexp.last_match[1]
538
- [nil, default_function]
539
- when /\A\(N'(.*)'\)\Z/m
540
- string_literal = SQLServer::Utils.unquote_string(Regexp.last_match[1])
541
- [string_literal, nil]
542
- when /CREATE DEFAULT/mi
543
- [nil, nil]
544
- else
545
- type = case ci[:type]
546
- when /smallint|int|bigint/ then ci[:_type]
547
- else ci[:type]
548
- end
549
- value = default.match(/\A\((.*)\)\Z/m)[1]
550
- value = select_value("SELECT CAST(#{value} AS #{type}) AS value", "SCHEMA")
551
- [value, nil]
552
- end
508
+ col = {
509
+ name: ci["name"],
510
+ numeric_scale: ci["numeric_scale"],
511
+ numeric_precision: ci["numeric_precision"],
512
+ datetime_precision: ci["datetime_precision"],
513
+ collation: ci["collation"],
514
+ ordinal_position: ci["ordinal_position"],
515
+ length: ci["length"]
516
+ }
517
+
518
+ col[:table_name] = view_exists ? view_table_name(table_name) : table_name
519
+ col[:type] = column_type(ci: ci)
520
+ col[:default_value], col[:default_function] = default_value_and_function(default: ci['default_value'],
521
+ name: ci['name'],
522
+ type: col[:type],
523
+ original_type: ci['type'],
524
+ view_exists: view_exists,
525
+ table_name: table_name,
526
+ default_functions: default_functions)
527
+
528
+ col[:null] = ci['is_nullable'].to_i == 1
529
+ col[:is_primary] = ci['is_primary'].to_i == 1
530
+
531
+ if [true, false].include?(ci['is_identity'])
532
+ col[:is_identity] = ci['is_identity']
533
+ else
534
+ col[:is_identity] = ci['is_identity'].to_i == 1
553
535
  end
554
- ci[:null] = ci[:is_nullable].to_i == 1
555
- ci.delete(:is_nullable)
556
- ci[:is_primary] = ci[:is_primary].to_i == 1
557
- ci[:is_identity] = ci[:is_identity].to_i == 1 unless [TrueClass, FalseClass].include?(ci[:is_identity].class)
558
- ci
536
+
537
+ col
538
+ end
539
+
540
+ columns
541
+ end
542
+
543
+ def default_value_and_function(default:, name:, type:, original_type:, view_exists:, table_name:, default_functions:)
544
+ if default.nil? && view_exists
545
+ view_column = views_real_column_name(table_name, name).downcase
546
+ default = default_functions[view_column] if view_column.present?
559
547
  end
560
548
 
561
- # Since Rails 7, it's expected that all adapter raise error when table doesn't exists.
562
- # I'm not aware of the possibility of tables without columns on SQL Server (postgres have those).
563
- # Raise error if the method return an empty array
564
- columns.tap do |result|
565
- raise ActiveRecord::StatementInvalid, "Table '#{table_name}' doesn't exist" if result.empty?
549
+ case default
550
+ when nil
551
+ [nil, nil]
552
+ when /\A\((\w+\(\))\)\Z/
553
+ default_function = Regexp.last_match[1]
554
+ [nil, default_function]
555
+ when /\A\(N'(.*)'\)\Z/m
556
+ string_literal = SQLServer::Utils.unquote_string(Regexp.last_match[1])
557
+ [string_literal, nil]
558
+ when /CREATE DEFAULT/mi
559
+ [nil, nil]
560
+ else
561
+ type = case type
562
+ when /smallint|int|bigint/ then original_type
563
+ else type
564
+ end
565
+ value = default.match(/\A\((.*)\)\Z/m)[1]
566
+ value = select_value("SELECT CAST(#{value} AS #{type}) AS value", "SCHEMA")
567
+ [value, nil]
568
+ end
569
+ end
570
+
571
+ def column_type(ci:)
572
+ case ci['type']
573
+ when /^bit|image|text|ntext|datetime$/
574
+ ci['type']
575
+ when /^datetime2|datetimeoffset$/i
576
+ "#{ci['type']}(#{ci['datetime_precision']})"
577
+ when /^time$/i
578
+ "#{ci['type']}(#{ci['datetime_precision']})"
579
+ when /^numeric|decimal$/i
580
+ "#{ci['type']}(#{ci['numeric_precision']},#{ci['numeric_scale']})"
581
+ when /^float|real$/i
582
+ "#{ci['type']}"
583
+ when /^char|nchar|varchar|nvarchar|binary|varbinary|bigint|int|smallint$/
584
+ ci['length'].to_i == -1 ? "#{ci['type']}(max)" : "#{ci['type']}(#{ci['length']})"
585
+ else
586
+ ci['type']
566
587
  end
567
588
  end
568
589
 
@@ -681,6 +702,7 @@ module ActiveRecord
681
702
  s.split(/INSERT INTO/i)[1]
682
703
  .split(/OUTPUT INSERTED/i)[0]
683
704
  .split(/(DEFAULT)?\s+VALUES/i)[0]
705
+ .split(/\bSELECT\b(?![^\[]*\])/i)[0]
684
706
  .match(/\s*([^(]*)/i)[0]
685
707
  elsif s.match?(/^\s*UPDATE\s+.*/i)
686
708
  s.match(/UPDATE\s+([^\(\s]+)\s*/i)[1]
@@ -701,25 +723,26 @@ module ActiveRecord
701
723
 
702
724
  def view_table_name(table_name)
703
725
  view_info = view_information(table_name)
704
- view_info ? get_table_name(view_info["VIEW_DEFINITION"]) : table_name
726
+ view_info.present? ? get_table_name(view_info["VIEW_DEFINITION"]) : table_name
705
727
  end
706
728
 
707
729
  def view_information(table_name)
708
730
  @view_information ||= {}
731
+
709
732
  @view_information[table_name] ||= begin
710
733
  identifier = SQLServer::Utils.extract_identifiers(table_name)
711
734
  information_query_table = identifier.database.present? ? "[#{identifier.database}].[INFORMATION_SCHEMA].[VIEWS]" : "[INFORMATION_SCHEMA].[VIEWS]"
712
- view_info = select_one "SELECT * FROM #{information_query_table} WITH (NOLOCK) WHERE TABLE_NAME = #{quote(identifier.object)}", "SCHEMA"
713
-
714
- if view_info
715
- view_info = view_info.with_indifferent_access
716
- if view_info[:VIEW_DEFINITION].blank? || view_info[:VIEW_DEFINITION].length == 4000
717
- view_info[:VIEW_DEFINITION] = begin
718
- select_values("EXEC sp_helptext #{identifier.object_quoted}", "SCHEMA").join
719
- rescue
720
- warn "No view definition found, possible permissions problem.\nPlease run GRANT VIEW DEFINITION TO your_user;"
721
- nil
722
- end
735
+
736
+ view_info = select_one("SELECT * FROM #{information_query_table} WITH (NOLOCK) WHERE TABLE_NAME = #{quote(identifier.object)}", "SCHEMA").to_h
737
+
738
+ if view_info.present?
739
+ if view_info['VIEW_DEFINITION'].blank? || view_info['VIEW_DEFINITION'].length == 4000
740
+ view_info['VIEW_DEFINITION'] = begin
741
+ select_values("EXEC sp_helptext #{identifier.object_quoted}", "SCHEMA").join
742
+ rescue
743
+ warn "No view definition found, possible permissions problem.\nPlease run GRANT VIEW DEFINITION TO your_user;"
744
+ nil
745
+ end
723
746
  end
724
747
  end
725
748
 
@@ -728,8 +751,8 @@ module ActiveRecord
728
751
  end
729
752
 
730
753
  def views_real_column_name(table_name, column_name)
731
- view_definition = view_information(table_name)[:VIEW_DEFINITION]
732
- return column_name unless view_definition
754
+ view_definition = view_information(table_name)['VIEW_DEFINITION']
755
+ return column_name if view_definition.blank?
733
756
 
734
757
  # Remove "CREATE VIEW ... AS SELECT ..." and then match the column name.
735
758
  match_data = view_definition.sub(/CREATE\s+VIEW.*AS\s+SELECT\s/, '').match(/([\w-]*)\s+AS\s+#{column_name}\W/im)
@@ -16,6 +16,7 @@ module ActiveRecord
16
16
  sql = to_sql(arel)
17
17
  result = with_showplan_on { internal_exec_query(sql, "EXPLAIN", binds) }
18
18
  printer = showplan_printer.new(result)
19
+
19
20
  printer.pp
20
21
  end
21
22
 
@@ -36,9 +36,10 @@ module ActiveRecord
36
36
 
37
37
  def cast_value(value)
38
38
  value = super
39
- return if value.blank?
40
39
 
41
- value = value.change year: 2000, month: 01, day: 01
40
+ return value unless value.is_a?(::Time)
41
+
42
+ value = value.change(year: 2000, month: 01, day: 01)
42
43
  apply_seconds_precision(value)
43
44
  end
44
45
 
@@ -4,9 +4,7 @@ require "tiny_tds"
4
4
  require "base64"
5
5
  require "active_record"
6
6
  require "arel_sqlserver"
7
- require "active_record/connection_adapters/abstract_adapter"
8
7
  require "active_record/connection_adapters/sqlserver/core_ext/active_record"
9
- require "active_record/connection_adapters/sqlserver/core_ext/calculations"
10
8
  require "active_record/connection_adapters/sqlserver/core_ext/explain"
11
9
  require "active_record/connection_adapters/sqlserver/core_ext/explain_subscriber"
12
10
  require "active_record/connection_adapters/sqlserver/core_ext/attribute_methods"
@@ -431,36 +429,28 @@ module ActiveRecord
431
429
  TYPE_MAP
432
430
  end
433
431
 
434
- def translate_exception(e, message:, sql:, binds:)
432
+ def translate_exception(exception, message:, sql:, binds:)
435
433
  case message
436
434
  when /(SQL Server client is not connected)|(failed to execute statement)/i
437
- ConnectionNotEstablished.new(message)
435
+ ConnectionNotEstablished.new(message, connection_pool: @pool)
438
436
  when /(cannot insert duplicate key .* with unique index) | (violation of (unique|primary) key constraint)/i
439
- RecordNotUnique.new(message, sql: sql, binds: binds)
437
+ RecordNotUnique.new(message, sql: sql, binds: binds, connection_pool: @pool)
440
438
  when /(conflicted with the foreign key constraint) | (The DELETE statement conflicted with the REFERENCE constraint)/i
441
- InvalidForeignKey.new(message, sql: sql, binds: binds)
439
+ InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
442
440
  when /has been chosen as the deadlock victim/i
443
- DeadlockVictim.new(message, sql: sql, binds: binds)
441
+ DeadlockVictim.new(message, sql: sql, binds: binds, connection_pool: @pool)
444
442
  when /database .* does not exist/i
445
- NoDatabaseError.new(message)
443
+ NoDatabaseError.new(message, connection_pool: @pool)
446
444
  when /data would be truncated/
447
- ValueTooLong.new(message, sql: sql, binds: binds)
445
+ ValueTooLong.new(message, sql: sql, binds: binds, connection_pool: @pool)
448
446
  when /connection timed out/
449
- StatementTimeout.new(message, sql: sql, binds: binds)
447
+ StatementTimeout.new(message, sql: sql, binds: binds, connection_pool: @pool)
450
448
  when /Column '(.*)' is not the same data type as referencing column '(.*)' in foreign key/
451
- pk_id, fk_id = SQLServer::Utils.extract_identifiers($1), SQLServer::Utils.extract_identifiers($2)
452
- MismatchedForeignKey.new(
453
- self,
454
- message: message,
455
- table: fk_id.schema,
456
- foreign_key: fk_id.object,
457
- target_table: pk_id.schema,
458
- primary_key: pk_id.object
459
- )
449
+ MismatchedForeignKey.new(message: message, connection_pool: @pool)
460
450
  when /Cannot insert the value NULL into column.*does not allow nulls/
461
- NotNullViolation.new(message, sql: sql, binds: binds)
451
+ NotNullViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
462
452
  when /Arithmetic overflow error/
463
- RangeError.new(message, sql: sql, binds: binds)
453
+ RangeError.new(message, sql: sql, binds: binds, connection_pool: @pool)
464
454
  else
465
455
  super
466
456
  end
@@ -134,26 +134,24 @@ module Arel
134
134
 
135
135
  def visit_Arel_Nodes_SelectStatement(o, collector)
136
136
  @select_statement = o
137
+ optimizer_hints = nil
137
138
  distinct_One_As_One_Is_So_Not_Fetch o
138
139
  if o.with
139
140
  collector = visit o.with, collector
140
141
  collector << " "
141
142
  end
142
- collector = o.cores.inject(collector) { |c, x|
143
- visit_Arel_Nodes_SelectCore(x, c)
144
- }
143
+ collector = o.cores.inject(collector) do |collect, core|
144
+ optimizer_hints = core.optimizer_hints if core.optimizer_hints
145
+ visit_Arel_Nodes_SelectCore(core, collect)
146
+ end
145
147
  collector = visit_Orders_And_Let_Fetch_Happen o, collector
146
148
  collector = visit_Make_Fetch_Happen o, collector
149
+ collector = maybe_visit optimizer_hints, collector
147
150
  collector
148
151
  ensure
149
152
  @select_statement = nil
150
153
  end
151
154
 
152
- def visit_Arel_Nodes_SelectCore(o, collector)
153
- collector = super
154
- maybe_visit o.optimizer_hints, collector
155
- end
156
-
157
155
  def visit_Arel_Nodes_OptimizerHints(o, collector)
158
156
  hints = o.expr.map { |v| sanitize_as_option_clause(v) }.join(", ")
159
157
  collector << "OPTION (#{hints})"
@@ -464,13 +464,13 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
464
464
  assert SSTestStringDefaultsView.lease_connection.data_source_exists?(SSTestStringDefaultsView.table_name)
465
465
  end
466
466
 
467
- # That have more than 4000 chars for their defintion
467
+ # That have more than 4000 chars for their definition
468
468
 
469
- it "cope with null returned for the defintion" do
469
+ it "cope with null returned for the definition" do
470
470
  assert_nothing_raised() { SSTestStringDefaultsBigView.columns }
471
471
  end
472
472
 
473
- it "using alternate view defintion still be able to find real default" do
473
+ it "using alternate view definition still be able to find real default" do
474
474
  assert_equal "null", SSTestStringDefaultsBigView.new.pretend_null,
475
475
  SSTestStringDefaultsBigView.columns_hash["pretend_null"].inspect
476
476
  end
@@ -542,7 +542,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
542
542
  @conn.execute("INSERT INTO [subscribers] ([nick]) VALUES ('aido')")
543
543
 
544
544
  ActiveRecord::Base.while_preventing_writes do
545
- assert_equal 1, @conn.execute("SELECT * FROM [subscribers] WHERE [subscribers].[nick] = 'aido'")
545
+ assert_equal 1, @conn.execute("SELECT * FROM [subscribers] WHERE [subscribers].[nick] = 'aido'").count
546
546
  end
547
547
  end
548
548
  end
@@ -580,4 +580,40 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
580
580
  end
581
581
  end
582
582
  end
583
+
584
+ describe "mismatched foreign keys error" do
585
+ def setup
586
+ @conn = ActiveRecord::Base.lease_connection
587
+ end
588
+
589
+ it 'raises an error when the foreign key is mismatched' do
590
+ error = assert_raises(ActiveRecord::MismatchedForeignKey) do
591
+ @conn.add_reference :engines, :old_car
592
+ @conn.add_foreign_key :engines, :old_cars
593
+ end
594
+
595
+ assert_match(
596
+ %r/Column 'old_cars\.id' is not the same data type as referencing column 'engines\.old_car_id' in foreign key '.*'/,
597
+ error.message
598
+ )
599
+ assert_not_nil error.cause
600
+ assert_equal @conn.pool, error.connection_pool
601
+ ensure
602
+ @conn.execute("ALTER TABLE engines DROP COLUMN old_car_id") rescue nil
603
+ end
604
+ end
605
+
606
+ describe "placeholder conditions" do
607
+ it 'using time placeholder' do
608
+ assert_equal Task.where("starting < ?", Time.now).count, 1
609
+ end
610
+
611
+ it 'using date placeholder' do
612
+ assert_equal Task.where("starting < ?", Date.today).count, 1
613
+ end
614
+
615
+ it 'using date-time placeholder' do
616
+ assert_equal Task.where("starting < ?", DateTime.current).count, 1
617
+ end
618
+ end
583
619
  end
@@ -520,6 +520,9 @@ class CalculationsTest < ActiveRecord::TestCase
520
520
 
521
521
  # SELECT columns must be in the GROUP clause. Since since `ids` only selects the primary key you cannot perform this query in SQL Server.
522
522
  coerce_tests! :test_ids_with_includes_and_non_primary_key_order
523
+
524
+ # To limit the results in SQL Server we use `FETCH NEXT @0 ROWS ONLY` instead of `LIMIT @0`. To use `FETCH NEXT` an order must be provided.
525
+ coerce_tests! :test_no_order_by_when_counting_all
523
526
  end
524
527
 
525
528
  module ActiveRecord
@@ -1337,6 +1340,20 @@ module ActiveRecord
1337
1340
  def test_registering_new_handlers_for_association_coerced
1338
1341
  assert_match %r{#{Regexp.escape(topic_title)} ~ N'rails'}i, Reply.joins(:topic).where(topics: { title: /rails/ }).to_sql
1339
1342
  end
1343
+
1344
+ # Same as original test except string has `N` prefix to indicate unicode string.
1345
+ coerce_tests! :test_registering_new_handlers_for_joins
1346
+ def test_registering_new_handlers_for_joins_coerced
1347
+ Reply.belongs_to :regexp_topic, -> { where(title: /rails/) }, class_name: "Topic", foreign_key: "parent_id"
1348
+
1349
+ assert_match %r{#{Regexp.escape(quote_table_name("regexp_topic.title"))} ~ N'rails'}i, Reply.joins(:regexp_topic).references(Arel.sql("regexp_topic")).to_sql
1350
+ end
1351
+
1352
+ private
1353
+
1354
+ def topic_title
1355
+ Topic.lease_connection.quote_table_name("topics.title")
1356
+ end
1340
1357
  end
1341
1358
  end
1342
1359
 
@@ -2168,17 +2185,6 @@ class EnumTest < ActiveRecord::TestCase
2168
2185
  Book.lease_connection.add_index(:books, [:author_id, :name], unique: true)
2169
2186
  end
2170
2187
 
2171
- # Need to remove index as SQL Server considers NULLs on a unique-index to be equal unlike PostgreSQL/MySQL/SQLite.
2172
- coerce_tests! %r{declare multiple enums at a time}
2173
- test "declare multiple enums at a time coerced" do
2174
- Book.lease_connection.remove_index(:books, column: [:author_id, :name])
2175
-
2176
- send(:'original_declare multiple enums at a time')
2177
- ensure
2178
- Book.where(author_id: nil, name: nil).delete_all
2179
- Book.lease_connection.add_index(:books, [:author_id, :name], unique: true)
2180
- end
2181
-
2182
2188
  # Need to remove index as SQL Server considers NULLs on a unique-index to be equal unlike PostgreSQL/MySQL/SQLite.
2183
2189
  coerce_tests! %r{serializable\? with large number label}
2184
2190
  test "serializable? with large number label coerced" do
@@ -2250,14 +2256,6 @@ class LogSubscriberTest < ActiveRecord::TestCase
2250
2256
  def test_verbose_query_logs_coerced
2251
2257
  original_test_verbose_query_logs
2252
2258
  end
2253
-
2254
- # Bindings logged slightly differently.
2255
- coerce_tests! :test_where_in_binds_logging_include_attribute_names
2256
- def test_where_in_binds_logging_include_attribute_names_coerced
2257
- Developer.where(id: [1, 2, 3, 4, 5]).load
2258
- wait
2259
- assert_match(%{@0 = 1, @1 = 2, @2 = 3, @3 = 4, @4 = 5 [["id", nil], ["id", nil], ["id", nil], ["id", nil], ["id", nil]]}, @logger.logged(:debug).last)
2260
- end
2261
2259
  end
2262
2260
 
2263
2261
  class ReloadModelsTest < ActiveRecord::TestCase
@@ -2419,7 +2417,9 @@ class QueryLogsTest < ActiveRecord::TestCase
2419
2417
  # SQL requires double single-quotes.
2420
2418
  coerce_tests! :test_sql_commenter_format
2421
2419
  def test_sql_commenter_format_coerced
2422
- ActiveRecord::QueryLogs.update_formatter(:sqlcommenter)
2420
+ ActiveRecord::QueryLogs.tags_formatter = :sqlcommenter
2421
+ ActiveRecord::QueryLogs.tags = [:application]
2422
+
2423
2423
  assert_queries_match(%r{/\*application=''active_record''\*/}) do
2424
2424
  Dashboard.first
2425
2425
  end
@@ -2428,7 +2428,7 @@ class QueryLogsTest < ActiveRecord::TestCase
2428
2428
  # SQL requires double single-quotes.
2429
2429
  coerce_tests! :test_sqlcommenter_format_value
2430
2430
  def test_sqlcommenter_format_value_coerced
2431
- ActiveRecord::QueryLogs.update_formatter(:sqlcommenter)
2431
+ ActiveRecord::QueryLogs.tags_formatter = :sqlcommenter
2432
2432
 
2433
2433
  ActiveRecord::QueryLogs.tags = [
2434
2434
  :application,
@@ -2443,7 +2443,7 @@ class QueryLogsTest < ActiveRecord::TestCase
2443
2443
  # SQL requires double single-quotes.
2444
2444
  coerce_tests! :test_sqlcommenter_format_value_string_coercible
2445
2445
  def test_sqlcommenter_format_value_string_coercible_coerced
2446
- ActiveRecord::QueryLogs.update_formatter(:sqlcommenter)
2446
+ ActiveRecord::QueryLogs.tags_formatter = :sqlcommenter
2447
2447
 
2448
2448
  ActiveRecord::QueryLogs.tags = [
2449
2449
  :application,
@@ -2458,7 +2458,7 @@ class QueryLogsTest < ActiveRecord::TestCase
2458
2458
  # SQL requires double single-quotes.
2459
2459
  coerce_tests! :test_sqlcommenter_format_allows_string_keys
2460
2460
  def test_sqlcommenter_format_allows_string_keys_coerced
2461
- ActiveRecord::QueryLogs.update_formatter(:sqlcommenter)
2461
+ ActiveRecord::QueryLogs.tags_formatter = :sqlcommenter
2462
2462
 
2463
2463
  ActiveRecord::QueryLogs.tags = [
2464
2464
  :application,
@@ -2493,6 +2493,17 @@ class InsertAllTest < ActiveRecord::TestCase
2493
2493
  result = Book.insert_all! [{ name: "Rework", author_id: 1 }], returning: Arel.sql("UPPER(INSERTED.name) as name")
2494
2494
  assert_equal %w[ REWORK ], result.pluck("name")
2495
2495
  end
2496
+
2497
+ # Need to remove index as SQL Server considers NULLs on a unique-index to be equal unlike PostgreSQL/MySQL/SQLite.
2498
+ coerce_tests! :test_insert_with_type_casting_and_serialize_is_consistent
2499
+ def test_insert_with_type_casting_and_serialize_is_consistent_coerced
2500
+ connection.remove_index(:books, column: [:author_id, :name])
2501
+
2502
+ original_test_insert_with_type_casting_and_serialize_is_consistent
2503
+ ensure
2504
+ Book.where(author_id: nil, name: '["Array"]').delete_all
2505
+ Book.lease_connection.add_index(:books, [:author_id, :name], unique: true)
2506
+ end
2496
2507
  end
2497
2508
 
2498
2509
  module ActiveRecord
@@ -2655,6 +2666,28 @@ module ActiveRecord
2655
2666
  end
2656
2667
  end
2657
2668
 
2669
+ module ActiveRecord
2670
+ module ConnectionAdapters
2671
+ class RegistrationIsolatedTest < ActiveRecord::TestCase
2672
+ # SQL Server was not included in the list of available adapters in the error message.
2673
+ coerce_tests! %r{resolve raises if the adapter is using the pre 7.2 adapter registration API}
2674
+ def resolve_raises_if_the_adapter_is_using_the_pre_7_2_adapter_registration_API
2675
+ exception = assert_raises(ActiveRecord::AdapterNotFound) do
2676
+ ActiveRecord::ConnectionAdapters.resolve("fake_legacy")
2677
+ end
2678
+
2679
+ assert_equal(
2680
+ "Database configuration specifies nonexistent 'ridiculous' adapter. Available adapters are: abstract, fake, mysql2, postgresql, sqlite3, sqlserver, trilogy. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile if it's not in the list of available adapters.",
2681
+ exception.message
2682
+ )
2683
+ ensure
2684
+ ActiveRecord::ConnectionAdapters.instance_variable_get(:@adapters).delete("fake_legacy")
2685
+ end
2686
+ end
2687
+ end
2688
+ end
2689
+
2690
+
2658
2691
  module ActiveRecord
2659
2692
  class TableMetadataTest < ActiveSupport::TestCase
2660
2693
  # Adapter returns an object that is subclass of what is expected in the original test.
@@ -36,6 +36,14 @@ class OptimizerHitsTestSQLServer < ActiveRecord::TestCase
36
36
  end
37
37
  end
38
38
 
39
+ it "support order" do
40
+ assert_queries_match(%r{\ASELECT .+ FROM .+ ORDER .+ OPTION .+\z}) do
41
+ companies = Company.optimizer_hints("LABEL='FindCompanies'")
42
+ companies = companies.order(:id)
43
+ companies.to_a
44
+ end
45
+ end
46
+
39
47
  it "sanitize values" do
40
48
  assert_queries_match(%r{\ASELECT .+ FROM .+ OPTION \(HASH GROUP\)\z}) do
41
49
  companies = Company.optimizer_hints("OPTION (HASH GROUP)")
@@ -93,39 +93,41 @@ class SchemaDumperTestSQLServer < ActiveRecord::TestCase
93
93
  assert_line :binary_col, type: "binary"
94
94
 
95
95
  # Our type methods.
96
- _(columns["real_col"].sql_type).must_equal "real"
97
- _(columns["money_col"].sql_type).must_equal "money"
98
- _(columns["smalldatetime_col"].sql_type).must_equal "smalldatetime"
99
- _(columns["datetime2_col"].sql_type).must_equal "datetime2(7)"
100
- _(columns["datetimeoffset"].sql_type).must_equal "datetimeoffset(7)"
101
- _(columns["smallmoney_col"].sql_type).must_equal "smallmoney"
102
- _(columns["char_col"].sql_type).must_equal "char(1)"
103
- _(columns["varchar_col"].sql_type).must_equal "varchar(8000)"
104
- _(columns["text_basic_col"].sql_type).must_equal "text"
105
- _(columns["nchar_col"].sql_type).must_equal "nchar(1)"
106
- _(columns["ntext_col"].sql_type).must_equal "ntext"
107
- _(columns["binary_basic_col"].sql_type).must_equal "binary(1)"
108
- _(columns["varbinary_col"].sql_type).must_equal "varbinary(8000)"
109
- _(columns["uuid_col"].sql_type).must_equal "uniqueidentifier"
110
- _(columns["sstimestamp_col"].sql_type).must_equal "timestamp"
111
- _(columns["json_col"].sql_type).must_equal "nvarchar(max)"
112
-
113
- assert_line :real_col, type: "real"
114
- assert_line :money_col, type: "money", precision: 19, scale: 4
115
- assert_line :smalldatetime_col, type: "smalldatetime"
116
- assert_line :datetime2_col, type: "datetime", precision: 7
117
- assert_line :datetimeoffset, type: "datetimeoffset", precision: 7
118
- assert_line :smallmoney_col, type: "smallmoney", precision: 10, scale: 4
119
- assert_line :char_col, type: "char", limit: 1
120
- assert_line :varchar_col, type: "varchar"
121
- assert_line :text_basic_col, type: "text_basic"
122
- assert_line :nchar_col, type: "nchar", limit: 1
123
- assert_line :ntext_col, type: "ntext"
124
- assert_line :binary_basic_col, type: "binary_basic", limit: 1
125
- assert_line :varbinary_col, type: "varbinary"
126
- assert_line :uuid_col, type: "uuid"
127
- assert_line :sstimestamp_col, type: "ss_timestamp", null: false
128
- assert_line :json_col, type: "text"
96
+ _(columns["real_col"].sql_type).must_equal "real"
97
+ _(columns["money_col"].sql_type).must_equal "money"
98
+ _(columns["smalldatetime_col"].sql_type).must_equal "smalldatetime"
99
+ _(columns["datetime2_col"].sql_type).must_equal "datetime2(7)"
100
+ _(columns["datetimeoffset"].sql_type).must_equal "datetimeoffset(7)"
101
+ _(columns["smallmoney_col"].sql_type).must_equal "smallmoney"
102
+ _(columns["char_col"].sql_type).must_equal "char(1)"
103
+ _(columns["varchar_col"].sql_type).must_equal "varchar(8000)"
104
+ _(columns["text_basic_col"].sql_type).must_equal "text"
105
+ _(columns["nchar_col"].sql_type).must_equal "nchar(1)"
106
+ _(columns["ntext_col"].sql_type).must_equal "ntext"
107
+ _(columns["binary_basic_col"].sql_type).must_equal "binary(1)"
108
+ _(columns["binary_basic_16_col"].sql_type).must_equal "binary(16)"
109
+ _(columns["varbinary_col"].sql_type).must_equal "varbinary(8000)"
110
+ _(columns["uuid_col"].sql_type).must_equal "uniqueidentifier"
111
+ _(columns["sstimestamp_col"].sql_type).must_equal "timestamp"
112
+ _(columns["json_col"].sql_type).must_equal "nvarchar(max)"
113
+
114
+ assert_line :real_col, type: "real"
115
+ assert_line :money_col, type: "money", precision: 19, scale: 4
116
+ assert_line :smalldatetime_col, type: "smalldatetime"
117
+ assert_line :datetime2_col, type: "datetime", precision: 7
118
+ assert_line :datetimeoffset, type: "datetimeoffset", precision: 7
119
+ assert_line :smallmoney_col, type: "smallmoney", precision: 10, scale: 4
120
+ assert_line :char_col, type: "char", limit: 1
121
+ assert_line :varchar_col, type: "varchar"
122
+ assert_line :text_basic_col, type: "text_basic"
123
+ assert_line :nchar_col, type: "nchar", limit: 1
124
+ assert_line :ntext_col, type: "ntext"
125
+ assert_line :binary_basic_col, type: "binary_basic", limit: 1
126
+ assert_line :binary_basic_16_col, type: "binary_basic", limit: 16
127
+ assert_line :varbinary_col, type: "varbinary"
128
+ assert_line :uuid_col, type: "uuid"
129
+ assert_line :sstimestamp_col, type: "ss_timestamp", null: false
130
+ assert_line :json_col, type: "text"
129
131
  end
130
132
 
131
133
  it "dump column collation" do
@@ -96,6 +96,10 @@ class SchemaTestSQLServer < ActiveRecord::TestCase
96
96
  it do
97
97
  assert_equal "[test].[aliens]", connection.send(:get_raw_table_name, "EXEC sp_executesql N'INSERT INTO [test].[aliens] ([name]) OUTPUT INSERTED.[id] VALUES (@0)', N'@0 varchar(255)', @0 = 'Trisolarans'")
98
98
  end
99
+
100
+ it do
101
+ assert_equal "[with].[select notation]", connection.send(:get_raw_table_name, "INSERT INTO [with].[select notation] SELECT * FROM [table_name]")
102
+ end
99
103
  end
100
104
  end
101
105
  end
@@ -33,6 +33,7 @@ ActiveRecord::Schema.define do
33
33
  t.nchar :nchar_col
34
34
  t.ntext :ntext_col
35
35
  t.binary_basic :binary_basic_col
36
+ t.binary_basic :binary_basic_16_col, limit: 16
36
37
  t.varbinary :varbinary_col
37
38
  t.uuid :uuid_col
38
39
  t.ss_timestamp :sstimestamp_col
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-sqlserver-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.2.1
4
+ version: 8.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken Collins
@@ -15,7 +15,7 @@ authors:
15
15
  autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
- date: 2024-10-02 00:00:00.000000000 Z
18
+ date: 2024-11-11 00:00:00.000000000 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: activerecord
@@ -23,14 +23,14 @@ dependencies:
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 7.2.0
26
+ version: 8.0.0
27
27
  type: :runtime
28
28
  prerelease: false
29
29
  version_requirements: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 7.2.0
33
+ version: 8.0.0
34
34
  - !ruby/object:Gem::Dependency
35
35
  name: tiny_tds
36
36
  requirement: !ruby/object:Gem::Requirement
@@ -79,7 +79,6 @@ files:
79
79
  - lib/active_record/connection_adapters/sqlserver/core_ext/abstract_adapter.rb
80
80
  - lib/active_record/connection_adapters/sqlserver/core_ext/active_record.rb
81
81
  - lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb
82
- - lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb
83
82
  - lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb
84
83
  - lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb
85
84
  - lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb
@@ -239,8 +238,8 @@ licenses:
239
238
  - MIT
240
239
  metadata:
241
240
  bug_tracker_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues
242
- changelog_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/blob/v7.2.1/CHANGELOG.md
243
- source_code_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/v7.2.1
241
+ changelog_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/blob/v8.0.0/CHANGELOG.md
242
+ source_code_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/v8.0.0
244
243
  post_install_message:
245
244
  rdoc_options: []
246
245
  require_paths:
@@ -249,14 +248,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
249
248
  requirements:
250
249
  - - ">="
251
250
  - !ruby/object:Gem::Version
252
- version: 3.1.0
251
+ version: 3.2.0
253
252
  required_rubygems_version: !ruby/object:Gem::Requirement
254
253
  requirements:
255
254
  - - ">="
256
255
  - !ruby/object:Gem::Version
257
256
  version: '0'
258
257
  requirements: []
259
- rubygems_version: 3.5.17
258
+ rubygems_version: 3.5.21
260
259
  signing_key:
261
260
  specification_version: 4
262
261
  summary: ActiveRecord SQL Server Adapter.
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "active_record/relation"
4
- require "active_record/version"
5
-
6
- module ActiveRecord
7
- module ConnectionAdapters
8
- module SQLServer
9
- module CoreExt
10
- module Calculations
11
-
12
- private
13
-
14
- def build_count_subquery(relation, column_name, distinct)
15
- klass.with_connection do |connection|
16
- relation = relation.unscope(:order) if connection.sqlserver?
17
- super(relation, column_name, distinct)
18
- end
19
- end
20
- end
21
- end
22
- end
23
- end
24
- end
25
-
26
- ActiveSupport.on_load(:active_record) do
27
- mod = ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::Calculations
28
- ActiveRecord::Relation.include(mod)
29
- end