activerecord-sqlserver-adapter 6.0.1 → 6.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +28 -0
  3. data/CHANGELOG.md +24 -0
  4. data/Gemfile +2 -1
  5. data/README.md +19 -1
  6. data/RUNNING_UNIT_TESTS.md +1 -1
  7. data/VERSION +1 -1
  8. data/appveyor.yml +5 -7
  9. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +2 -0
  10. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +4 -0
  11. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +2 -0
  12. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +2 -0
  13. data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +2 -0
  14. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +1 -1
  15. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +4 -4
  16. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +23 -3
  17. data/lib/active_record/connection_adapters/sqlserver/type/decimal_without_scale.rb +22 -0
  18. data/lib/active_record/connection_adapters/sqlserver/type.rb +1 -0
  19. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +17 -2
  20. data/lib/arel/visitors/sqlserver.rb +52 -12
  21. data/test/cases/adapter_test_sqlserver.rb +34 -2
  22. data/test/cases/coerced_tests.rb +179 -34
  23. data/test/cases/column_test_sqlserver.rb +5 -2
  24. data/test/cases/lateral_test_sqlserver.rb +35 -0
  25. data/test/cases/migration_test_sqlserver.rb +44 -0
  26. data/test/cases/optimizer_hints_test_sqlserver.rb +72 -0
  27. data/test/cases/schema_dumper_test_sqlserver.rb +5 -3
  28. metadata +11 -6
  29. data/.travis.yml +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a7160737c2d7b0946054cdf6e9fd7c26144cef8f1bc66be90d0b51b7e9ba91f
4
- data.tar.gz: 67ef07c9e35bd3f4735bed2303f3c63f58d09e1bacb7203339b365457c03d296
3
+ metadata.gz: 6a4ddc7c26331060001ac99b3e2ea5c7d30a97204237c48c383b356280a763ac
4
+ data.tar.gz: c9e1154f442e957e1930fa7cc36addf9b8f465d579d44fcb87fb6a4ffe0426bb
5
5
  SHA512:
6
- metadata.gz: 599f09313bdd68db576bd7282174f04ac9c7e848b2d84b73857e5da9ca5a76427bd22a63bc717e90cd7293747701bdcbcbbc4159e7309bd5328a8be8baa5a245
7
- data.tar.gz: dd54538736ba65dd12ee1ce87aae3da4e324888ca3a9b7a793cae34817451f66ec5b19e0e67882335057e7136f55f1871dad89553f8dfca0824ecdd6c4f34540
6
+ metadata.gz: fc3e2cbdcd6ef7d2f334a4af20a66e9c1f1a4ec28548dc664e879a94e87aa487a0d68c6468182b3a026f3e05aa0c735ce16c774b8b404cb8f7e140188d8c06f0
7
+ data.tar.gz: 5f759150e7c204c5de007a320897b71bfe80913ee78d162dcfa891ab840102089f3798a7641ccaa2fe6ef6a56250deed62877f2b7900983f705a21677985c291
@@ -0,0 +1,28 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ name: Run test suite
8
+ runs-on: ubuntu-latest
9
+
10
+ env:
11
+ COMPOSE_FILE: docker-compose.ci.yml
12
+
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ ruby:
17
+ - 2.7.5
18
+ - 3.0.3
19
+
20
+ steps:
21
+ - name: Checkout code
22
+ uses: actions/checkout@v2
23
+
24
+ - name: Build docker images
25
+ run: docker-compose build --build-arg TARGET_VERSION=${{ matrix.ruby }}
26
+
27
+ - name: Run tests
28
+ run: docker-compose run ci
data/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ ## v6.0.3
2
+
3
+ #### Fixed
4
+
5
+ [#1054](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1054) Conditionally apply SQL Server monkey patches to ActiveRecord so that it is safe to use this gem alongside other database adapters (e.g. PostgreSQL) in a multi-database Rails app
6
+
7
+ ## v6.0.2
8
+
9
+ #### Fixed
10
+
11
+ - [#858](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/858) Allow table existence to be tested across database schemas.
12
+
13
+ #### Changed
14
+
15
+ - [#852](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/852) Updated the column name matchers to accept database and owner names
16
+
17
+ #### Added
18
+
19
+ - [#855](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/855) Add helpers to create/change/drop a schema.
20
+ - [#857](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/857) Included WAITFOR as read query type.
21
+ - [#865](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/865) Implemented optimizer hints.
22
+ - [#845](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/845) Add support for lateral using CROSS/OUTER APPLY.
23
+ - [#870](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/870) Added DecimalWithoutScale type
24
+
1
25
  ## v6.0.1
2
26
 
3
27
  #### Fixed
data/Gemfile CHANGED
@@ -7,9 +7,10 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
7
7
  gemspec
8
8
 
9
9
  gem "bcrypt"
10
- gem "pg", ">= 0.18.0"
10
+ gem "pg", "~> 1.3"
11
11
  gem "sqlite3", "~> 1.4"
12
12
  gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby]
13
+ gem "minitest", ">= 5.15.0", "< 5.16"
13
14
 
14
15
  if ENV["RAILS_SOURCE"]
15
16
  gemspec path: ENV["RAILS_SOURCE"]
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ActiveRecord SQL Server Adapter. For SQL Server 2012 And Higher.
2
2
 
3
- * [![TravisCI](https://travis-ci.org/rails-sqlserver/activerecord-sqlserver-adapter.svg?branch=master)](https://travis-ci.org/rails-sqlserver/activerecord-sqlserver-adapter) - TravisCI
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
  * [![Build Status](https://ci.appveyor.com/api/projects/status/mtgbx8f57vr7k2qa/branch/master?svg=true)](https://ci.appveyor.com/project/rails-sqlserver/activerecord-sqlserver-adapter/branch/master) - Appveyor
5
5
  * [![Gem Version](http://img.shields.io/gem/v/activerecord-sqlserver-adapter.svg)](https://rubygems.org/gems/activerecord-sqlserver-adapter) - Gem Version
6
6
  * [![Gitter chat](https://img.shields.io/badge/%E2%8A%AA%20GITTER%20-JOIN%20CHAT%20%E2%86%92-brightgreen.svg?style=flat)](https://gitter.im/rails-sqlserver/activerecord-sqlserver-adapter) - Community
@@ -52,6 +52,24 @@ Depending on your user and schema setup, it may be needed to use a table name pr
52
52
  ActiveRecord::Base.table_name_prefix = 'dbo.'
53
53
  ```
54
54
 
55
+ It's also possible to create/change/drop a schema in the migration file as in the example below:
56
+
57
+ ```ruby
58
+ class CreateFooSchema < ActiveRecord::Migration[6.0]
59
+ def up
60
+ create_schema('foo')
61
+
62
+ # Or you could move a table to a different schema
63
+
64
+ change_table_schema('foo', 'dbo.admin')
65
+ end
66
+
67
+ def down
68
+ drop_schema('foo')
69
+ end
70
+ end
71
+ ```
72
+
55
73
 
56
74
  #### Configure Connection & App Name
57
75
 
@@ -5,7 +5,7 @@ This process is much easier than it has been before!
5
5
 
6
6
  ## MS SQL SERVER
7
7
 
8
- If you don't have easy access to MS SQL Server, you can set up a Vagrant/VirtualBox virtual machine with MS SQL Server. [Here's how](/https://github.com/rails-sqlserver/activerecord-sqlserver-adapter-dev-box).
8
+ If you don't have easy access to MS SQL Server, you can set up a Vagrant/VirtualBox virtual machine with MS SQL Server. [Here's how](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter-dev-box).
9
9
 
10
10
  ## TL;DR
11
11
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 6.0.1
1
+ 6.0.3
data/appveyor.yml CHANGED
@@ -5,10 +5,10 @@ build: off
5
5
  matrix:
6
6
  fast_finish: true
7
7
  allow_failures:
8
- - ruby_version: "25"
9
- - ruby_version: "26"
10
- - ruby_version: "27"
11
8
  - ruby_version: "27-x64"
9
+ - ruby_version: "27"
10
+ - ruby_version: "30"
11
+ - ruby_version: "30-x64"
12
12
  services:
13
13
  - mssql2014
14
14
 
@@ -38,9 +38,7 @@ environment:
38
38
  CI_AZURE_PASS:
39
39
  secure: cSQp8sk4urJYvq0utpsK+r7J+snJ2wpcdp8RdXJfB+w=
40
40
  matrix:
41
- - ruby_version: "25-x64"
42
- - ruby_version: "25"
43
- - ruby_version: "26-x64"
44
- - ruby_version: "26"
45
41
  - ruby_version: "27-x64"
46
42
  - ruby_version: "27"
43
+ - ruby_version: "30"
44
+ - ruby_version: "30-x64"
@@ -10,6 +10,8 @@ module ActiveRecord
10
10
  private
11
11
 
12
12
  def attributes_for_update(attribute_names)
13
+ return super unless self.class.connection.adapter_name == "SQLServer"
14
+
13
15
  super.reject do |name|
14
16
  column = self.class.columns_hash[name]
15
17
  column && column.respond_to?(:is_identity?) && column.is_identity?
@@ -10,6 +10,8 @@ module ActiveRecord
10
10
  module Calculations
11
11
  # Same as original except we don't perform PostgreSQL hack that removes ordering.
12
12
  def calculate(operation, column_name)
13
+ return super unless klass.connection.adapter_name == "SQLServer"
14
+
13
15
  if has_include?(column_name)
14
16
  relation = apply_join_dependency
15
17
 
@@ -29,6 +31,8 @@ module ActiveRecord
29
31
  private
30
32
 
31
33
  def build_count_subquery(relation, column_name, distinct)
34
+ return super unless klass.connection.adapter_name == "SQLServer"
35
+
32
36
  super(relation.unscope(:order), column_name, distinct)
33
37
  end
34
38
 
@@ -9,6 +9,8 @@ module ActiveRecord
9
9
  SQLSERVER_STATEMENT_REGEXP = /N'(.+)', N'(.+)', (.+)/
10
10
 
11
11
  def exec_explain(queries)
12
+ return super unless connection.adapter_name == "SQLServer"
13
+
12
14
  unprepared_queries = queries.map do |(sql, binds)|
13
15
  [unprepare_sqlserver_statement(sql, binds), binds]
14
16
  end
@@ -12,6 +12,8 @@ module ActiveRecord
12
12
 
13
13
  # Same as original except we order by values in distinct select if present.
14
14
  def construct_relation_for_exists(conditions)
15
+ return super unless klass.connection.adapter_name == "SQLServer"
16
+
15
17
  conditions = sanitize_forbidden_attributes(conditions)
16
18
 
17
19
  if distinct_value && offset_value
@@ -10,6 +10,8 @@ module ActiveRecord
10
10
  private
11
11
 
12
12
  def records_for(ids)
13
+ return super unless klass.connection.adapter_name == "SQLServer"
14
+
13
15
  ids.each_slice(in_clause_length).flat_map do |slice|
14
16
  scope.where(association_key_name => slice).load do |record|
15
17
  # Processing only the first owner
@@ -4,7 +4,7 @@ module ActiveRecord
4
4
  module ConnectionAdapters
5
5
  module SQLServer
6
6
  module DatabaseStatements
7
- READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :dbcc, :explain, :save, :select, :set, :rollback) # :nodoc:
7
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :dbcc, :explain, :save, :select, :set, :rollback, :waitfor) # :nodoc:
8
8
  private_constant :READ_QUERY
9
9
 
10
10
  def write_query?(sql) # :nodoc:
@@ -83,8 +83,8 @@ module ActiveRecord
83
83
  \A
84
84
  (
85
85
  (?:
86
- # [table_name].[column_name] | function(one or no argument)
87
- ((?:\w+\.|\[\w+\]\.)?(?:\w+|\[\w+\])) | \w+\((?:|\g<2>)\)
86
+ # [database_name].[database_owner].[table_name].[column_name] | function(one or no argument)
87
+ ((?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+|\[\w+\])) | \w+\((?:|\g<2>)\)
88
88
  )
89
89
  (?:\s+AS\s+(?:\w+|\[\w+\]))?
90
90
  )
@@ -96,8 +96,8 @@ module ActiveRecord
96
96
  \A
97
97
  (
98
98
  (?:
99
- # [table_name].[column_name] | function(one or no argument)
100
- ((?:\w+\.|\[\w+\]\.)?(?:\w+|\[\w+\])) | \w+\((?:|\g<2>)\)
99
+ # [database_name].[database_owner].[table_name].[column_name] | function(one or no argument)
100
+ ((?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+|\[\w+\])) | \w+\((?:|\g<2>)\)
101
101
  )
102
102
  (?:\s+ASC|\s+DESC)?
103
103
  (?:\s+NULLS\s+(?:FIRST|LAST))?
@@ -281,14 +281,33 @@ module ActiveRecord
281
281
  SQLServer::SchemaDumper.create(self, options)
282
282
  end
283
283
 
284
+ def create_schema(schema_name, authorization = nil)
285
+ sql = "CREATE SCHEMA [#{schema_name}]"
286
+ sql += " AUTHORIZATION [#{authorization}]" if authorization
287
+
288
+ execute sql
289
+ end
290
+
291
+ def change_table_schema(schema_name, table_name)
292
+ execute "ALTER SCHEMA [#{schema_name}] TRANSFER [#{table_name}]"
293
+ end
294
+
295
+ def drop_schema(schema_name)
296
+ execute "DROP SCHEMA [#{schema_name}]"
297
+ end
298
+
284
299
  private
285
300
 
286
301
  def data_source_sql(name = nil, type: nil)
287
302
  scope = quoted_scope name, type: type
288
- table_name = lowercase_schema_reflection_sql "TABLE_NAME"
303
+
304
+ table_name = lowercase_schema_reflection_sql 'TABLE_NAME'
305
+ database = scope[:database].present? ? "#{scope[:database]}." : ""
306
+ table_catalog = scope[:database].present? ? quote(scope[:database]) : "DB_NAME()"
307
+
289
308
  sql = "SELECT #{table_name}"
290
- sql += " FROM INFORMATION_SCHEMA.TABLES WITH (NOLOCK)"
291
- sql += " WHERE TABLE_CATALOG = DB_NAME()"
309
+ sql += " FROM #{database}INFORMATION_SCHEMA.TABLES WITH (NOLOCK)"
310
+ sql += " WHERE TABLE_CATALOG = #{table_catalog}"
292
311
  sql += " AND TABLE_SCHEMA = #{quote(scope[:schema])}"
293
312
  sql += " AND TABLE_NAME = #{quote(scope[:name])}" if scope[:name]
294
313
  sql += " AND TABLE_TYPE = #{quote(scope[:type])}" if scope[:type]
@@ -299,6 +318,7 @@ module ActiveRecord
299
318
  def quoted_scope(name = nil, type: nil)
300
319
  identifier = SQLServer::Utils.extract_identifiers(name)
301
320
  {}.tap do |scope|
321
+ scope[:database] = identifier.database if identifier.database
302
322
  scope[:schema] = identifier.schema || "dbo"
303
323
  scope[:name] = identifier.object if identifier.object
304
324
  scope[:type] = type if type
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module SQLServer
6
+ module Type
7
+ class DecimalWithoutScale < ActiveRecord::Type::DecimalWithoutScale
8
+ def sqlserver_type
9
+ "decimal".yield_self do |type|
10
+ type += "(#{precision.to_i},0)" if precision
11
+ type
12
+ end
13
+ end
14
+
15
+ def type_cast_for_schema(value)
16
+ value.is_a?(BigDecimal) ? value.to_s : value.inspect
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -11,6 +11,7 @@ require "active_record/connection_adapters/sqlserver/type/small_integer"
11
11
  require "active_record/connection_adapters/sqlserver/type/tiny_integer"
12
12
  require "active_record/connection_adapters/sqlserver/type/boolean"
13
13
  require "active_record/connection_adapters/sqlserver/type/decimal"
14
+ require "active_record/connection_adapters/sqlserver/type/decimal_without_scale"
14
15
  require "active_record/connection_adapters/sqlserver/type/money"
15
16
  require "active_record/connection_adapters/sqlserver/type/small_money"
16
17
  # Approximate Numerics
@@ -152,6 +152,10 @@ module ActiveRecord
152
152
  true
153
153
  end
154
154
 
155
+ def supports_optimizer_hints?
156
+ true
157
+ end
158
+
155
159
  def supports_lazy_transactions?
156
160
  true
157
161
  end
@@ -296,6 +300,7 @@ module ActiveRecord
296
300
 
297
301
  def initialize_type_map(m = type_map)
298
302
  m.register_type %r{.*}, SQLServer::Type::UnicodeString.new
303
+
299
304
  # Exact Numerics
300
305
  register_class_with_limit m, "bigint(8)", SQLServer::Type::BigInteger
301
306
  m.alias_type "bigint", "bigint(8)"
@@ -308,16 +313,22 @@ module ActiveRecord
308
313
  m.alias_type "tinyint", "tinyint(1)"
309
314
  m.register_type "bit", SQLServer::Type::Boolean.new
310
315
  m.register_type %r{\Adecimal}i do |sql_type|
311
- scale = extract_scale(sql_type)
316
+ scale = extract_scale(sql_type)
312
317
  precision = extract_precision(sql_type)
313
- SQLServer::Type::Decimal.new precision: precision, scale: scale
318
+ if scale == 0
319
+ SQLServer::Type::DecimalWithoutScale.new(precision: precision)
320
+ else
321
+ SQLServer::Type::Decimal.new(precision: precision, scale: scale)
322
+ end
314
323
  end
315
324
  m.alias_type %r{\Anumeric}i, "decimal"
316
325
  m.register_type "money", SQLServer::Type::Money.new
317
326
  m.register_type "smallmoney", SQLServer::Type::SmallMoney.new
327
+
318
328
  # Approximate Numerics
319
329
  m.register_type "float", SQLServer::Type::Float.new
320
330
  m.register_type "real", SQLServer::Type::Real.new
331
+
321
332
  # Date and Time
322
333
  m.register_type "date", SQLServer::Type::Date.new
323
334
  m.register_type %r{\Adatetime} do |sql_type|
@@ -337,11 +348,13 @@ module ActiveRecord
337
348
  precision = extract_precision(sql_type) || DEFAULT_TIME_PRECISION
338
349
  SQLServer::Type::Time.new precision: precision
339
350
  end
351
+
340
352
  # Character Strings
341
353
  register_class_with_limit m, %r{\Achar}i, SQLServer::Type::Char
342
354
  register_class_with_limit m, %r{\Avarchar}i, SQLServer::Type::Varchar
343
355
  m.register_type "varchar(max)", SQLServer::Type::VarcharMax.new
344
356
  m.register_type "text", SQLServer::Type::Text.new
357
+
345
358
  # Unicode Character Strings
346
359
  register_class_with_limit m, %r{\Anchar}i, SQLServer::Type::UnicodeChar
347
360
  register_class_with_limit m, %r{\Anvarchar}i, SQLServer::Type::UnicodeVarchar
@@ -349,10 +362,12 @@ module ActiveRecord
349
362
  m.register_type "nvarchar(max)", SQLServer::Type::UnicodeVarcharMax.new
350
363
  m.register_type "nvarchar(max)", SQLServer::Type::UnicodeVarcharMax.new
351
364
  m.register_type "ntext", SQLServer::Type::UnicodeText.new
365
+
352
366
  # Binary Strings
353
367
  register_class_with_limit m, %r{\Abinary}i, SQLServer::Type::Binary
354
368
  register_class_with_limit m, %r{\Avarbinary}i, SQLServer::Type::Varbinary
355
369
  m.register_type "varbinary(max)", SQLServer::Type::VarbinaryMax.new
370
+
356
371
  # Other Data Types
357
372
  m.register_type "uniqueidentifier", SQLServer::Type::Uuid.new
358
373
  m.register_type "timestamp", SQLServer::Type::Timestamp.new
@@ -80,6 +80,16 @@ module Arel
80
80
  @select_statement = nil
81
81
  end
82
82
 
83
+ def visit_Arel_Nodes_SelectCore(o, collector)
84
+ collector = super
85
+ maybe_visit o.optimizer_hints, collector
86
+ end
87
+
88
+ def visit_Arel_Nodes_OptimizerHints(o, collector)
89
+ hints = o.expr.map { |v| sanitize_as_option_clause(v) }.join(", ")
90
+ collector << "OPTION (#{hints})"
91
+ end
92
+
83
93
  def visit_Arel_Table o, collector
84
94
  # Apparently, o.engine.connection can actually be a different adapter
85
95
  # than sqlserver. Can be removed if fixed in ActiveRecord. See:
@@ -115,23 +125,33 @@ module Arel
115
125
  end
116
126
 
117
127
  def visit_Arel_Nodes_InnerJoin o, collector
118
- collector << "INNER JOIN "
119
- collector = visit o.left, collector
120
- collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
121
- if o.right
122
- collector << " "
123
- visit(o.right, collector)
128
+ if o.left.is_a?(Arel::Nodes::As) && o.left.left.is_a?(Arel::Nodes::Lateral)
129
+ collector << "CROSS "
130
+ visit o.left, collector
124
131
  else
125
- collector
132
+ collector << "INNER JOIN "
133
+ collector = visit o.left, collector
134
+ collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
135
+ if o.right
136
+ collector << " "
137
+ visit(o.right, collector)
138
+ else
139
+ collector
140
+ end
126
141
  end
127
142
  end
128
143
 
129
144
  def visit_Arel_Nodes_OuterJoin o, collector
130
- collector << "LEFT OUTER JOIN "
131
- collector = visit o.left, collector
132
- collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
133
- collector << " "
134
- visit o.right, collector
145
+ if o.left.is_a?(Arel::Nodes::As) && o.left.left.is_a?(Arel::Nodes::Lateral)
146
+ collector << "OUTER "
147
+ visit o.left, collector
148
+ else
149
+ collector << "LEFT OUTER JOIN "
150
+ collector = visit o.left, collector
151
+ collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
152
+ collector << " "
153
+ visit o.right, collector
154
+ end
135
155
  end
136
156
 
137
157
  def collect_in_clause(left, right, collector)
@@ -144,6 +164,10 @@ module Arel
144
164
  super
145
165
  end
146
166
 
167
+ def collect_optimizer_hints(o, collector)
168
+ collector
169
+ end
170
+
147
171
  # SQLServer ToSql/Visitor (Additions)
148
172
 
149
173
  def visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, options = {}
@@ -174,6 +198,18 @@ module Arel
174
198
  collector
175
199
  end
176
200
 
201
+ def visit_Arel_Nodes_Lateral o, collector
202
+ collector << "APPLY"
203
+ collector << " "
204
+ if o.expr.is_a?(Arel::Nodes::SelectStatement)
205
+ collector << "("
206
+ visit(o.expr, collector)
207
+ collector << ")"
208
+ else
209
+ visit(o.expr, collector)
210
+ end
211
+ end
212
+
177
213
  # SQLServer Helpers
178
214
 
179
215
  def node_value(node)
@@ -247,6 +283,10 @@ module Arel
247
283
 
248
284
  node.orders = [] unless node.offset || node.limit
249
285
  end
286
+
287
+ def sanitize_as_option_clause(value)
288
+ value.gsub(%r{OPTION \s* \( (.+) \)}xi, "\\1")
289
+ end
250
290
  end
251
291
  end
252
292
  end
@@ -6,6 +6,7 @@ require "models/task"
6
6
  require "models/post"
7
7
  require "models/subscriber"
8
8
  require "models/minimalistic"
9
+ require "models/college"
9
10
 
10
11
  class AdapterTestSQLServer < ActiveRecord::TestCase
11
12
  fixtures :tasks
@@ -42,17 +43,48 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
42
43
  assert connection.supports_ddl_transactions?
43
44
  end
44
45
 
45
- it "allow owner table name prefixs like dbo to still allow table exists to return true" do
46
+ it "table exists works if table name prefixed by schema and owner" do
46
47
  begin
47
48
  assert_equal "topics", Topic.table_name
48
49
  assert Topic.table_exists?
50
+
51
+ # Test when owner included in table name.
49
52
  Topic.table_name = "dbo.topics"
50
- assert Topic.table_exists?, "Tasks table name of dbo.topics should return true for exists."
53
+ assert Topic.table_exists?, "Topics table name of 'dbo.topics' should return true for exists."
54
+
55
+ # Test when database and owner included in table name.
56
+ Topic.table_name = "#{ActiveRecord::Base.configurations["arunit"]['database']}.dbo.topics"
57
+ assert Topic.table_exists?, "Topics table name of '[DATABASE].dbo.topics' should return true for exists."
51
58
  ensure
52
59
  Topic.table_name = "topics"
53
60
  end
54
61
  end
55
62
 
63
+ it "test table existence across database schemas" do
64
+ arunit_connection = Topic.connection
65
+ arunit2_connection = College.connection
66
+
67
+ arunit_database = arunit_connection.pool.spec.config[:database]
68
+ arunit2_database = arunit2_connection.pool.spec.config[:database]
69
+
70
+ # Assert that connections use different default databases schemas.
71
+ assert_not_equal arunit_database, arunit2_database
72
+
73
+ # Assert that the Topics table exists when using the Topics connection.
74
+ assert arunit_connection.table_exists?('topics'), 'Topics table exists using table name'
75
+ assert arunit_connection.table_exists?('dbo.topics'), 'Topics table exists using owner and table name'
76
+ assert arunit_connection.table_exists?("#{arunit_database}.dbo.topics"), 'Topics table exists using database, owner and table name'
77
+
78
+ # Assert that the Colleges table exists when using the Colleges connection.
79
+ assert arunit2_connection.table_exists?('colleges'), 'College table exists using table name'
80
+ assert arunit2_connection.table_exists?('dbo.colleges'), 'College table exists using owner and table name'
81
+ assert arunit2_connection.table_exists?("#{arunit2_database}.dbo.colleges"), 'College table exists using database, owner and table name'
82
+
83
+ # Assert that the tables exist when using each others connection.
84
+ assert arunit_connection.table_exists?("#{arunit2_database}.dbo.colleges"), 'Colleges table exists using Topics connection'
85
+ assert arunit2_connection.table_exists?("#{arunit_database}.dbo.topics"), 'Topics table exists using Colleges connection'
86
+ end
87
+
56
88
  it "return true to insert sql query for inserts only" do
57
89
  assert connection.send(:insert_sql?, "INSERT...")
58
90
  assert connection.send(:insert_sql?, "EXEC sp_executesql N'INSERT INTO [fk_test_has_fks] ([fk_id]) VALUES (@0); SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident', N'@0 int', @0 = 0")
@@ -396,39 +396,6 @@ module ActiveRecord
396
396
  end
397
397
 
398
398
  class MigrationTest < ActiveRecord::TestCase
399
- # We do not have do the DecimalWithoutScale type.
400
- coerce_tests! :test_add_table_with_decimals
401
- def test_add_table_with_decimals_coerced
402
- Person.connection.drop_table :big_numbers rescue nil
403
- assert !BigNumber.table_exists?
404
- GiveMeBigNumbers.up
405
- BigNumber.reset_column_information
406
- assert BigNumber.create(
407
- :bank_balance => 1586.43,
408
- :big_bank_balance => BigDecimal("1000234000567.95"),
409
- :world_population => 6000000000,
410
- :my_house_population => 3,
411
- :value_of_e => BigDecimal("2.7182818284590452353602875")
412
- )
413
- b = BigNumber.first
414
- assert_not_nil b
415
- assert_not_nil b.bank_balance
416
- assert_not_nil b.big_bank_balance
417
- assert_not_nil b.world_population
418
- assert_not_nil b.my_house_population
419
- assert_not_nil b.value_of_e
420
- assert_kind_of BigDecimal, b.world_population
421
- assert_equal "6000000000.0", b.world_population.to_s
422
- assert_kind_of Integer, b.my_house_population
423
- assert_equal 3, b.my_house_population
424
- assert_kind_of BigDecimal, b.bank_balance
425
- assert_equal BigDecimal("1586.43"), b.bank_balance
426
- assert_kind_of BigDecimal, b.big_bank_balance
427
- assert_equal BigDecimal("1000234000567.95"), b.big_bank_balance
428
- GiveMeBigNumbers.down
429
- assert_raise(ActiveRecord::StatementInvalid) { BigNumber.first }
430
- end
431
-
432
399
  # For some reason our tests set Rails.@_env which breaks test env switching.
433
400
  coerce_tests! :test_internal_metadata_stores_environment_when_other_data_exists
434
401
  coerce_tests! :test_internal_metadata_stores_environment
@@ -982,6 +949,21 @@ class RelationTest < ActiveRecord::TestCase
982
949
  end
983
950
  end
984
951
 
952
+ module ActiveRecord
953
+ class RelationTest < ActiveRecord::TestCase
954
+ # Skipping this test. SQL Server doesn't support optimizer hint as comments
955
+ coerce_tests! :test_relation_with_optimizer_hints_filters_sql_comment_delimiters
956
+
957
+ coerce_tests! :test_does_not_duplicate_optimizer_hints_on_merge
958
+ def test_does_not_duplicate_optimizer_hints_on_merge_coerced
959
+ escaped_table = Post.connection.quote_table_name("posts")
960
+ expected = "SELECT #{escaped_table}.* FROM #{escaped_table} OPTION (OMGHINT)"
961
+ query = Post.optimizer_hints("OMGHINT").merge(Post.optimizer_hints("OMGHINT")).to_sql
962
+ assert_equal expected, query
963
+ end
964
+ end
965
+ end
966
+
985
967
  require "models/post"
986
968
  class SanitizeTest < ActiveRecord::TestCase
987
969
  # Use nvarchar string (N'') in assert
@@ -1106,7 +1088,8 @@ class YamlSerializationTest < ActiveRecord::TestCase
1106
1088
  coerce_tests! :test_types_of_virtual_columns_are_not_changed_on_round_trip
1107
1089
  def test_types_of_virtual_columns_are_not_changed_on_round_trip_coerced
1108
1090
  author = Author.select("authors.*, 5 as posts_count").first
1109
- dumped = YAML.load(YAML.dump(author))
1091
+ dumped_author = YAML.dump(author)
1092
+ dumped = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(dumped_author) : YAML.load(dumped_author)
1110
1093
  assert_equal 5, author.posts_count
1111
1094
  assert_equal 5, dumped.posts_count
1112
1095
  end
@@ -1225,6 +1208,7 @@ module ActiveRecord
1225
1208
 
1226
1209
  original_test_statement_cache_values_differ
1227
1210
  ensure
1211
+ Book.where(author_id: nil, name: 'my book').delete_all
1228
1212
  Book.connection.add_index(:books, [:author_id, :name], unique: true)
1229
1213
  end
1230
1214
  end
@@ -1243,7 +1227,11 @@ module ActiveRecord
1243
1227
  end
1244
1228
  end
1245
1229
 
1230
+ require "models/post"
1231
+ require "models/comment"
1246
1232
  class UnsafeRawSqlTest < ActiveRecord::TestCase
1233
+ fixtures :posts
1234
+
1247
1235
  # Use LEN() vs length() function.
1248
1236
  coerce_tests! %r{order: always allows Arel}
1249
1237
  test "order: always allows Arel" do
@@ -1273,6 +1261,86 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase
1273
1261
  assert_equal ids_expected, ids_depr
1274
1262
  assert_equal ids_expected, ids_disabled
1275
1263
  end
1264
+
1265
+ test "order: allows string column names that are quoted" do
1266
+ ids_expected = Post.order(Arel.sql("id")).pluck(:id)
1267
+
1268
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("[id]").pluck(:id) }
1269
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("[id]").pluck(:id) }
1270
+
1271
+ assert_equal ids_expected, ids_depr
1272
+ assert_equal ids_expected, ids_disabled
1273
+ end
1274
+
1275
+ test "order: allows string column names that are quoted with table" do
1276
+ ids_expected = Post.order(Arel.sql("id")).pluck(:id)
1277
+
1278
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("[posts].[id]").pluck(:id) }
1279
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("[posts].[id]").pluck(:id) }
1280
+
1281
+ assert_equal ids_expected, ids_depr
1282
+ assert_equal ids_expected, ids_disabled
1283
+ end
1284
+
1285
+ test "order: allows string column names that are quoted with table and user" do
1286
+ ids_expected = Post.order(Arel.sql("id")).pluck(:id)
1287
+
1288
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("[dbo].[posts].[id]").pluck(:id) }
1289
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("[dbo].[posts].[id]").pluck(:id) }
1290
+
1291
+ assert_equal ids_expected, ids_depr
1292
+ assert_equal ids_expected, ids_disabled
1293
+ end
1294
+
1295
+ test "order: allows string column names that are quoted with table, user and database" do
1296
+ ids_expected = Post.order(Arel.sql("id")).pluck(:id)
1297
+
1298
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("[activerecord_unittest].[dbo].[posts].[id]").pluck(:id) }
1299
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("[activerecord_unittest].[dbo].[posts].[id]").pluck(:id) }
1300
+
1301
+ assert_equal ids_expected, ids_depr
1302
+ assert_equal ids_expected, ids_disabled
1303
+ end
1304
+
1305
+ test "pluck: allows string column name that are quoted" do
1306
+ titles_expected = Post.pluck(Arel.sql("title"))
1307
+
1308
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("[title]") }
1309
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("[title]") }
1310
+
1311
+ assert_equal titles_expected, titles_depr
1312
+ assert_equal titles_expected, titles_disabled
1313
+ end
1314
+
1315
+ test "pluck: allows string column name that are quoted with table" do
1316
+ titles_expected = Post.pluck(Arel.sql("title"))
1317
+
1318
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("[posts].[title]") }
1319
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("[posts].[title]") }
1320
+
1321
+ assert_equal titles_expected, titles_depr
1322
+ assert_equal titles_expected, titles_disabled
1323
+ end
1324
+
1325
+ test "pluck: allows string column name that are quoted with table and user" do
1326
+ titles_expected = Post.pluck(Arel.sql("title"))
1327
+
1328
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("[dbo].[posts].[title]") }
1329
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("[dbo].[posts].[title]") }
1330
+
1331
+ assert_equal titles_expected, titles_depr
1332
+ assert_equal titles_expected, titles_disabled
1333
+ end
1334
+
1335
+ test "pluck: allows string column name that are quoted with table, user and database" do
1336
+ titles_expected = Post.pluck(Arel.sql("title"))
1337
+
1338
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("[activerecord_unittest].[dbo].[posts].[title]") }
1339
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("[activerecord_unittest].[dbo].[posts].[title]") }
1340
+
1341
+ assert_equal titles_expected, titles_depr
1342
+ assert_equal titles_expected, titles_disabled
1343
+ end
1276
1344
  end
1277
1345
 
1278
1346
  class ReservedWordTest < ActiveRecord::TestCase
@@ -1333,6 +1401,7 @@ class EnumTest < ActiveRecord::TestCase
1333
1401
 
1334
1402
  send(:'original_enums are distinct per class')
1335
1403
  ensure
1404
+ Book.where(author_id: nil, name: nil).delete_all
1336
1405
  Book.connection.add_index(:books, [:author_id, :name], unique: true)
1337
1406
  end
1338
1407
 
@@ -1343,6 +1412,7 @@ class EnumTest < ActiveRecord::TestCase
1343
1412
 
1344
1413
  send(:'original_creating new objects with enum scopes')
1345
1414
  ensure
1415
+ Book.where(author_id: nil, name: nil).delete_all
1346
1416
  Book.connection.add_index(:books, [:author_id, :name], unique: true)
1347
1417
  end
1348
1418
 
@@ -1353,6 +1423,7 @@ class EnumTest < ActiveRecord::TestCase
1353
1423
 
1354
1424
  send(:'original_enums are inheritable')
1355
1425
  ensure
1426
+ Book.where(author_id: nil, name: nil).delete_all
1356
1427
  Book.connection.add_index(:books, [:author_id, :name], unique: true)
1357
1428
  end
1358
1429
 
@@ -1363,6 +1434,7 @@ class EnumTest < ActiveRecord::TestCase
1363
1434
 
1364
1435
  send(:'original_declare multiple enums at a time')
1365
1436
  ensure
1437
+ Book.where(author_id: nil, name: nil).delete_all
1366
1438
  Book.connection.add_index(:books, [:author_id, :name], unique: true)
1367
1439
  end
1368
1440
  end
@@ -1456,3 +1528,76 @@ class ReloadModelsTest < ActiveRecord::TestCase
1456
1528
  # `activesupport/lib/active_support/testing/isolation.rb` exceeds what Windows can handle.
1457
1529
  coerce_tests! :test_has_one_with_reload if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
1458
1530
  end
1531
+
1532
+ require "models/post"
1533
+ class AnnotateTest < ActiveRecord::TestCase
1534
+ # Same as original coerced test except our SQL starts with `EXEC sp_executesql`.
1535
+ # TODO: Remove coerce after Rails 7 (see https://github.com/rails/rails/pull/42027)
1536
+ coerce_tests! :test_annotate_wraps_content_in_an_inline_comment
1537
+ def test_annotate_wraps_content_in_an_inline_comment_coerced
1538
+ quoted_posts_id, quoted_posts = regexp_escape_table_name("posts.id"), regexp_escape_table_name("posts")
1539
+
1540
+ assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do
1541
+ posts = Post.select(:id).annotate("foo")
1542
+ assert posts.first
1543
+ end
1544
+ end
1545
+
1546
+ # Same as original coerced test except our SQL starts with `EXEC sp_executesql`.
1547
+ # TODO: Remove coerce after Rails 7 (see https://github.com/rails/rails/pull/42027)
1548
+ coerce_tests! :test_annotate_is_sanitized
1549
+ def test_annotate_is_sanitized_coerced
1550
+ quoted_posts_id, quoted_posts = regexp_escape_table_name("posts.id"), regexp_escape_table_name("posts")
1551
+
1552
+ assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* \* /foo/ \* \*/}i) do
1553
+ posts = Post.select(:id).annotate("*/foo/*")
1554
+ assert posts.first
1555
+ end
1556
+
1557
+ assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* \*\* //foo// \*\* \*/}i) do
1558
+ posts = Post.select(:id).annotate("**//foo//**")
1559
+ assert posts.first
1560
+ end
1561
+
1562
+ assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* \* \* //foo// \* \* \*/}i) do
1563
+ posts = Post.select(:id).annotate("* *//foo//* *")
1564
+ assert posts.first
1565
+ end
1566
+
1567
+ assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* \* /foo/ \* \*/ /\* \* /bar \*/}i) do
1568
+ posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar")
1569
+ assert posts.first
1570
+ end
1571
+
1572
+ assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* \+ MAX_EXECUTION_TIME\(1\) \*/}i) do
1573
+ posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)")
1574
+ assert posts.first
1575
+ end
1576
+ end
1577
+ end
1578
+
1579
+ class NestedThroughAssociationsTest < ActiveRecord::TestCase
1580
+ # Same as original but replace order with "order(:id)" to ensure that assert_includes_and_joins_equal doesn't raise
1581
+ # "A column has been specified more than once in the order by list"
1582
+ # Example: original test generate queries like "ORDER BY authors.id, [authors].[id]". We don't support duplicate columns in the order list
1583
+ coerce_tests! :test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload_via_joins, :test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload_via_joins
1584
+ def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload_via_joins_coerced
1585
+ # preload table schemas
1586
+ Author.joins(:category_post_comments).first
1587
+
1588
+ assert_includes_and_joins_equal(
1589
+ Author.where("comments.id" => comments(:does_it_hurt).id).order(:id),
1590
+ [authors(:david), authors(:mary)], :category_post_comments
1591
+ )
1592
+ end
1593
+
1594
+ def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload_via_joins_coerced
1595
+ # preload table schemas
1596
+ Category.joins(:post_comments).first
1597
+
1598
+ assert_includes_and_joins_equal(
1599
+ Category.where("comments.id" => comments(:more_greetings).id).order(:id),
1600
+ [categories(:general), categories(:technology)], :post_comments
1601
+ )
1602
+ end
1603
+ end
@@ -157,13 +157,16 @@ class ColumnTestSQLServer < ActiveRecord::TestCase
157
157
  _(col.default).must_equal BigDecimal("191")
158
158
  _(obj.numeric_18_0).must_equal BigDecimal("191")
159
159
  _(col.default_function).must_be_nil
160
+
160
161
  type = connection.lookup_cast_type_from_column(col)
161
- _(type).must_be_instance_of Type::Decimal
162
+ _(type).must_be_instance_of Type::DecimalWithoutScale
162
163
  _(type.limit).must_be_nil
163
164
  _(type.precision).must_equal 18
164
- _(type.scale).must_equal 0
165
+ _(type.scale).must_be_nil
166
+
165
167
  obj.numeric_18_0 = "192.1"
166
168
  _(obj.numeric_18_0).must_equal BigDecimal("192")
169
+
167
170
  obj.save!
168
171
  _(obj.reload.numeric_18_0).must_equal BigDecimal("192")
169
172
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cases/helper_sqlserver"
4
+ require "models/post"
5
+ require "models/author"
6
+
7
+ class LateralTestSQLServer < ActiveRecord::TestCase
8
+ fixtures :posts, :authors
9
+
10
+ it 'uses OUTER APPLY for OUTER JOIN LATERAL' do
11
+ post = Arel::Table.new(:posts)
12
+ author = Arel::Table.new(:authors)
13
+ subselect = post.project(Arel.star).take(1).where(post[:author_id].eq(author[:id])).where(post[:id].eq(42))
14
+
15
+ one = Arel::Nodes::Quoted.new(1)
16
+ eq = Arel::Nodes::Equality.new(one, one)
17
+
18
+ sql = author.project(Arel.star).where(author[:name].matches("David")).outer_join(subselect.lateral.as("bar")).on(eq).to_sql
19
+ results = ActiveRecord::Base.connection.exec_query sql
20
+ assert_equal sql, "SELECT * FROM [authors] OUTER APPLY (SELECT * FROM [posts] WHERE [posts].[author_id] = [authors].[id] AND [posts].[id] = 42 ORDER BY [posts].[id] ASC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) AS bar WHERE [authors].[name] LIKE N'David'"
21
+ assert_equal results.length, 1
22
+ end
23
+
24
+ it 'uses CROSS APPLY for INNER JOIN LATERAL' do
25
+ post = Arel::Table.new(:posts)
26
+ author = Arel::Table.new(:authors)
27
+ subselect = post.project(Arel.star).take(1).where(post[:author_id].eq(author[:id])).where(post[:id].eq(42))
28
+
29
+ sql = author.project(Arel.star).where(author[:name].matches("David")).join(subselect.lateral.as("bar")).to_sql
30
+ results = ActiveRecord::Base.connection.exec_query sql
31
+
32
+ assert_equal sql, "SELECT * FROM [authors] CROSS APPLY (SELECT * FROM [posts] WHERE [posts].[author_id] = [authors].[id] AND [posts].[id] = 42 ORDER BY [posts].[id] ASC OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY) AS bar WHERE [authors].[name] LIKE N'David'"
33
+ assert_equal results.length, 0
34
+ end
35
+ end
@@ -64,4 +64,48 @@ class MigrationTestSQLServer < ActiveRecord::TestCase
64
64
  assert_nothing_raised { connection.change_column :people, :first_name, :text, null: true, default: nil }
65
65
  end
66
66
  end
67
+
68
+ describe "#create_schema" do
69
+ it "creates a new schema" do
70
+ connection.create_schema("some schema")
71
+
72
+ schemas = connection.exec_query("select name from sys.schemas").to_a
73
+
74
+ assert_includes schemas, { "name" => "some schema" }
75
+ end
76
+
77
+ it "creates a new schema with an owner" do
78
+ connection.create_schema("some schema", :guest)
79
+
80
+ schemas = connection.exec_query("select name, principal_id from sys.schemas").to_a
81
+
82
+ assert_includes schemas, { "name" => "some schema", "principal_id" => 2 }
83
+ end
84
+ end
85
+
86
+ describe "#change_table_schema" do
87
+ before { connection.create_schema("foo") }
88
+
89
+ it "transfer the given table to the given schema" do
90
+ connection.change_table_schema("foo", "orders")
91
+
92
+ assert connection.data_source_exists?("foo.orders")
93
+ end
94
+ end
95
+
96
+ describe "#drop_schema" do
97
+ before { connection.create_schema("some schema") }
98
+
99
+ it "drops a schema" do
100
+ schemas = connection.exec_query("select name from sys.schemas").to_a
101
+
102
+ assert_includes schemas, { "name" => "some schema" }
103
+
104
+ connection.drop_schema("some schema")
105
+
106
+ schemas = connection.exec_query("select name from sys.schemas").to_a
107
+
108
+ refute_includes schemas, { "name" => "some schema" }
109
+ end
110
+ end
67
111
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cases/helper_sqlserver"
4
+ require "models/company"
5
+
6
+ class OptimizerHitsTestSQLServer < ActiveRecord::TestCase
7
+ fixtures :companies
8
+
9
+ it "apply optimizations" do
10
+ assert_sql(%r{\ASELECT .+ FROM .+ OPTION \(HASH GROUP\)\z}) do
11
+ companies = Company.optimizer_hints("HASH GROUP")
12
+ companies = companies.distinct.select("firm_id")
13
+ assert_includes companies.explain, "| Hash Match | Aggregate |"
14
+ end
15
+
16
+ assert_sql(%r{\ASELECT .+ FROM .+ OPTION \(ORDER GROUP\)\z}) do
17
+ companies = Company.optimizer_hints("ORDER GROUP")
18
+ companies = companies.distinct.select("firm_id")
19
+ assert_includes companies.explain, "| Stream Aggregate | Aggregate |"
20
+ end
21
+ end
22
+
23
+ it "apply multiple optimizations" do
24
+ assert_sql(%r{\ASELECT .+ FROM .+ OPTION \(HASH GROUP, FAST 1\)\z}) do
25
+ companies = Company.optimizer_hints("HASH GROUP", "FAST 1")
26
+ companies = companies.distinct.select("firm_id")
27
+ assert_includes companies.explain, "| Hash Match | Flow Distinct |"
28
+ end
29
+ end
30
+
31
+ it "support subqueries" do
32
+ assert_sql(%r{.*'SELECT COUNT\(count_column\) FROM \(SELECT .*\) subquery_for_count OPTION \(MAXDOP 2\)'.*}) do
33
+ companies = Company.optimizer_hints("MAXDOP 2")
34
+ companies = companies.select(:id).where(firm_id: [0, 1]).limit(3)
35
+ assert_equal 3, companies.count
36
+ end
37
+ end
38
+
39
+ it "sanitize values" do
40
+ assert_sql(%r{\ASELECT .+ FROM .+ OPTION \(HASH GROUP\)\z}) do
41
+ companies = Company.optimizer_hints("OPTION (HASH GROUP)")
42
+ companies = companies.distinct.select("firm_id")
43
+ companies.to_a
44
+ end
45
+
46
+ assert_sql(%r{\ASELECT .+ FROM .+ OPTION \(HASH GROUP\)\z}) do
47
+ companies = Company.optimizer_hints("OPTION(HASH GROUP)")
48
+ companies = companies.distinct.select("firm_id")
49
+ companies.to_a
50
+ end
51
+
52
+ assert_sql(%r{\ASELECT .+ FROM .+ OPTION \(TABLE HINT \(\[companies\], INDEX\(1\)\)\)\z}) do
53
+ companies = Company.optimizer_hints("OPTION(TABLE HINT ([companies], INDEX(1)))")
54
+ companies = companies.distinct.select("firm_id")
55
+ companies.to_a
56
+ end
57
+
58
+ assert_sql(%r{\ASELECT .+ FROM .+ OPTION \(HASH GROUP\)\z}) do
59
+ companies = Company.optimizer_hints("Option(HASH GROUP)")
60
+ companies = companies.distinct.select("firm_id")
61
+ companies.to_a
62
+ end
63
+ end
64
+
65
+ it "skip optimization after unscope" do
66
+ assert_sql("SELECT DISTINCT [companies].[firm_id] FROM [companies]") do
67
+ companies = Company.optimizer_hints("HASH GROUP")
68
+ companies = companies.distinct.select("firm_id")
69
+ companies.unscope(:optimizer_hints).load
70
+ end
71
+ end
72
+ end
@@ -16,7 +16,7 @@ class SchemaDumperTestSQLServer < ActiveRecord::TestCase
16
16
  assert_line :tinyint, type: "integer", limit: 1, precision: nil, scale: nil, default: 42
17
17
  assert_line :bit, type: "boolean", limit: nil, precision: nil, scale: nil, default: true
18
18
  assert_line :decimal_9_2, type: "decimal", limit: nil, precision: 9, scale: 2, default: 12345.01
19
- assert_line :numeric_18_0, type: "decimal", limit: nil, precision: 18, scale: 0, default: 191.0
19
+ assert_line :numeric_18_0, type: "decimal", limit: nil, precision: 18, scale: nil, default: 191
20
20
  assert_line :numeric_36_2, type: "decimal", limit: nil, precision: 36, scale: 2, default: 12345678901234567890.01
21
21
  assert_line :money, type: "money", limit: nil, precision: 19, scale: 4, default: 4.2
22
22
  assert_line :smallmoney, type: "smallmoney", limit: nil, precision: 10, scale: 4, default: 4.2
@@ -75,7 +75,7 @@ class SchemaDumperTestSQLServer < ActiveRecord::TestCase
75
75
  assert_line :integer_col, type: "integer", limit: nil, precision: nil, scale: nil, default: nil
76
76
  assert_line :bigint_col, type: "bigint", limit: nil, precision: nil, scale: nil, default: nil
77
77
  assert_line :boolean_col, type: "boolean", limit: nil, precision: nil, scale: nil, default: nil
78
- assert_line :decimal_col, type: "decimal", limit: nil, precision: 18, scale: 0, default: nil
78
+ assert_line :decimal_col, type: "decimal", limit: nil, precision: 18, scale: nil, default: nil
79
79
  assert_line :float_col, type: "float", limit: nil, precision: nil, scale: nil, default: nil
80
80
  assert_line :string_col, type: "string", limit: nil, precision: nil, scale: nil, default: nil
81
81
  assert_line :text_col, type: "text", limit: nil, precision: nil, scale: nil, default: nil
@@ -166,13 +166,15 @@ class SchemaDumperTestSQLServer < ActiveRecord::TestCase
166
166
 
167
167
  def assert_line(column_name, options = {})
168
168
  line = line(column_name)
169
- assert line, "Count not find line with column name: #{column_name.inspect} in schema:\n#{schema}"
169
+ assert line, "Could not find line with column name: #{column_name.inspect} in schema:\n#{schema}"
170
+
170
171
  [:type, :limit, :precision, :scale, :collation, :default].each do |key|
171
172
  next unless options.key?(key)
172
173
 
173
174
  actual = key == :type ? line.send(:type_method) : line.send(key)
174
175
  expected = options[key]
175
176
  message = "#{key.to_s.titleize} of #{expected.inspect} not found in:\n#{line}"
177
+
176
178
  if expected.nil?
177
179
  _(actual).must_be_nil message
178
180
  elsif expected.is_a?(Array)
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: 6.0.1
4
+ version: 6.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken Collins
@@ -14,7 +14,7 @@ authors:
14
14
  autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
- date: 2021-02-16 00:00:00.000000000 Z
17
+ date: 2023-05-23 00:00:00.000000000 Z
18
18
  dependencies:
19
19
  - !ruby/object:Gem::Dependency
20
20
  name: activerecord
@@ -54,9 +54,9 @@ extra_rdoc_files: []
54
54
  files:
55
55
  - ".editorconfig"
56
56
  - ".github/issue_template.md"
57
+ - ".github/workflows/ci.yml"
57
58
  - ".gitignore"
58
59
  - ".rubocop.yml"
59
- - ".travis.yml"
60
60
  - CHANGELOG.md
61
61
  - CODE_OF_CONDUCT.md
62
62
  - Dockerfile.ci
@@ -104,6 +104,7 @@ files:
104
104
  - lib/active_record/connection_adapters/sqlserver/type/datetime2.rb
105
105
  - lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb
106
106
  - lib/active_record/connection_adapters/sqlserver/type/decimal.rb
107
+ - lib/active_record/connection_adapters/sqlserver/type/decimal_without_scale.rb
107
108
  - lib/active_record/connection_adapters/sqlserver/type/float.rb
108
109
  - lib/active_record/connection_adapters/sqlserver/type/integer.rb
109
110
  - lib/active_record/connection_adapters/sqlserver/type/json.rb
@@ -154,7 +155,9 @@ files:
154
155
  - test/cases/in_clause_test_sqlserver.rb
155
156
  - test/cases/index_test_sqlserver.rb
156
157
  - test/cases/json_test_sqlserver.rb
158
+ - test/cases/lateral_test_sqlserver.rb
157
159
  - test/cases/migration_test_sqlserver.rb
160
+ - test/cases/optimizer_hints_test_sqlserver.rb
158
161
  - test/cases/order_test_sqlserver.rb
159
162
  - test/cases/pessimistic_locking_test_sqlserver.rb
160
163
  - test/cases/rake_test_sqlserver.rb
@@ -214,8 +217,8 @@ licenses:
214
217
  - MIT
215
218
  metadata:
216
219
  bug_tracker_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues
217
- changelog_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/blob/v6.0.1/CHANGELOG.md
218
- source_code_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/v6.0.1
220
+ changelog_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/blob/v6.0.3/CHANGELOG.md
221
+ source_code_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/v6.0.3
219
222
  post_install_message:
220
223
  rdoc_options: []
221
224
  require_paths:
@@ -231,7 +234,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
231
234
  - !ruby/object:Gem::Version
232
235
  version: '0'
233
236
  requirements: []
234
- rubygems_version: 3.0.3
237
+ rubygems_version: 3.4.7
235
238
  signing_key:
236
239
  specification_version: 4
237
240
  summary: ActiveRecord SQL Server Adapter.
@@ -253,7 +256,9 @@ test_files:
253
256
  - test/cases/in_clause_test_sqlserver.rb
254
257
  - test/cases/index_test_sqlserver.rb
255
258
  - test/cases/json_test_sqlserver.rb
259
+ - test/cases/lateral_test_sqlserver.rb
256
260
  - test/cases/migration_test_sqlserver.rb
261
+ - test/cases/optimizer_hints_test_sqlserver.rb
257
262
  - test/cases/order_test_sqlserver.rb
258
263
  - test/cases/pessimistic_locking_test_sqlserver.rb
259
264
  - test/cases/rake_test_sqlserver.rb
data/.travis.yml DELETED
@@ -1,23 +0,0 @@
1
- sudo: required
2
- cache: bundler
3
- services:
4
- - docker
5
- env:
6
- global:
7
- - COMPOSE_FILE: docker-compose.ci.yml
8
- before_install:
9
- - sudo rm /usr/local/bin/docker-compose
10
- - sudo curl -L "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
11
- - sudo chmod +x /usr/local/bin/docker-compose
12
- install:
13
- - docker-compose build --build-arg TARGET_VERSION=$TARGET_VERSION
14
- script:
15
- - docker-compose run ci
16
- matrix:
17
- include:
18
- - name: 2.5.8
19
- env: TARGET_VERSION=2.5.8
20
- - name: 2.6.6
21
- env: TARGET_VERSION=2.6.6
22
- - name: 2.7.1
23
- env: TARGET_VERSION=2.7.1