activerecord-sqlserver-adapter 8.0.4 → 8.0.5
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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +16 -0
- data/VERSION +1 -1
- data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +145 -12
- data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +2 -0
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +2 -2
- data/test/cases/adapter_test_sqlserver.rb +11 -1
- data/test/cases/coerced_tests.rb +34 -44
- data/test/cases/schema_test_sqlserver.rb +18 -0
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 488cfb3fda58b07e4bacc2db733d766492f84d4d0af50ecd6e5548f066ac34b3
|
4
|
+
data.tar.gz: b64864d043dd035dde5413736d9fc9adb5d2bf1e364b25ef57ca2ea69bc386cf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fe2eb7bbc39edbc855bba7403792e532a43cae178d1cd8943189142f61c4203b346cb0191366626422ec287fde5d77d174240bcdef37152d177acda8d1e5dad4
|
7
|
+
data.tar.gz: 88ae10b6aa5b67f2bbc6a760a2f5af7679422fea1d10086aa3bb6592aaeadc202a2657c48906e4bb8b237acd7ca87e57588de6f02590a20a32278c855777b5c8
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -168,6 +168,22 @@ ActiveRecord::ConnectionAdapters::SQLServerAdapter.showplan_option = 'SHOWPLAN_X
|
|
168
168
|
```
|
169
169
|
**NOTE:** The method we utilize to make SHOWPLANs work is very brittle to complex SQL. There is no getting around this as we have to deconstruct an already prepared statement for the sp_executesql method. If you find that explain breaks your app, simple disable it. Do not open a github issue unless you have a patch. Please [consult the Rails guides](http://guides.rubyonrails.org/active_record_querying.html#running-explain) for more info.
|
170
170
|
|
171
|
+
#### `insert_all` / `upsert_all` support
|
172
|
+
|
173
|
+
`insert_all` and `upsert_all` on other database system like MySQL, SQlite or PostgreSQL use a clause with their `INSERT` statement to either skip duplicates (`ON DUPLICATE KEY IGNORE`) or to update the existing record (`ON DUPLICATE KEY UPDATE`). Microsoft SQL Server does not offer these clauses, so the support for these two options is implemented slightly different.
|
174
|
+
|
175
|
+
Behind the scenes, we execute a `MERGE` query, which joins your data that you want to insert or update into the table existing on the server. The emphasis here is "JOINING", so we also need to remove any duplicates that might make the `JOIN` operation fail, e.g. something like this:
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
Book.insert_all [
|
179
|
+
{ id: 200, author_id: 8, name: "Refactoring" },
|
180
|
+
{ id: 200, author_id: 8, name: "Refactoring" }
|
181
|
+
]
|
182
|
+
```
|
183
|
+
|
184
|
+
The removal of duplicates happens during the SQL query.
|
185
|
+
|
186
|
+
Because of this implementation, if you pass `on_duplicate` to `upsert_all`, make sure to assign your value to `target.[column_name]` (e.g. `target.status = GREATEST(target.status, 1)`). To access the values that you want to upsert, use `source.[column_name]`.
|
171
187
|
|
172
188
|
## New Rails Applications
|
173
189
|
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
8.0.
|
1
|
+
8.0.5
|
@@ -143,18 +143,54 @@ module ActiveRecord
|
|
143
143
|
private :default_insert_value
|
144
144
|
|
145
145
|
def build_insert_sql(insert) # :nodoc:
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
146
|
+
# Use regular insert if not skipping/updating duplicates.
|
147
|
+
return build_sql_for_regular_insert(insert:) unless insert.skip_duplicates? || insert.update_duplicates?
|
148
|
+
|
149
|
+
insert_all = insert.send(:insert_all)
|
150
|
+
columns_with_uniqueness_constraints = get_columns_with_uniqueness_constraints(insert_all:, insert:)
|
151
|
+
|
152
|
+
# If we do not have any columns that might have conflicting values just execute a regular insert, else use merge.
|
153
|
+
if columns_with_uniqueness_constraints.flatten.empty?
|
154
|
+
build_sql_for_regular_insert(insert:)
|
155
|
+
else
|
156
|
+
build_sql_for_merge_insert(insert:, insert_all:, columns_with_uniqueness_constraints:)
|
155
157
|
end
|
158
|
+
end
|
159
|
+
|
160
|
+
|
161
|
+
def build_sql_for_merge_insert(insert:, insert_all:, columns_with_uniqueness_constraints:) # :nodoc:
|
162
|
+
sql = <<~SQL
|
163
|
+
MERGE INTO #{insert.model.quoted_table_name} WITH (UPDLOCK, HOLDLOCK) AS target
|
164
|
+
USING (
|
165
|
+
SELECT *
|
166
|
+
FROM (
|
167
|
+
SELECT #{insert.send(:columns_list)}, #{partition_by_columns_with_uniqueness_constraints(columns_with_uniqueness_constraints:)}
|
168
|
+
FROM (#{insert.values_list})
|
169
|
+
AS t1 (#{insert.send(:columns_list)})
|
170
|
+
) AS ranked_source
|
171
|
+
WHERE #{is_first_record_across_all_uniqueness_constraints(columns_with_uniqueness_constraints:)}
|
172
|
+
) AS source
|
173
|
+
ON (#{joining_on_columns_with_uniqueness_constraints(columns_with_uniqueness_constraints:)})
|
174
|
+
SQL
|
175
|
+
|
176
|
+
if insert.update_duplicates?
|
177
|
+
sql << " WHEN MATCHED THEN UPDATE SET "
|
178
|
+
|
179
|
+
if insert.raw_update_sql?
|
180
|
+
sql << insert.raw_update_sql
|
181
|
+
else
|
182
|
+
if insert.record_timestamps?
|
183
|
+
sql << build_sql_for_recording_timestamps_when_updating(insert:)
|
184
|
+
end
|
185
|
+
|
186
|
+
sql << insert.updatable_columns.map { |column| "target.#{quote_column_name(column)}=source.#{quote_column_name(column)}" }.join(",")
|
187
|
+
end
|
188
|
+
end
|
189
|
+
sql << " WHEN NOT MATCHED BY TARGET THEN"
|
190
|
+
sql << " INSERT (#{insert.send(:columns_list)}) VALUES (#{insert_all.keys_including_timestamps.map { |column| "source.#{quote_column_name(column)}" }.join(", ")})"
|
191
|
+
sql << build_sql_for_returning(insert:, insert_all: insert.send(:insert_all))
|
192
|
+
sql << ";"
|
156
193
|
|
157
|
-
sql << " #{insert.values_list}"
|
158
194
|
sql
|
159
195
|
end
|
160
196
|
|
@@ -406,11 +442,18 @@ module ActiveRecord
|
|
406
442
|
raw_table_name = get_raw_table_name(sql)
|
407
443
|
id_column = identity_columns(raw_table_name).first
|
408
444
|
|
409
|
-
id_column &&
|
445
|
+
if id_column && (
|
446
|
+
sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)[^(]+\([^)]*\b(#{id_column.name})\b,?[^)]*\)/i ||
|
447
|
+
sql =~ /^\s*MERGE INTO.+THEN INSERT \([^)]*\b(#{id_column.name})\b,?[^)]*\)/im
|
448
|
+
)
|
449
|
+
SQLServer::Utils.extract_identifiers(raw_table_name).quoted
|
450
|
+
else
|
451
|
+
false
|
452
|
+
end
|
410
453
|
end
|
411
454
|
|
412
455
|
def insert_sql?(sql)
|
413
|
-
!(sql =~ /\A\s*(INSERT|EXEC sp_executesql N'INSERT)/
|
456
|
+
!(sql =~ /\A\s*(INSERT|EXEC sp_executesql N'INSERT|MERGE INTO.+THEN INSERT)/im).nil?
|
414
457
|
end
|
415
458
|
|
416
459
|
def identity_columns(table_name)
|
@@ -455,6 +498,96 @@ module ActiveRecord
|
|
455
498
|
|
456
499
|
perform_do ? result.do : result
|
457
500
|
end
|
501
|
+
|
502
|
+
# === SQLServer Specific (insert_all / upsert_all support) ===================== #
|
503
|
+
def build_sql_for_returning(insert:, insert_all:)
|
504
|
+
return "" unless insert_all.returning
|
505
|
+
|
506
|
+
returning_values_sql = if insert_all.returning.is_a?(String)
|
507
|
+
insert_all.returning
|
508
|
+
else
|
509
|
+
Array(insert_all.returning).map do |attribute|
|
510
|
+
if insert.model.attribute_alias?(attribute)
|
511
|
+
"INSERTED.#{quote_column_name(insert.model.attribute_alias(attribute))} AS #{quote_column_name(attribute)}"
|
512
|
+
else
|
513
|
+
"INSERTED.#{quote_column_name(attribute)}"
|
514
|
+
end
|
515
|
+
end.join(",")
|
516
|
+
end
|
517
|
+
|
518
|
+
" OUTPUT #{returning_values_sql}"
|
519
|
+
end
|
520
|
+
private :build_sql_for_returning
|
521
|
+
|
522
|
+
def get_columns_with_uniqueness_constraints(insert_all:, insert:)
|
523
|
+
if (unique_by = insert_all.unique_by)
|
524
|
+
[unique_by.columns]
|
525
|
+
else
|
526
|
+
# Compare against every unique constraint (primary key included).
|
527
|
+
# Discard constraints that are not fully included on insert.keys. Prevents invalid queries.
|
528
|
+
# Example: ignore unique index for columns ["name"] if insert keys is ["description"]
|
529
|
+
(insert_all.send(:unique_indexes).map(&:columns) + [insert_all.primary_keys]).select do |columns|
|
530
|
+
columns.to_set.subset?(insert.keys)
|
531
|
+
end
|
532
|
+
end
|
533
|
+
end
|
534
|
+
private :get_columns_with_uniqueness_constraints
|
535
|
+
|
536
|
+
def build_sql_for_regular_insert(insert:)
|
537
|
+
sql = "INSERT #{insert.into}"
|
538
|
+
sql << build_sql_for_returning(insert:, insert_all: insert.send(:insert_all))
|
539
|
+
sql << " #{insert.values_list}"
|
540
|
+
sql
|
541
|
+
end
|
542
|
+
private :build_sql_for_regular_insert
|
543
|
+
|
544
|
+
# why is the "PARTITION BY" clause needed?
|
545
|
+
# in every DBMS system, insert_all / upsert_all is usually implemented with INSERT, that allows to define what happens
|
546
|
+
# when duplicates are found (SKIP OR UPDATE)
|
547
|
+
# by default rows are considered to be unique by every unique index on the table
|
548
|
+
# but since we have to use MERGE in MSSQL, which in return is a JOIN, we have to perform the "de-duplication" ourselves
|
549
|
+
# otherwise the "JOIN" clause would complain about non-unique values and being unable to JOIN the two tables
|
550
|
+
# this works easiest by using PARTITION and make sure that any record
|
551
|
+
# we are trying to insert is "the first one seen across all the potential columns with uniqueness constraints"
|
552
|
+
def partition_by_columns_with_uniqueness_constraints(columns_with_uniqueness_constraints:)
|
553
|
+
columns_with_uniqueness_constraints.map.with_index do |group_of_columns_with_uniqueness_constraints, index|
|
554
|
+
<<~PARTITION_BY
|
555
|
+
ROW_NUMBER() OVER (
|
556
|
+
PARTITION BY #{group_of_columns_with_uniqueness_constraints.map { |column| quote_column_name(column) }.join(",")}
|
557
|
+
ORDER BY #{group_of_columns_with_uniqueness_constraints.map { |column| "#{quote_column_name(column)} DESC" }.join(",")}
|
558
|
+
) AS rn_#{index}
|
559
|
+
PARTITION_BY
|
560
|
+
end.join(", ")
|
561
|
+
end
|
562
|
+
private :partition_by_columns_with_uniqueness_constraints
|
563
|
+
|
564
|
+
def is_first_record_across_all_uniqueness_constraints(columns_with_uniqueness_constraints:)
|
565
|
+
columns_with_uniqueness_constraints.map.with_index do |group_of_columns_with_uniqueness_constraints, index|
|
566
|
+
"rn_#{index} = 1"
|
567
|
+
end.join(" AND ")
|
568
|
+
end
|
569
|
+
private :is_first_record_across_all_uniqueness_constraints
|
570
|
+
|
571
|
+
def joining_on_columns_with_uniqueness_constraints(columns_with_uniqueness_constraints:)
|
572
|
+
columns_with_uniqueness_constraints.map do |columns|
|
573
|
+
columns.map do |column|
|
574
|
+
"target.#{quote_column_name(column)} = source.#{quote_column_name(column)}"
|
575
|
+
end.join(" AND ")
|
576
|
+
end.join(") OR (")
|
577
|
+
end
|
578
|
+
private :joining_on_columns_with_uniqueness_constraints
|
579
|
+
|
580
|
+
# normally, generating the CASE SQL is done entirely by Rails
|
581
|
+
# and you would just hook into "touch_model_timestamps_unless" to add your database-specific instructions
|
582
|
+
# however, since we need to have "target." for the assignment, we also generate the CASE switch ourselves
|
583
|
+
def build_sql_for_recording_timestamps_when_updating(insert:)
|
584
|
+
insert.model.timestamp_attributes_for_update_in_model.filter_map do |column_name|
|
585
|
+
if insert.send(:touch_timestamp_attribute?, column_name)
|
586
|
+
"target.#{quote_column_name(column_name)}=CASE WHEN (#{insert.updatable_columns.map { |column| "(COALESCE(target.#{quote_column_name(column)}, 'NULL') = COALESCE(source.#{quote_column_name(column)}, 'NULL'))" }.join(" AND ")}) THEN target.#{quote_column_name(column_name)} ELSE #{high_precision_current_timestamp} END,"
|
587
|
+
end
|
588
|
+
end.join
|
589
|
+
end
|
590
|
+
private :build_sql_for_recording_timestamps_when_updating
|
458
591
|
end
|
459
592
|
end
|
460
593
|
end
|
@@ -720,6 +720,8 @@ module ActiveRecord
|
|
720
720
|
.match(/\s*([^(]*)/i)[0]
|
721
721
|
elsif s.match?(/^\s*UPDATE\s+.*/i)
|
722
722
|
s.match(/UPDATE\s+([^\(\s]+)\s*/i)[1]
|
723
|
+
elsif s.match?(/^\s*MERGE INTO.*/i)
|
724
|
+
s.match(/^\s*MERGE\s+INTO\s+(\[?[a-z_ -]+\]?\.?\[?[a-z_ -]+\]?)\s+(AS|WITH|USING)/i)[1]
|
723
725
|
else
|
724
726
|
s.match(/FROM[\s|\(]+((\[[^\(\]]+\])|[^\(\s]+)\s*/i)[1]
|
725
727
|
end.strip
|
@@ -13,6 +13,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
|
|
13
13
|
fixtures :tasks
|
14
14
|
|
15
15
|
let(:basic_insert_sql) { "INSERT INTO [funny_jokes] ([name]) VALUES('Knock knock')" }
|
16
|
+
let(:basic_merge_sql) { "MERGE INTO [ships] WITH (UPDLOCK, HOLDLOCK) AS target USING ( SELECT * FROM ( SELECT [id], [name], ROW_NUMBER() OVER ( PARTITION BY [id] ORDER BY [id] DESC ) AS rn_0 FROM ( VALUES (101, N'RSS Sir David Attenborough') ) AS t1 ([id], [name]) ) AS ranked_source WHERE rn_0 = 1 ) AS source ON (target.[id] = source.[id]) WHEN MATCHED THEN UPDATE SET target.[name] = source.[name]" }
|
16
17
|
let(:basic_update_sql) { "UPDATE [customers] SET [address_street] = NULL WHERE [id] = 2" }
|
17
18
|
let(:basic_select_sql) { "SELECT * FROM [customers] WHERE ([customers].[id] = 1)" }
|
18
19
|
|
@@ -93,6 +94,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
|
|
93
94
|
|
94
95
|
it "return unquoted table name object from basic INSERT UPDATE and SELECT statements" do
|
95
96
|
assert_equal "funny_jokes", connection.send(:get_table_name, basic_insert_sql)
|
97
|
+
assert_equal "ships", connection.send(:get_table_name, basic_merge_sql)
|
96
98
|
assert_equal "customers", connection.send(:get_table_name, basic_update_sql)
|
97
99
|
assert_equal "customers", connection.send(:get_table_name, basic_select_sql)
|
98
100
|
end
|
@@ -213,6 +215,10 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
|
|
213
215
|
@identity_insert_sql_unquoted_sp = "EXEC sp_executesql N'INSERT INTO funny_jokes (id, name) VALUES (@0, @1)', N'@0 int, @1 nvarchar(255)', @0 = 420, @1 = N'Knock knock'"
|
214
216
|
@identity_insert_sql_unordered_sp = "EXEC sp_executesql N'INSERT INTO [funny_jokes] ([name],[id]) VALUES (@0, @1)', N'@0 nvarchar(255), @1 int', @0 = N'Knock knock', @1 = 420"
|
215
217
|
|
218
|
+
@identity_merge_sql = "MERGE INTO [ships] WITH (UPDLOCK, HOLDLOCK) AS target USING ( SELECT * FROM ( SELECT [id], [name], ROW_NUMBER() OVER ( PARTITION BY [id] ORDER BY [id] DESC ) AS rn_0 FROM ( VALUES (101, N'RSS Sir David Attenborough') ) AS t1 ([id], [name]) ) AS ranked_source WHERE rn_0 = 1 ) AS source ON (target.[id] = source.[id]) WHEN MATCHED THEN UPDATE SET target.[name] = source.[name] WHEN NOT MATCHED BY TARGET THEN INSERT ([id], [name]) VALUES (source.[id], source.[name]) OUTPUT INSERTED.[id]"
|
219
|
+
@identity_merge_sql_unquoted = "MERGE INTO ships WITH (UPDLOCK, HOLDLOCK) AS target USING ( SELECT * FROM ( SELECT id, name, ROW_NUMBER() OVER ( PARTITION BY id ORDER BY id DESC ) AS rn_0 FROM ( VALUES (101, N'RSS Sir David Attenborough') ) AS t1 (id, name) ) AS ranked_source WHERE rn_0 = 1 ) AS source ON (target.id = source.id) WHEN MATCHED THEN UPDATE SET target.name = source.name WHEN NOT MATCHED BY TARGET THEN INSERT (id, name) VALUES (source.id, source.name) OUTPUT INSERTED.id"
|
220
|
+
@identity_merge_sql_unordered = "MERGE INTO [ships] WITH (UPDLOCK, HOLDLOCK) AS target USING ( SELECT * FROM ( SELECT [name], [id], ROW_NUMBER() OVER ( PARTITION BY [id] ORDER BY [id] DESC ) AS rn_0 FROM ( VALUES (101, N'RSS Sir David Attenborough') ) AS t1 ([name], [id]) ) AS ranked_source WHERE rn_0 = 1 ) AS source ON (target.[id] = source.[id]) WHEN MATCHED THEN UPDATE SET target.[name] = source.[name] WHEN NOT MATCHED BY TARGET THEN INSERT ([name], [id]) VALUES (source.[name], source.[id]) OUTPUT INSERTED.[id]"
|
221
|
+
|
216
222
|
@identity_insert_sql_non_dbo = "INSERT INTO [test].[aliens] ([id],[name]) VALUES(420,'Mork')"
|
217
223
|
@identity_insert_sql_non_dbo_unquoted = "INSERT INTO test.aliens ([id],[name]) VALUES(420,'Mork')"
|
218
224
|
@identity_insert_sql_non_dbo_unordered = "INSERT INTO [test].[aliens] ([name],[id]) VALUES('Mork',420)"
|
@@ -229,6 +235,10 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
|
|
229
235
|
assert_equal "[funny_jokes]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_unquoted_sp)
|
230
236
|
assert_equal "[funny_jokes]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_unordered_sp)
|
231
237
|
|
238
|
+
assert_equal "[ships]", connection.send(:query_requires_identity_insert?, @identity_merge_sql)
|
239
|
+
assert_equal "[ships]", connection.send(:query_requires_identity_insert?, @identity_merge_sql_unquoted)
|
240
|
+
assert_equal "[ships]", connection.send(:query_requires_identity_insert?, @identity_merge_sql_unordered)
|
241
|
+
|
232
242
|
assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo)
|
233
243
|
assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_unquoted)
|
234
244
|
assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_unordered)
|
@@ -238,7 +248,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
|
|
238
248
|
end
|
239
249
|
|
240
250
|
it "return false to #query_requires_identity_insert? for normal SQL" do
|
241
|
-
[basic_insert_sql, basic_update_sql, basic_select_sql].each do |sql|
|
251
|
+
[basic_insert_sql, basic_merge_sql, basic_update_sql, basic_select_sql].each do |sql|
|
242
252
|
assert !connection.send(:query_requires_identity_insert?, sql), "SQL was #{sql}"
|
243
253
|
end
|
244
254
|
end
|
data/test/cases/coerced_tests.rb
CHANGED
@@ -376,6 +376,22 @@ module ActiveRecord
|
|
376
376
|
end
|
377
377
|
|
378
378
|
class CalculationsTest < ActiveRecord::TestCase
|
379
|
+
# SELECT columns must be in the GROUP clause.
|
380
|
+
coerce_tests! :test_should_count_with_group_by_qualified_name_on_loaded
|
381
|
+
def test_should_count_with_group_by_qualified_name_on_loaded_coerced
|
382
|
+
accounts = Account.group("accounts.id").select("accounts.id")
|
383
|
+
|
384
|
+
expected = {1 => 1, 2 => 1, 3 => 1, 4 => 1, 5 => 1, 6 => 1}
|
385
|
+
|
386
|
+
assert_not_predicate accounts, :loaded?
|
387
|
+
assert_equal expected, accounts.count
|
388
|
+
|
389
|
+
accounts.load
|
390
|
+
|
391
|
+
assert_predicate accounts, :loaded?
|
392
|
+
assert_equal expected, accounts.count(:id)
|
393
|
+
end
|
394
|
+
|
379
395
|
# Fix randomly failing test. The loading of the model's schema was affecting the test.
|
380
396
|
coerce_tests! :test_offset_is_kept
|
381
397
|
def test_offset_is_kept_coerced
|
@@ -2200,35 +2216,6 @@ class EnumTest < ActiveRecord::TestCase
|
|
2200
2216
|
end
|
2201
2217
|
end
|
2202
2218
|
|
2203
|
-
require "models/task"
|
2204
|
-
class QueryCacheExpiryTest < ActiveRecord::TestCase
|
2205
|
-
# SQL Server does not support skipping or upserting duplicates.
|
2206
|
-
coerce_tests! :test_insert_all
|
2207
|
-
def test_insert_all_coerced
|
2208
|
-
assert_raises(ArgumentError, /does not support skipping duplicates/) do
|
2209
|
-
Task.cache { Task.insert({ starting: Time.now }) }
|
2210
|
-
end
|
2211
|
-
|
2212
|
-
assert_raises(ArgumentError, /does not support upsert/) do
|
2213
|
-
Task.cache { Task.upsert({ starting: Time.now }) }
|
2214
|
-
end
|
2215
|
-
|
2216
|
-
assert_raises(ArgumentError, /does not support upsert/) do
|
2217
|
-
Task.cache { Task.upsert_all([{ starting: Time.now }]) }
|
2218
|
-
end
|
2219
|
-
|
2220
|
-
Task.cache do
|
2221
|
-
assert_called(ActiveRecord::Base.connection_pool.query_cache, :clear, times: 1) do
|
2222
|
-
Task.insert_all!([ starting: Time.now ])
|
2223
|
-
end
|
2224
|
-
|
2225
|
-
assert_called(ActiveRecord::Base.connection_pool.query_cache, :clear, times: 1) do
|
2226
|
-
Task.insert!({ starting: Time.now })
|
2227
|
-
end
|
2228
|
-
end
|
2229
|
-
end
|
2230
|
-
end
|
2231
|
-
|
2232
2219
|
require "models/citation"
|
2233
2220
|
class EagerLoadingTooManyIdsTest < ActiveRecord::TestCase
|
2234
2221
|
fixtures :citations
|
@@ -2507,6 +2494,24 @@ class InsertAllTest < ActiveRecord::TestCase
|
|
2507
2494
|
Book.where(author_id: nil, name: '["Array"]').delete_all
|
2508
2495
|
Book.lease_connection.add_index(:books, [:author_id, :name], unique: true)
|
2509
2496
|
end
|
2497
|
+
|
2498
|
+
# Same as original but using target.status for assignment and CASE instead of GREATEST for operator
|
2499
|
+
coerce_tests! :test_upsert_all_updates_using_provided_sql
|
2500
|
+
def test_upsert_all_updates_using_provided_sql_coerced
|
2501
|
+
Book.upsert_all(
|
2502
|
+
[{id: 1, status: 1}, {id: 2, status: 1}],
|
2503
|
+
on_duplicate: Arel.sql(<<~SQL
|
2504
|
+
target.status = CASE
|
2505
|
+
WHEN target.status > 1 THEN target.status
|
2506
|
+
ELSE 1
|
2507
|
+
END
|
2508
|
+
SQL
|
2509
|
+
)
|
2510
|
+
)
|
2511
|
+
|
2512
|
+
assert_equal "published", Book.find(1).status
|
2513
|
+
assert_equal "written", Book.find(2).status
|
2514
|
+
end
|
2510
2515
|
end
|
2511
2516
|
|
2512
2517
|
module ActiveRecord
|
@@ -2524,21 +2529,6 @@ module ActiveRecord
|
|
2524
2529
|
end
|
2525
2530
|
end
|
2526
2531
|
|
2527
|
-
# SQL Server does not support upsert. Removed dependency on `insert_all` that uses upsert.
|
2528
|
-
class ActiveRecord::Encryption::ConcurrencyTest < ActiveRecord::EncryptionTestCase
|
2529
|
-
undef_method :thread_encrypting_and_decrypting
|
2530
|
-
def thread_encrypting_and_decrypting(thread_label)
|
2531
|
-
posts = 100.times.collect { |index| EncryptedPost.create! title: "Article #{index} (#{thread_label})", body: "Body #{index} (#{thread_label})" }
|
2532
|
-
|
2533
|
-
Thread.new do
|
2534
|
-
posts.each.with_index do |article, index|
|
2535
|
-
assert_encrypted_attribute article, :title, "Article #{index} (#{thread_label})"
|
2536
|
-
article.decrypt
|
2537
|
-
assert_not_encrypted_attribute article, :title, "Article #{index} (#{thread_label})"
|
2538
|
-
end
|
2539
|
-
end
|
2540
|
-
end
|
2541
|
-
end
|
2542
2532
|
|
2543
2533
|
# Need to use `install_unregistered_type_fallback` instead of `install_unregistered_type_error` so that message-pack
|
2544
2534
|
# can read and write `ActiveRecord::ConnectionAdapters::SQLServer::Type::Data` objects.
|
@@ -102,6 +102,24 @@ class SchemaTestSQLServer < ActiveRecord::TestCase
|
|
102
102
|
end
|
103
103
|
end
|
104
104
|
|
105
|
+
describe "MERGE statements" do
|
106
|
+
it do
|
107
|
+
assert_equal "[dashboards]", connection.send(:get_raw_table_name, "MERGE INTO [dashboards] AS target")
|
108
|
+
end
|
109
|
+
|
110
|
+
it do
|
111
|
+
assert_equal "lock_without_defaults", connection.send(:get_raw_table_name, "MERGE INTO lock_without_defaults AS target")
|
112
|
+
end
|
113
|
+
|
114
|
+
it do
|
115
|
+
assert_equal "[WITH - SPACES]", connection.send(:get_raw_table_name, "MERGE INTO [WITH - SPACES] AS target")
|
116
|
+
end
|
117
|
+
|
118
|
+
it do
|
119
|
+
assert_equal "[with].[select notation]", connection.send(:get_raw_table_name, "MERGE INTO [with].[select notation] AS target")
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
105
123
|
describe 'CREATE VIEW statements' do
|
106
124
|
it do
|
107
125
|
assert_equal "test_table_as", connection.send(:get_raw_table_name, "CREATE VIEW test_views ( test_table_a_id, test_table_b_id ) AS SELECT test_table_as.id as test_table_a_id, test_table_bs.id as test_table_b_id FROM (test_table_as with(nolock) LEFT JOIN test_table_bs with(nolock) ON (test_table_as.id = test_table_bs.test_table_a_id))")
|
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: 8.0.
|
4
|
+
version: 8.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ken Collins
|
@@ -15,7 +15,7 @@ authors:
|
|
15
15
|
autorequire:
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
|
-
date: 2025-03-
|
18
|
+
date: 2025-03-20 00:00:00.000000000 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: activerecord
|
@@ -239,8 +239,8 @@ licenses:
|
|
239
239
|
- MIT
|
240
240
|
metadata:
|
241
241
|
bug_tracker_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues
|
242
|
-
changelog_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/blob/v8.0.
|
243
|
-
source_code_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/v8.0.
|
242
|
+
changelog_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/blob/v8.0.5/CHANGELOG.md
|
243
|
+
source_code_uri: https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/tree/v8.0.5
|
244
244
|
post_install_message:
|
245
245
|
rdoc_options: []
|
246
246
|
require_paths:
|