activerecord-sqlserver-adapter 6.0.1 → 6.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a7160737c2d7b0946054cdf6e9fd7c26144cef8f1bc66be90d0b51b7e9ba91f
4
- data.tar.gz: 67ef07c9e35bd3f4735bed2303f3c63f58d09e1bacb7203339b365457c03d296
3
+ metadata.gz: c6cf80abcc5b46383279819d86c83c095b5aabcf4969e84bdb2ca3e101777b3a
4
+ data.tar.gz: e11e7d40bba8a4ce3c2d47e08002c5321f82c25b91fde75caa0f01b37a7f6e70
5
5
  SHA512:
6
- metadata.gz: 599f09313bdd68db576bd7282174f04ac9c7e848b2d84b73857e5da9ca5a76427bd22a63bc717e90cd7293747701bdcbcbbc4159e7309bd5328a8be8baa5a245
7
- data.tar.gz: dd54538736ba65dd12ee1ce87aae3da4e324888ca3a9b7a793cae34817451f66ec5b19e0e67882335057e7136f55f1871dad89553f8dfca0824ecdd6c4f34540
6
+ metadata.gz: b6f04caed7f5cc895fc185e8976d306901dc0563152b8867f40be0319f8572099495ef27bc205aad7733c8aac0e4baf451769cdc46719c2db248f50a802be324
7
+ data.tar.gz: 3c8f05837c093f0d44909d1a26bbc97010f8dc5ab07cd5d48bba94d09624c5fe2e5f21488915931c528207eb726860ae0d59b8ca229f353efe0846ff16f7a955
@@ -0,0 +1,26 @@
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: [2.5.9, 2.6.7, 2.7.3, 3.0.1]
17
+
18
+ steps:
19
+ - name: Checkout code
20
+ uses: actions/checkout@v2
21
+
22
+ - name: Build docker images
23
+ run: docker-compose build --build-arg TARGET_VERSION=${{ matrix.ruby }}
24
+
25
+ - name: Run tests
26
+ run: docker-compose run ci
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## v6.0.2
2
+
3
+ #### Fixed
4
+
5
+ - [#858](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/858) Allow table existence to be tested across database schemas.
6
+
7
+ #### Changed
8
+
9
+ - [#852](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/852) Updated the column name matchers to accept database and owner names
10
+
11
+ #### Added
12
+
13
+ - [#855](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/855) Add helpers to create/change/drop a schema.
14
+ - [#857](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/857) Included WAITFOR as read query type.
15
+ - [#865](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/865) Implemented optimizer hints.
16
+ - [#845](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/845) Add support for lateral using CROSS/OUTER APPLY.
17
+ - [#870](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/870) Added DecimalWithoutScale type
18
+
1
19
  ## v6.0.1
2
20
 
3
21
  #### Fixed
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.2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1243,7 +1225,11 @@ module ActiveRecord
1243
1225
  end
1244
1226
  end
1245
1227
 
1228
+ require "models/post"
1229
+ require "models/comment"
1246
1230
  class UnsafeRawSqlTest < ActiveRecord::TestCase
1231
+ fixtures :posts
1232
+
1247
1233
  # Use LEN() vs length() function.
1248
1234
  coerce_tests! %r{order: always allows Arel}
1249
1235
  test "order: always allows Arel" do
@@ -1273,6 +1259,86 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase
1273
1259
  assert_equal ids_expected, ids_depr
1274
1260
  assert_equal ids_expected, ids_disabled
1275
1261
  end
1262
+
1263
+ test "order: allows string column names that are quoted" do
1264
+ ids_expected = Post.order(Arel.sql("id")).pluck(:id)
1265
+
1266
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("[id]").pluck(:id) }
1267
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("[id]").pluck(:id) }
1268
+
1269
+ assert_equal ids_expected, ids_depr
1270
+ assert_equal ids_expected, ids_disabled
1271
+ end
1272
+
1273
+ test "order: allows string column names that are quoted with table" do
1274
+ ids_expected = Post.order(Arel.sql("id")).pluck(:id)
1275
+
1276
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("[posts].[id]").pluck(:id) }
1277
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("[posts].[id]").pluck(:id) }
1278
+
1279
+ assert_equal ids_expected, ids_depr
1280
+ assert_equal ids_expected, ids_disabled
1281
+ end
1282
+
1283
+ test "order: allows string column names that are quoted with table and user" do
1284
+ ids_expected = Post.order(Arel.sql("id")).pluck(:id)
1285
+
1286
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("[dbo].[posts].[id]").pluck(:id) }
1287
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("[dbo].[posts].[id]").pluck(:id) }
1288
+
1289
+ assert_equal ids_expected, ids_depr
1290
+ assert_equal ids_expected, ids_disabled
1291
+ end
1292
+
1293
+ test "order: allows string column names that are quoted with table, user and database" do
1294
+ ids_expected = Post.order(Arel.sql("id")).pluck(:id)
1295
+
1296
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("[activerecord_unittest].[dbo].[posts].[id]").pluck(:id) }
1297
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("[activerecord_unittest].[dbo].[posts].[id]").pluck(:id) }
1298
+
1299
+ assert_equal ids_expected, ids_depr
1300
+ assert_equal ids_expected, ids_disabled
1301
+ end
1302
+
1303
+ test "pluck: allows string column name that are quoted" do
1304
+ titles_expected = Post.pluck(Arel.sql("title"))
1305
+
1306
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("[title]") }
1307
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("[title]") }
1308
+
1309
+ assert_equal titles_expected, titles_depr
1310
+ assert_equal titles_expected, titles_disabled
1311
+ end
1312
+
1313
+ test "pluck: allows string column name that are quoted with table" do
1314
+ titles_expected = Post.pluck(Arel.sql("title"))
1315
+
1316
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("[posts].[title]") }
1317
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("[posts].[title]") }
1318
+
1319
+ assert_equal titles_expected, titles_depr
1320
+ assert_equal titles_expected, titles_disabled
1321
+ end
1322
+
1323
+ test "pluck: allows string column name that are quoted with table and user" do
1324
+ titles_expected = Post.pluck(Arel.sql("title"))
1325
+
1326
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("[dbo].[posts].[title]") }
1327
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("[dbo].[posts].[title]") }
1328
+
1329
+ assert_equal titles_expected, titles_depr
1330
+ assert_equal titles_expected, titles_disabled
1331
+ end
1332
+
1333
+ test "pluck: allows string column name that are quoted with table, user and database" do
1334
+ titles_expected = Post.pluck(Arel.sql("title"))
1335
+
1336
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("[activerecord_unittest].[dbo].[posts].[title]") }
1337
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("[activerecord_unittest].[dbo].[posts].[title]") }
1338
+
1339
+ assert_equal titles_expected, titles_depr
1340
+ assert_equal titles_expected, titles_disabled
1341
+ end
1276
1342
  end
1277
1343
 
1278
1344
  class ReservedWordTest < ActiveRecord::TestCase
@@ -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.2
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: 2021-04-12 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.2/CHANGELOG.md
221
+ source_code_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/v6.0.2
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.1.4
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