activerecord-sqlserver-adapter 6.0.1 → 6.0.2

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: 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