activerecord-sqlserver-adapter 7.2.7 → 8.0.0
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/.github/workflows/ci.yml +10 -6
- data/CHANGELOG.md +5 -55
- data/Dockerfile.ci +1 -1
- data/Gemfile +2 -0
- data/README.md +16 -17
- data/VERSION +1 -1
- data/activerecord-sqlserver-adapter.gemspec +2 -2
- data/docker-compose.ci.yml +0 -1
- data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +1 -1
- data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +47 -52
- data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +129 -118
- data/lib/active_record/connection_adapters/sqlserver/showplan.rb +1 -0
- data/lib/active_record/connection_adapters/sqlserver/type/time.rb +3 -2
- data/lib/active_record/connection_adapters/sqlserver/utils.rb +0 -4
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +11 -21
- data/test/cases/adapter_test_sqlserver.rb +36 -38
- data/test/cases/coerced_tests.rb +56 -35
- data/test/cases/optimizer_hints_test_sqlserver.rb +0 -1
- data/test/cases/schema_test_sqlserver.rb +0 -6
- data/test/cases/view_test_sqlserver.rb +3 -9
- metadata +11 -15
- data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +0 -29
- data/test/cases/temp_test_sqlserver.rb +0 -9
- data/test/cases/temporary_table_test_sqlserver.rb +0 -19
- data/test/fixtures/sst_customers_view.yml +0 -6
@@ -14,22 +14,24 @@ module ActiveRecord
|
|
14
14
|
res
|
15
15
|
end
|
16
16
|
|
17
|
-
def drop_table(
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
17
|
+
def drop_table(*table_names, **options)
|
18
|
+
table_names.each do |table_name|
|
19
|
+
# Mimic CASCADE option as best we can.
|
20
|
+
if options[:force] == :cascade
|
21
|
+
execute_procedure(:sp_fkeys, pktable_name: table_name).each do |fkdata|
|
22
|
+
fktable = fkdata["FKTABLE_NAME"]
|
23
|
+
fkcolmn = fkdata["FKCOLUMN_NAME"]
|
24
|
+
pktable = fkdata["PKTABLE_NAME"]
|
25
|
+
pkcolmn = fkdata["PKCOLUMN_NAME"]
|
26
|
+
remove_foreign_key fktable, name: fkdata["FK_NAME"]
|
27
|
+
execute "DELETE FROM #{quote_table_name(fktable)} WHERE #{quote_column_name(fkcolmn)} IN ( SELECT #{quote_column_name(pkcolmn)} FROM #{quote_table_name(pktable)} )"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
if options[:if_exists] && version_year < 2016
|
31
|
+
execute "IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = #{quote(table_name)}) DROP TABLE #{quote_table_name(table_name)}", "SCHEMA"
|
32
|
+
else
|
33
|
+
super
|
27
34
|
end
|
28
|
-
end
|
29
|
-
if options[:if_exists] && version_year < 2016
|
30
|
-
execute "IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = #{quote(table_name)}) DROP TABLE #{quote_table_name(table_name)}", "SCHEMA"
|
31
|
-
else
|
32
|
-
super
|
33
35
|
end
|
34
36
|
end
|
35
37
|
|
@@ -37,18 +39,16 @@ module ActiveRecord
|
|
37
39
|
data = select("EXEC sp_helpindex #{quote(table_name)}", "SCHEMA") rescue []
|
38
40
|
|
39
41
|
data.reduce([]) do |indexes, index|
|
40
|
-
|
41
|
-
|
42
|
-
if index[:index_description].match?(/primary key/)
|
42
|
+
if index['index_description'].match?(/primary key/)
|
43
43
|
indexes
|
44
44
|
else
|
45
|
-
name = index[
|
46
|
-
unique = index[
|
45
|
+
name = index['index_name']
|
46
|
+
unique = index['index_description'].match?(/unique/)
|
47
47
|
where = select_value("SELECT [filter_definition] FROM sys.indexes WHERE name = #{quote(name)}", "SCHEMA")
|
48
48
|
orders = {}
|
49
49
|
columns = []
|
50
50
|
|
51
|
-
index[
|
51
|
+
index['index_keys'].split(",").each do |column|
|
52
52
|
column.strip!
|
53
53
|
|
54
54
|
if column.end_with?("(-)")
|
@@ -347,16 +347,12 @@ module ActiveRecord
|
|
347
347
|
|
348
348
|
def columns_for_distinct(columns, orders)
|
349
349
|
order_columns = orders.reject(&:blank?).map { |s|
|
350
|
-
s =
|
350
|
+
s = s.to_sql unless s.is_a?(String)
|
351
351
|
s.gsub(/\s+(?:ASC|DESC)\b/i, "")
|
352
352
|
.gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
|
353
|
-
|
354
|
-
.reject(&:blank?)
|
355
|
-
.reject { |s| columns.include?(s) }
|
353
|
+
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
|
356
354
|
|
357
|
-
|
358
|
-
|
359
|
-
(order_columns_aliased << super).join(", ")
|
355
|
+
(order_columns << super).join(", ")
|
360
356
|
end
|
361
357
|
|
362
358
|
def update_table_definition(table_name, base)
|
@@ -484,16 +480,15 @@ module ActiveRecord
|
|
484
480
|
end
|
485
481
|
|
486
482
|
def column_definitions(table_name)
|
487
|
-
identifier
|
488
|
-
database
|
489
|
-
view_exists
|
490
|
-
view_tblnm = view_table_name(table_name) if view_exists
|
483
|
+
identifier = database_prefix_identifier(table_name)
|
484
|
+
database = identifier.fully_qualified_database_quoted
|
485
|
+
view_exists = view_exists?(table_name)
|
491
486
|
|
492
487
|
if view_exists
|
493
488
|
sql = <<~SQL
|
494
489
|
SELECT LOWER(c.COLUMN_NAME) AS [name], c.COLUMN_DEFAULT AS [default]
|
495
490
|
FROM #{database}.INFORMATION_SCHEMA.COLUMNS c
|
496
|
-
WHERE c.TABLE_NAME = #{quote(
|
491
|
+
WHERE c.TABLE_NAME = #{quote(view_table_name(table_name))}
|
497
492
|
SQL
|
498
493
|
results = internal_exec_query(sql, "SCHEMA")
|
499
494
|
default_functions = results.each.with_object({}) { |row, out| out[row["name"]] = row["default"] }.compact
|
@@ -502,88 +497,103 @@ module ActiveRecord
|
|
502
497
|
sql = column_definitions_sql(database, identifier)
|
503
498
|
|
504
499
|
binds = []
|
505
|
-
nv128 = SQLServer::Type::UnicodeVarchar.new
|
500
|
+
nv128 = SQLServer::Type::UnicodeVarchar.new(limit: 128)
|
506
501
|
binds << Relation::QueryAttribute.new("TABLE_NAME", identifier.object, nv128)
|
507
502
|
binds << Relation::QueryAttribute.new("TABLE_SCHEMA", identifier.schema, nv128) unless identifier.schema.blank?
|
503
|
+
|
508
504
|
results = internal_exec_query(sql, "SCHEMA", binds)
|
505
|
+
raise ActiveRecord::StatementInvalid, "Table '#{table_name}' doesn't exist" if results.empty?
|
509
506
|
|
510
507
|
columns = results.map do |ci|
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
when nil
|
539
|
-
[nil, nil]
|
540
|
-
when /\A\((\w+\(\))\)\Z/
|
541
|
-
default_function = Regexp.last_match[1]
|
542
|
-
[nil, default_function]
|
543
|
-
when /\A\(N'(.*)'\)\Z/m
|
544
|
-
string_literal = SQLServer::Utils.unquote_string(Regexp.last_match[1])
|
545
|
-
[string_literal, nil]
|
546
|
-
when /CREATE DEFAULT/mi
|
547
|
-
[nil, nil]
|
548
|
-
else
|
549
|
-
type = case ci[:type]
|
550
|
-
when /smallint|int|bigint/ then ci[:_type]
|
551
|
-
else ci[:type]
|
552
|
-
end
|
553
|
-
value = default.match(/\A\((.*)\)\Z/m)[1]
|
554
|
-
value = select_value("SELECT CAST(#{value} AS #{type}) AS value", "SCHEMA")
|
555
|
-
[value, nil]
|
556
|
-
end
|
508
|
+
col = {
|
509
|
+
name: ci["name"],
|
510
|
+
numeric_scale: ci["numeric_scale"],
|
511
|
+
numeric_precision: ci["numeric_precision"],
|
512
|
+
datetime_precision: ci["datetime_precision"],
|
513
|
+
collation: ci["collation"],
|
514
|
+
ordinal_position: ci["ordinal_position"],
|
515
|
+
length: ci["length"]
|
516
|
+
}
|
517
|
+
|
518
|
+
col[:table_name] = view_exists ? view_table_name(table_name) : table_name
|
519
|
+
col[:type] = column_type(ci: ci)
|
520
|
+
col[:default_value], col[:default_function] = default_value_and_function(default: ci['default_value'],
|
521
|
+
name: ci['name'],
|
522
|
+
type: col[:type],
|
523
|
+
original_type: ci['type'],
|
524
|
+
view_exists: view_exists,
|
525
|
+
table_name: table_name,
|
526
|
+
default_functions: default_functions)
|
527
|
+
|
528
|
+
col[:null] = ci['is_nullable'].to_i == 1
|
529
|
+
col[:is_primary] = ci['is_primary'].to_i == 1
|
530
|
+
|
531
|
+
if [true, false].include?(ci['is_identity'])
|
532
|
+
col[:is_identity] = ci['is_identity']
|
533
|
+
else
|
534
|
+
col[:is_identity] = ci['is_identity'].to_i == 1
|
557
535
|
end
|
558
|
-
ci[:null] = ci[:is_nullable].to_i == 1
|
559
|
-
ci.delete(:is_nullable)
|
560
|
-
ci[:is_primary] = ci[:is_primary].to_i == 1
|
561
|
-
ci[:is_identity] = ci[:is_identity].to_i == 1 unless [TrueClass, FalseClass].include?(ci[:is_identity].class)
|
562
|
-
ci
|
563
|
-
end
|
564
536
|
|
565
|
-
|
566
|
-
# I'm not aware of the possibility of tables without columns on SQL Server (postgres have those).
|
567
|
-
# Raise error if the method return an empty array
|
568
|
-
columns.tap do |result|
|
569
|
-
raise ActiveRecord::StatementInvalid, "Table '#{table_name}' doesn't exist" if result.empty?
|
537
|
+
col
|
570
538
|
end
|
539
|
+
|
540
|
+
columns
|
571
541
|
end
|
572
542
|
|
573
|
-
def
|
574
|
-
|
575
|
-
|
543
|
+
def default_value_and_function(default:, name:, type:, original_type:, view_exists:, table_name:, default_functions:)
|
544
|
+
if default.nil? && view_exists
|
545
|
+
view_column = views_real_column_name(table_name, name).downcase
|
546
|
+
default = default_functions[view_column] if view_column.present?
|
547
|
+
end
|
548
|
+
|
549
|
+
case default
|
550
|
+
when nil
|
551
|
+
[nil, nil]
|
552
|
+
when /\A\((\w+\(\))\)\Z/
|
553
|
+
default_function = Regexp.last_match[1]
|
554
|
+
[nil, default_function]
|
555
|
+
when /\A\(N'(.*)'\)\Z/m
|
556
|
+
string_literal = SQLServer::Utils.unquote_string(Regexp.last_match[1])
|
557
|
+
[string_literal, nil]
|
558
|
+
when /CREATE DEFAULT/mi
|
559
|
+
[nil, nil]
|
560
|
+
else
|
561
|
+
type = case type
|
562
|
+
when /smallint|int|bigint/ then original_type
|
563
|
+
else type
|
564
|
+
end
|
565
|
+
value = default.match(/\A\((.*)\)\Z/m)[1]
|
566
|
+
value = select_value("SELECT CAST(#{value} AS #{type}) AS value", "SCHEMA")
|
567
|
+
[value, nil]
|
568
|
+
end
|
569
|
+
end
|
576
570
|
|
577
|
-
|
578
|
-
|
579
|
-
|
571
|
+
def column_type(ci:)
|
572
|
+
case ci['type']
|
573
|
+
when /^bit|image|text|ntext|datetime$/
|
574
|
+
ci['type']
|
575
|
+
when /^datetime2|datetimeoffset$/i
|
576
|
+
"#{ci['type']}(#{ci['datetime_precision']})"
|
577
|
+
when /^time$/i
|
578
|
+
"#{ci['type']}(#{ci['datetime_precision']})"
|
579
|
+
when /^numeric|decimal$/i
|
580
|
+
"#{ci['type']}(#{ci['numeric_precision']},#{ci['numeric_scale']})"
|
581
|
+
when /^float|real$/i
|
582
|
+
"#{ci['type']}"
|
583
|
+
when /^char|nchar|varchar|nvarchar|binary|varbinary|bigint|int|smallint$/
|
584
|
+
ci['length'].to_i == -1 ? "#{ci['type']}(max)" : "#{ci['type']}(#{ci['length']})"
|
580
585
|
else
|
581
|
-
|
582
|
-
schema_name = quote(identifier.schema) if identifier.schema.present?
|
586
|
+
ci['type']
|
583
587
|
end
|
588
|
+
end
|
584
589
|
|
585
|
-
|
586
|
-
|
590
|
+
def column_definitions_sql(database, identifier)
|
591
|
+
object_name = prepared_statements ? "@0" : quote(identifier.object)
|
592
|
+
schema_name = if identifier.schema.blank?
|
593
|
+
"schema_name()"
|
594
|
+
else
|
595
|
+
prepared_statements ? "@1" : quote(identifier.schema)
|
596
|
+
end
|
587
597
|
|
588
598
|
%{
|
589
599
|
SELECT
|
@@ -638,7 +648,7 @@ module ActiveRecord
|
|
638
648
|
AND k.unique_index_id = ic.index_id
|
639
649
|
AND c.column_id = ic.column_id
|
640
650
|
WHERE
|
641
|
-
o.
|
651
|
+
o.name = #{object_name}
|
642
652
|
AND s.name = #{schema_name}
|
643
653
|
ORDER BY
|
644
654
|
c.column_id
|
@@ -660,7 +670,7 @@ module ActiveRecord
|
|
660
670
|
end
|
661
671
|
|
662
672
|
def remove_default_constraint(table_name, column_name)
|
663
|
-
# If
|
673
|
+
# If their are foreign keys in this table, we could still get back a 2D array, so flatten just in case.
|
664
674
|
execute_procedure(:sp_helpconstraint, table_name, "nomsg").flatten.select do |row|
|
665
675
|
row["constraint_type"] == "DEFAULT on column #{column_name}"
|
666
676
|
end.each do |row|
|
@@ -697,7 +707,7 @@ module ActiveRecord
|
|
697
707
|
elsif s.match?(/^\s*UPDATE\s+.*/i)
|
698
708
|
s.match(/UPDATE\s+([^\(\s]+)\s*/i)[1]
|
699
709
|
else
|
700
|
-
s.match(/FROM
|
710
|
+
s.match(/FROM\s+((\[[^\(\]]+\])|[^\(\s]+)\s*/i)[1]
|
701
711
|
end.strip
|
702
712
|
end
|
703
713
|
|
@@ -713,25 +723,26 @@ module ActiveRecord
|
|
713
723
|
|
714
724
|
def view_table_name(table_name)
|
715
725
|
view_info = view_information(table_name)
|
716
|
-
view_info ? get_table_name(view_info["VIEW_DEFINITION"]) : table_name
|
726
|
+
view_info.present? ? get_table_name(view_info["VIEW_DEFINITION"]) : table_name
|
717
727
|
end
|
718
728
|
|
719
729
|
def view_information(table_name)
|
720
730
|
@view_information ||= {}
|
731
|
+
|
721
732
|
@view_information[table_name] ||= begin
|
722
733
|
identifier = SQLServer::Utils.extract_identifiers(table_name)
|
723
734
|
information_query_table = identifier.database.present? ? "[#{identifier.database}].[INFORMATION_SCHEMA].[VIEWS]" : "[INFORMATION_SCHEMA].[VIEWS]"
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
if view_info[
|
729
|
-
view_info[
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
+
|
736
|
+
view_info = select_one("SELECT * FROM #{information_query_table} WITH (NOLOCK) WHERE TABLE_NAME = #{quote(identifier.object)}", "SCHEMA").to_h
|
737
|
+
|
738
|
+
if view_info.present?
|
739
|
+
if view_info['VIEW_DEFINITION'].blank? || view_info['VIEW_DEFINITION'].length == 4000
|
740
|
+
view_info['VIEW_DEFINITION'] = begin
|
741
|
+
select_values("EXEC sp_helptext #{identifier.object_quoted}", "SCHEMA").join
|
742
|
+
rescue
|
743
|
+
warn "No view definition found, possible permissions problem.\nPlease run GRANT VIEW DEFINITION TO your_user;"
|
744
|
+
nil
|
745
|
+
end
|
735
746
|
end
|
736
747
|
end
|
737
748
|
|
@@ -740,8 +751,8 @@ module ActiveRecord
|
|
740
751
|
end
|
741
752
|
|
742
753
|
def views_real_column_name(table_name, column_name)
|
743
|
-
view_definition = view_information(table_name)[
|
744
|
-
return column_name
|
754
|
+
view_definition = view_information(table_name)['VIEW_DEFINITION']
|
755
|
+
return column_name if view_definition.blank?
|
745
756
|
|
746
757
|
# Remove "CREATE VIEW ... AS SELECT ..." and then match the column name.
|
747
758
|
match_data = view_definition.sub(/CREATE\s+VIEW.*AS\s+SELECT\s/, '').match(/([\w-]*)\s+AS\s+#{column_name}\W/im)
|
@@ -36,9 +36,10 @@ module ActiveRecord
|
|
36
36
|
|
37
37
|
def cast_value(value)
|
38
38
|
value = super
|
39
|
-
return if value.blank?
|
40
39
|
|
41
|
-
value
|
40
|
+
return value unless value.is_a?(::Time)
|
41
|
+
|
42
|
+
value = value.change(year: 2000, month: 01, day: 01)
|
42
43
|
apply_seconds_precision(value)
|
43
44
|
end
|
44
45
|
|
@@ -4,9 +4,7 @@ require "tiny_tds"
|
|
4
4
|
require "base64"
|
5
5
|
require "active_record"
|
6
6
|
require "arel_sqlserver"
|
7
|
-
require "active_record/connection_adapters/abstract_adapter"
|
8
7
|
require "active_record/connection_adapters/sqlserver/core_ext/active_record"
|
9
|
-
require "active_record/connection_adapters/sqlserver/core_ext/calculations"
|
10
8
|
require "active_record/connection_adapters/sqlserver/core_ext/explain"
|
11
9
|
require "active_record/connection_adapters/sqlserver/core_ext/explain_subscriber"
|
12
10
|
require "active_record/connection_adapters/sqlserver/core_ext/attribute_methods"
|
@@ -431,36 +429,28 @@ module ActiveRecord
|
|
431
429
|
TYPE_MAP
|
432
430
|
end
|
433
431
|
|
434
|
-
def translate_exception(
|
432
|
+
def translate_exception(exception, message:, sql:, binds:)
|
435
433
|
case message
|
436
434
|
when /(SQL Server client is not connected)|(failed to execute statement)/i
|
437
|
-
ConnectionNotEstablished.new(message)
|
435
|
+
ConnectionNotEstablished.new(message, connection_pool: @pool)
|
438
436
|
when /(cannot insert duplicate key .* with unique index) | (violation of (unique|primary) key constraint)/i
|
439
|
-
RecordNotUnique.new(message, sql: sql, binds: binds)
|
437
|
+
RecordNotUnique.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
440
438
|
when /(conflicted with the foreign key constraint) | (The DELETE statement conflicted with the REFERENCE constraint)/i
|
441
|
-
InvalidForeignKey.new(message, sql: sql, binds: binds)
|
439
|
+
InvalidForeignKey.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
442
440
|
when /has been chosen as the deadlock victim/i
|
443
|
-
DeadlockVictim.new(message, sql: sql, binds: binds)
|
441
|
+
DeadlockVictim.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
444
442
|
when /database .* does not exist/i
|
445
|
-
NoDatabaseError.new(message)
|
443
|
+
NoDatabaseError.new(message, connection_pool: @pool)
|
446
444
|
when /data would be truncated/
|
447
|
-
ValueTooLong.new(message, sql: sql, binds: binds)
|
445
|
+
ValueTooLong.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
448
446
|
when /connection timed out/
|
449
|
-
StatementTimeout.new(message, sql: sql, binds: binds)
|
447
|
+
StatementTimeout.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
450
448
|
when /Column '(.*)' is not the same data type as referencing column '(.*)' in foreign key/
|
451
|
-
|
452
|
-
MismatchedForeignKey.new(
|
453
|
-
self,
|
454
|
-
message: message,
|
455
|
-
table: fk_id.schema,
|
456
|
-
foreign_key: fk_id.object,
|
457
|
-
target_table: pk_id.schema,
|
458
|
-
primary_key: pk_id.object
|
459
|
-
)
|
449
|
+
MismatchedForeignKey.new(message: message, connection_pool: @pool)
|
460
450
|
when /Cannot insert the value NULL into column.*does not allow nulls/
|
461
|
-
NotNullViolation.new(message, sql: sql, binds: binds)
|
451
|
+
NotNullViolation.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
462
452
|
when /Arithmetic overflow error/
|
463
|
-
RangeError.new(message, sql: sql, binds: binds)
|
453
|
+
RangeError.new(message, sql: sql, binds: binds, connection_pool: @pool)
|
464
454
|
else
|
465
455
|
super
|
466
456
|
end
|
@@ -7,17 +7,10 @@ require "models/post"
|
|
7
7
|
require "models/subscriber"
|
8
8
|
require "models/minimalistic"
|
9
9
|
require "models/college"
|
10
|
-
require "models/dog"
|
11
|
-
require "models/other_dog"
|
12
10
|
|
13
11
|
class AdapterTestSQLServer < ActiveRecord::TestCase
|
14
12
|
fixtures :tasks
|
15
13
|
|
16
|
-
let(:arunit_connection) { Topic.lease_connection }
|
17
|
-
let(:arunit2_connection) { College.lease_connection }
|
18
|
-
let(:arunit_database) { arunit_connection.pool.db_config.database }
|
19
|
-
let(:arunit2_database) { arunit2_connection.pool.db_config.database }
|
20
|
-
|
21
14
|
let(:basic_insert_sql) { "INSERT INTO [funny_jokes] ([name]) VALUES('Knock knock')" }
|
22
15
|
let(:basic_update_sql) { "UPDATE [customers] SET [address_street] = NULL WHERE [id] = 2" }
|
23
16
|
let(:basic_select_sql) { "SELECT * FROM [customers] WHERE ([customers].[id] = 1)" }
|
@@ -57,7 +50,8 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
|
|
57
50
|
assert Topic.table_exists?, "Topics table name of 'dbo.topics' should return true for exists."
|
58
51
|
|
59
52
|
# Test when database and owner included in table name.
|
60
|
-
|
53
|
+
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
|
54
|
+
Topic.table_name = "#{db_config.database}.dbo.topics"
|
61
55
|
assert Topic.table_exists?, "Topics table name of '[DATABASE].dbo.topics' should return true for exists."
|
62
56
|
ensure
|
63
57
|
Topic.table_name = "topics"
|
@@ -65,6 +59,12 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
|
|
65
59
|
end
|
66
60
|
|
67
61
|
it "test table existence across database schemas" do
|
62
|
+
arunit_connection = Topic.lease_connection
|
63
|
+
arunit2_connection = College.lease_connection
|
64
|
+
|
65
|
+
arunit_database = arunit_connection.pool.db_config.database
|
66
|
+
arunit2_database = arunit2_connection.pool.db_config.database
|
67
|
+
|
68
68
|
# Assert that connections use different default databases schemas.
|
69
69
|
assert_not_equal arunit_database, arunit2_database
|
70
70
|
|
@@ -200,9 +200,6 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
|
|
200
200
|
@identity_insert_sql_non_dbo_sp = "EXEC sp_executesql N'INSERT INTO [test].[aliens] ([id],[name]) VALUES (@0, @1)', N'@0 int, @1 nvarchar(255)', @0 = 420, @1 = N'Mork'"
|
201
201
|
@identity_insert_sql_non_dbo_unquoted_sp = "EXEC sp_executesql N'INSERT INTO test.aliens (id, name) VALUES (@0, @1)', N'@0 int, @1 nvarchar(255)', @0 = 420, @1 = N'Mork'"
|
202
202
|
@identity_insert_sql_non_dbo_unordered_sp = "EXEC sp_executesql N'INSERT INTO [test].[aliens] ([name],[id]) VALUES (@0, @1)', N'@0 nvarchar(255), @1 int', @0 = N'Mork', @1 = 420"
|
203
|
-
|
204
|
-
@non_identity_insert_sql_cross_database = "INSERT INTO #{arunit2_database}.dbo.dogs SELECT * FROM #{arunit_database}.dbo.dogs"
|
205
|
-
@identity_insert_sql_cross_database = "INSERT INTO #{arunit2_database}.dbo.dogs(id) SELECT id FROM #{arunit_database}.dbo.dogs"
|
206
203
|
end
|
207
204
|
|
208
205
|
it "return quoted table_name to #query_requires_identity_insert? when INSERT sql contains id column" do
|
@@ -219,32 +216,20 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
|
|
219
216
|
assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_sp)
|
220
217
|
assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_unquoted_sp)
|
221
218
|
assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_unordered_sp)
|
222
|
-
|
223
|
-
assert_equal "[#{arunit2_database}].[dbo].[dogs]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_cross_database)
|
224
219
|
end
|
225
220
|
|
226
221
|
it "return false to #query_requires_identity_insert? for normal SQL" do
|
227
|
-
[basic_insert_sql, basic_update_sql, basic_select_sql
|
222
|
+
[basic_insert_sql, basic_update_sql, basic_select_sql].each do |sql|
|
228
223
|
assert !connection.send(:query_requires_identity_insert?, sql), "SQL was #{sql}"
|
229
224
|
end
|
230
225
|
end
|
231
226
|
|
232
|
-
it "find identity column" do
|
227
|
+
it "find identity column using #identity_columns" do
|
233
228
|
task_id_column = Task.columns_hash["id"]
|
234
229
|
assert_equal task_id_column.name, connection.send(:identity_columns, Task.table_name).first.name
|
235
230
|
assert_equal task_id_column.sql_type, connection.send(:identity_columns, Task.table_name).first.sql_type
|
236
231
|
end
|
237
232
|
|
238
|
-
it "find identity column cross database" do
|
239
|
-
id_column = Dog.columns_hash["id"]
|
240
|
-
assert_equal id_column.name, arunit2_connection.send(:identity_columns, Dog.table_name).first.name
|
241
|
-
assert_equal id_column.sql_type, arunit2_connection.send(:identity_columns, Dog.table_name).first.sql_type
|
242
|
-
|
243
|
-
id_column = OtherDog.columns_hash["id"]
|
244
|
-
assert_equal id_column.name, arunit_connection.send(:identity_columns, OtherDog.table_name).first.name
|
245
|
-
assert_equal id_column.sql_type, arunit_connection.send(:identity_columns, OtherDog.table_name).first.sql_type
|
246
|
-
end
|
247
|
-
|
248
233
|
it "return an empty array when calling #identity_columns for a table_name with no identity" do
|
249
234
|
_(connection.send(:identity_columns, Subscriber.table_name)).must_equal []
|
250
235
|
end
|
@@ -479,13 +464,13 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
|
|
479
464
|
assert SSTestStringDefaultsView.lease_connection.data_source_exists?(SSTestStringDefaultsView.table_name)
|
480
465
|
end
|
481
466
|
|
482
|
-
# That have more than 4000 chars for their
|
467
|
+
# That have more than 4000 chars for their definition
|
483
468
|
|
484
|
-
it "cope with null returned for the
|
469
|
+
it "cope with null returned for the definition" do
|
485
470
|
assert_nothing_raised() { SSTestStringDefaultsBigView.columns }
|
486
471
|
end
|
487
472
|
|
488
|
-
it "using alternate view
|
473
|
+
it "using alternate view definition still be able to find real default" do
|
489
474
|
assert_equal "null", SSTestStringDefaultsBigView.new.pretend_null,
|
490
475
|
SSTestStringDefaultsBigView.columns_hash["pretend_null"].inspect
|
491
476
|
end
|
@@ -557,7 +542,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
|
|
557
542
|
@conn.execute("INSERT INTO [subscribers] ([nick]) VALUES ('aido')")
|
558
543
|
|
559
544
|
ActiveRecord::Base.while_preventing_writes do
|
560
|
-
assert_equal 1, @conn.execute("SELECT * FROM [subscribers] WHERE [subscribers].[nick] = 'aido'")
|
545
|
+
assert_equal 1, @conn.execute("SELECT * FROM [subscribers] WHERE [subscribers].[nick] = 'aido'").count
|
561
546
|
end
|
562
547
|
end
|
563
548
|
end
|
@@ -596,6 +581,28 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
|
|
596
581
|
end
|
597
582
|
end
|
598
583
|
|
584
|
+
describe "mismatched foreign keys error" do
|
585
|
+
def setup
|
586
|
+
@conn = ActiveRecord::Base.lease_connection
|
587
|
+
end
|
588
|
+
|
589
|
+
it 'raises an error when the foreign key is mismatched' do
|
590
|
+
error = assert_raises(ActiveRecord::MismatchedForeignKey) do
|
591
|
+
@conn.add_reference :engines, :old_car
|
592
|
+
@conn.add_foreign_key :engines, :old_cars
|
593
|
+
end
|
594
|
+
|
595
|
+
assert_match(
|
596
|
+
%r/Column 'old_cars\.id' is not the same data type as referencing column 'engines\.old_car_id' in foreign key '.*'/,
|
597
|
+
error.message
|
598
|
+
)
|
599
|
+
assert_not_nil error.cause
|
600
|
+
assert_equal @conn.pool, error.connection_pool
|
601
|
+
ensure
|
602
|
+
@conn.execute("ALTER TABLE engines DROP COLUMN old_car_id") rescue nil
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
599
606
|
describe "placeholder conditions" do
|
600
607
|
it 'using time placeholder' do
|
601
608
|
assert_equal Task.where("starting < ?", Time.now).count, 1
|
@@ -609,13 +616,4 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
|
|
609
616
|
assert_equal Task.where("starting < ?", DateTime.current).count, 1
|
610
617
|
end
|
611
618
|
end
|
612
|
-
|
613
|
-
describe "distinct select query" do
|
614
|
-
it "generated SQL does not contain unnecessary alias projection" do
|
615
|
-
sqls = capture_sql do
|
616
|
-
Post.includes(:comments).joins(:comments).first
|
617
|
-
end
|
618
|
-
assert_no_match(/AS alias_0/, sqls.first)
|
619
|
-
end
|
620
|
-
end
|
621
619
|
end
|