activerecord-jdbcsqlserver-adapter 50.0.0 → 52.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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/.travis.yml +4 -5
  4. data/CHANGELOG.md +22 -101
  5. data/{Dockerfile → Dockerfile.ci} +0 -0
  6. data/Gemfile +1 -3
  7. data/README.md +5 -9
  8. data/VERSION +1 -1
  9. data/activerecord-jdbcsqlserver-adapter.gemspec +2 -2
  10. data/appveyor.yml +1 -1
  11. data/docker-compose.ci.yml +7 -5
  12. data/lib/active_record/connection_adapters/sqlserver/core_ext/active_record.rb +3 -1
  13. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +3 -1
  14. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +51 -0
  15. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +18 -20
  16. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +5 -3
  17. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +43 -0
  18. data/lib/active_record/connection_adapters/sqlserver/core_ext/query_methods.rb +26 -0
  19. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +13 -2
  20. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +94 -28
  21. data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +1 -0
  22. data/lib/active_record/connection_adapters/sqlserver/jdbc_overrides.rb +5 -25
  23. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +24 -1
  24. data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +23 -2
  25. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +110 -74
  26. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +15 -7
  27. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +3 -4
  28. data/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb +0 -4
  29. data/lib/active_record/connection_adapters/sqlserver/type/data.rb +5 -0
  30. data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +3 -6
  31. data/lib/active_record/connection_adapters/sqlserver/type/json.rb +1 -1
  32. data/lib/active_record/connection_adapters/sqlserver/type/string.rb +7 -0
  33. data/lib/active_record/connection_adapters/sqlserver/type/time.rb +1 -0
  34. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +47 -24
  35. data/lib/active_record/tasks/sqlserver_database_tasks.rb +5 -3
  36. data/lib/activerecord-jdbcsqlserver-adapter.rb +4 -1
  37. data/lib/arel/visitors/sqlserver.rb +17 -4
  38. data/lib/arel_sqlserver.rb +0 -1
  39. data/lib/jdbc_mssql_driver_loader.rb +22 -0
  40. data/test/bin/install-freetds.sh +18 -0
  41. data/test/bin/setup.sh +19 -0
  42. data/test/cases/adapter_test_sqlserver.rb +43 -39
  43. data/test/cases/change_column_null_test_sqlserver.rb +42 -0
  44. data/test/cases/coerced_tests.rb +419 -39
  45. data/test/cases/column_test_sqlserver.rb +496 -462
  46. data/test/cases/connection_test_sqlserver.rb +2 -2
  47. data/test/cases/fetch_test_sqlserver.rb +5 -5
  48. data/test/cases/helper_sqlserver.rb +12 -1
  49. data/test/cases/json_test_sqlserver.rb +6 -6
  50. data/test/cases/migration_test_sqlserver.rb +13 -3
  51. data/test/cases/order_test_sqlserver.rb +19 -19
  52. data/test/cases/pessimistic_locking_test_sqlserver.rb +37 -20
  53. data/test/cases/rake_test_sqlserver.rb +20 -20
  54. data/test/cases/schema_dumper_test_sqlserver.rb +44 -43
  55. data/test/cases/schema_test_sqlserver.rb +2 -2
  56. data/test/cases/showplan_test_sqlserver.rb +25 -10
  57. data/test/cases/specific_schema_test_sqlserver.rb +11 -17
  58. data/test/cases/transaction_test_sqlserver.rb +9 -9
  59. data/test/cases/trigger_test_sqlserver.rb +31 -0
  60. data/test/cases/utils_test_sqlserver.rb +36 -36
  61. data/test/cases/uuid_test_sqlserver.rb +8 -8
  62. data/test/config.yml +2 -2
  63. data/test/migrations/create_clients_and_change_column_null.rb +23 -0
  64. data/test/models/sqlserver/trigger.rb +7 -0
  65. data/test/models/sqlserver/trigger_history.rb +3 -0
  66. data/test/schema/datatypes/2012.sql +1 -0
  67. data/test/schema/sqlserver_specific_schema.rb +47 -5
  68. data/test/support/core_ext/query_cache.rb +29 -0
  69. data/test/support/sql_counter_sqlserver.rb +1 -1
  70. metadata +32 -15
  71. data/RAILS5-TODO.md +0 -5
  72. data/test/models/sqlserver/dot_table_name.rb +0 -3
@@ -5,10 +5,13 @@ module ActiveRecord
5
5
  module ColumnMethods
6
6
 
7
7
  def primary_key(name, type = :primary_key, **options)
8
- return super unless type == :uuid
9
- options[:default] = options.fetch(:default, 'NEWID()')
10
- options[:primary_key] = true
11
- column name, type, options
8
+ if [:integer, :bigint].include?(type)
9
+ options[:is_identity] = true unless options.key?(:default)
10
+ elsif type == :uuid
11
+ options[:default] = options.fetch(:default, 'NEWID()')
12
+ options[:primary_key] = true
13
+ end
14
+ super
12
15
  end
13
16
 
14
17
  def primary_key_nonclustered(*args, **options)
@@ -98,9 +101,14 @@ module ActiveRecord
98
101
  class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
99
102
  include ColumnMethods
100
103
 
101
- def new_column_definition(name, type, options)
102
- type = :datetime2 if type == :datetime && options[:precision]
103
- super name, type, options
104
+ def new_column_definition(name, type, **options)
105
+ case type
106
+ when :datetime
107
+ type = :datetime2 if options[:precision]
108
+ when :primary_key
109
+ options[:is_identity] = true
110
+ end
111
+ super
104
112
  end
105
113
  end
106
114
 
@@ -26,13 +26,13 @@ module ActiveRecord
26
26
 
27
27
  end
28
28
 
29
- Transaction.send :include, SQLServerTransaction
29
+ Transaction.send :prepend, SQLServerTransaction
30
30
 
31
31
  module SQLServerRealTransaction
32
32
 
33
33
  attr_reader :starting_isolation_level
34
34
 
35
- def initialize(connection, options, run_commit_callbacks: false)
35
+ def initialize(connection, options, *args)
36
36
  @connection = connection
37
37
  @starting_isolation_level = current_isolation_level if options[:isolation]
38
38
  super
@@ -58,7 +58,6 @@ module ActiveRecord
58
58
 
59
59
  end
60
60
 
61
- RealTransaction.send :include, SQLServerRealTransaction
62
-
61
+ RealTransaction.send :prepend, SQLServerRealTransaction
63
62
  end
64
63
  end
@@ -4,10 +4,6 @@ module ActiveRecord
4
4
  module Type
5
5
  class BigInteger < Integer
6
6
 
7
- def type
8
- :bigint
9
- end
10
-
11
7
  def sqlserver_type
12
8
  'bigint'.freeze
13
9
  end
@@ -23,6 +23,11 @@ module ActiveRecord
23
23
  @value.inspect
24
24
  end
25
25
 
26
+ def eql?(other)
27
+ self.class == other.class && self.value == other.value
28
+ end
29
+ alias :== :eql?
30
+
26
31
  end
27
32
  end
28
33
  end
@@ -51,7 +51,9 @@ module ActiveRecord
51
51
  private
52
52
 
53
53
  def fast_string_to_time(string)
54
- fast_string_to_time_zone.strptime(string, fast_string_to_time_format).time
54
+ time = ActiveSupport::TimeZone['UTC'].strptime(string, fast_string_to_time_format)
55
+ new_time(time.year, time.month, time.day, time.hour,
56
+ time.min, time.sec, Rational(time.nsec, 1_000))
55
57
  rescue ArgumentError
56
58
  super
57
59
  end
@@ -59,11 +61,6 @@ module ActiveRecord
59
61
  def fast_string_to_time_format
60
62
  "#{::Time::DATE_FORMATS[:_sqlserver_datetime]}.%N".freeze
61
63
  end
62
-
63
- def fast_string_to_time_zone
64
- ::Time.zone || ActiveSupport::TimeZone['UTC']
65
- end
66
-
67
64
  end
68
65
  end
69
66
  end
@@ -2,7 +2,7 @@ module ActiveRecord
2
2
  module ConnectionAdapters
3
3
  module SQLServer
4
4
  module Type
5
- class Json < ActiveRecord::Type::Internal::AbstractJson
5
+ class Json < ActiveRecord::Type::Json
6
6
 
7
7
  end
8
8
  end
@@ -4,6 +4,13 @@ module ActiveRecord
4
4
  module Type
5
5
  class String < ActiveRecord::Type::String
6
6
 
7
+ def changed_in_place?(raw_old_value, new_value)
8
+ if raw_old_value.is_a?(Data)
9
+ raw_old_value.value != new_value
10
+ else
11
+ super
12
+ end
13
+ end
7
14
 
8
15
  end
9
16
  end
@@ -20,6 +20,7 @@ module ActiveRecord
20
20
 
21
21
  # Currently only called from our custom Time type for formatting
22
22
  def _formatted(value)
23
+ return "#{value.to_s(:_sqlserver_time)}" unless precision
23
24
  "#{value.to_s(:_sqlserver_time)}.#{quote_fractional(value)}"
24
25
  end
25
26
 
@@ -3,9 +3,12 @@ require 'active_record'
3
3
  require 'arel_sqlserver'
4
4
  require 'active_record/connection_adapters/abstract_adapter'
5
5
  require 'active_record/connection_adapters/sqlserver/core_ext/active_record'
6
+ require 'active_record/connection_adapters/sqlserver/core_ext/calculations'
6
7
  require 'active_record/connection_adapters/sqlserver/core_ext/explain' unless defined? JRUBY_VERSION
7
8
  require 'active_record/connection_adapters/sqlserver/core_ext/explain_subscriber'
8
9
  require 'active_record/connection_adapters/sqlserver/core_ext/attribute_methods'
10
+ require 'active_record/connection_adapters/sqlserver/core_ext/finder_methods'
11
+ require 'active_record/connection_adapters/sqlserver/core_ext/query_methods'
9
12
  require 'active_record/connection_adapters/sqlserver/version'
10
13
  require 'active_record/connection_adapters/sqlserver/type'
11
14
  require 'active_record/connection_adapters/sqlserver/database_limits'
@@ -33,7 +36,6 @@ module ActiveRecord
33
36
  SQLServer::Quoting,
34
37
  SQLServer::DatabaseStatements,
35
38
  SQLServer::Showplan,
36
- SQLServer::SchemaDumper,
37
39
  SQLServer::SchemaStatements,
38
40
  SQLServer::DatabaseLimits,
39
41
  SQLServer::DatabaseTasks
@@ -52,15 +54,20 @@ module ActiveRecord
52
54
 
53
55
  ADAPTER_NAME = 'SQLServer'.freeze
54
56
 
57
+ # Default precision for 'time' (See https://docs.microsoft.com/en-us/sql/t-sql/data-types/time-transact-sql)
58
+ DEFAULT_TIME_PRECISION = 7
59
+
55
60
  attr_reader :spid
56
61
 
57
62
  cattr_accessor :cs_equality_operator, instance_accessor: false
58
63
  cattr_accessor :use_output_inserted, instance_accessor: false
64
+ cattr_accessor :exclude_output_inserted_table_names, instance_accessor: false
59
65
  cattr_accessor :showplan_option, instance_accessor: false
60
66
  cattr_accessor :lowercase_schema_reflection
61
67
 
62
68
  self.cs_equality_operator = 'COLLATE Latin1_General_CS_AS_WS'
63
69
  self.use_output_inserted = true
70
+ self.exclude_output_inserted_table_names = Concurrent::Map.new { false }
64
71
 
65
72
  def initialize(connection, logger = nil, config = {})
66
73
  super(connection, logger, config)
@@ -85,14 +92,6 @@ module ActiveRecord
85
92
  SQLServer::SchemaCreation.new self
86
93
  end
87
94
 
88
- def supports_migrations?
89
- true
90
- end
91
-
92
- def supports_primary_key?
93
- true
94
- end
95
-
96
95
  def supports_ddl_transactions?
97
96
  true
98
97
  end
@@ -153,16 +152,20 @@ module ActiveRecord
153
152
  false
154
153
  end
155
154
 
155
+ def supports_savepoints?
156
+ true
157
+ end
158
+
156
159
  def supports_in_memory_oltp?
157
160
  @version_year >= 2014
158
161
  end
159
162
 
160
163
  def disable_referential_integrity
161
164
  tables = tables_with_referential_integrity
162
- tables.each { |t| do_execute "ALTER TABLE #{t} NOCHECK CONSTRAINT ALL" }
165
+ tables.each { |t| do_execute "ALTER TABLE #{quote_table_name(t)} NOCHECK CONSTRAINT ALL" }
163
166
  yield
164
167
  ensure
165
- tables.each { |t| do_execute "ALTER TABLE #{t} CHECK CONSTRAINT ALL" }
168
+ tables.each { |t| do_execute "ALTER TABLE #{quote_table_name(t)} CHECK CONSTRAINT ALL" }
166
169
  end
167
170
 
168
171
  # === Abstract Adapter (Connection Management) ================== #
@@ -206,7 +209,7 @@ module ActiveRecord
206
209
 
207
210
  def tables_with_referential_integrity
208
211
  schemas_and_tables = select_rows <<-SQL.strip_heredoc
209
- SELECT s.name AS schema_name, o.name AS table_name
212
+ SELECT DISTINCT s.name AS schema_name, o.name AS table_name
210
213
  FROM sys.foreign_keys i
211
214
  INNER JOIN sys.objects o ON i.parent_object_id = o.OBJECT_ID
212
215
  INNER JOIN sys.schemas s ON o.schema_id = s.schema_id
@@ -229,7 +232,7 @@ module ActiveRecord
229
232
  end
230
233
 
231
234
  def sqlserver_azure?
232
- @sqlserver_azure ||= !!(select_value('SELECT @@version', 'SCHEMA') =~ /Azure/i)
235
+ !!(sqlserver_version =~ /Azure/i)
233
236
  end
234
237
 
235
238
  def database_prefix_remote_server?
@@ -242,6 +245,14 @@ module ActiveRecord
242
245
  @connection_options[:database_prefix]
243
246
  end
244
247
 
248
+ def database_prefix_identifier(name)
249
+ if database_prefix_remote_server?
250
+ SQLServer::Utils.extract_identifiers("#{database_prefix}#{name}")
251
+ else
252
+ SQLServer::Utils.extract_identifiers(name)
253
+ end
254
+ end
255
+
245
256
  def version
246
257
  self.class::VERSION
247
258
  end
@@ -262,7 +273,7 @@ module ActiveRecord
262
273
 
263
274
  # === Abstract Adapter (Misc Support) =========================== #
264
275
 
265
- def initialize_type_map(m)
276
+ def initialize_type_map(m = type_map)
266
277
  m.register_type %r{.*}, SQLServer::Type::UnicodeString.new
267
278
  # Exact Numerics
268
279
  register_class_with_limit m, 'bigint(8)', SQLServer::Type::BigInteger
@@ -302,8 +313,7 @@ module ActiveRecord
302
313
  end
303
314
  m.register_type 'smalldatetime', SQLServer::Type::SmallDateTime.new
304
315
  m.register_type %r{\Atime}i do |sql_type|
305
- scale = extract_scale(sql_type)
306
- precision = extract_precision(sql_type)
316
+ precision = extract_precision(sql_type) || DEFAULT_TIME_PRECISION
307
317
  SQLServer::Type::Time.new precision: precision
308
318
  end
309
319
  # Character Strings
@@ -339,6 +349,20 @@ module ActiveRecord
339
349
  NoDatabaseError.new(message)
340
350
  when /data would be truncated/
341
351
  ValueTooLong.new(message)
352
+ when /Column '(.*)' is not the same data type as referencing column '(.*)' in foreign key/
353
+ pk_id, fk_id = SQLServer::Utils.extract_identifiers($1), SQLServer::Utils.extract_identifiers($2)
354
+ MismatchedForeignKey.new(
355
+ self,
356
+ message: message,
357
+ table: fk_id.schema,
358
+ foreign_key: fk_id.object,
359
+ target_table: pk_id.schema,
360
+ primary_key: pk_id.object
361
+ )
362
+ when /Cannot insert the value NULL into column.*does not allow nulls/
363
+ NotNullViolation.new(message)
364
+ when /Arithmetic overflow error/
365
+ RangeError.new(message)
342
366
  else
343
367
  super
344
368
  end
@@ -430,16 +454,15 @@ module ActiveRecord
430
454
  end
431
455
 
432
456
  def version_year
433
- return @version_year if defined?(@version_year)
434
- @version_year = begin
435
- vstring = _raw_select('SELECT @@version', fetch: :rows).first.first.to_s
436
- return 2016 if vstring =~ /vNext/
437
- /SQL Server (\d+)/.match(vstring).to_a.last.to_s.to_i
438
- rescue Exception => e
439
- 2016
440
- end
457
+ return 2016 if sqlserver_version =~ /vNext/
458
+ /SQL Server (\d+)/.match(sqlserver_version).to_a.last.to_s.to_i
459
+ rescue StandardError => e
460
+ 2016
441
461
  end
442
462
 
463
+ def sqlserver_version
464
+ @sqlserver_version ||= _raw_select('SELECT @@version', fetch: :rows).first.first.to_s
465
+ end
443
466
  end
444
467
  end
445
468
  end
@@ -48,10 +48,12 @@ module ActiveRecord
48
48
  create true
49
49
  end
50
50
 
51
- def structure_dump(filename)
51
+ def structure_dump(filename, extra_flags)
52
+ server_arg = "-S #{Shellwords.escape(configuration['host'])}"
53
+ server_arg += ":#{Shellwords.escape(configuration['port'])}" if configuration['port']
52
54
  command = [
53
55
  "defncopy",
54
- "-S #{Shellwords.escape(configuration['host'])}",
56
+ server_arg,
55
57
  "-D #{Shellwords.escape(configuration['database'])}",
56
58
  "-U #{Shellwords.escape(configuration['username'])}",
57
59
  "-P #{Shellwords.escape(configuration['password'])}",
@@ -71,7 +73,7 @@ module ActiveRecord
71
73
  File.open(filename, "w") { |file| file.puts dump }
72
74
  end
73
75
 
74
- def structure_load(filename)
76
+ def structure_load(filename, extra_flags)
75
77
  connection.execute File.read(filename)
76
78
  end
77
79
 
@@ -1,8 +1,11 @@
1
1
  # Our core date/time overrides to support prepared statements
2
2
  require 'active_record/connection_adapters/sqlserver/core_ext/date_time'
3
3
 
4
+ require 'active_support' # Need this for the next line
5
+ require 'active_record/log_subscriber' # Need to make sure this is loaded before we load Core for monkey patching
6
+
4
7
  # Load the jar file for the jdbc driver
5
- require 'jdbc/mssql'
8
+ require_relative './jdbc_mssql_driver_loader'
6
9
 
7
10
  # Standadard arjdbc functionality
8
11
  require 'arjdbc/abstract/connection_management'
@@ -14,7 +14,7 @@ module Arel
14
14
  # SQLServer ToSql/Visitor (Overides)
15
15
 
16
16
  def visit_Arel_Nodes_BindParam o, collector
17
- collector.add_bind(o) { |i| "@#{i-1}" }
17
+ collector.add_bind(o.value) { |i| "@#{i-1}" }
18
18
  end unless defined? JRUBY_VERSION # converts bind argument markers "?" to "@n", but JDBC wants "?"
19
19
 
20
20
  def visit_Arel_Nodes_Bin o, collector
@@ -95,17 +95,29 @@ module Arel
95
95
  collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector
96
96
  end
97
97
  if o.right.any?
98
- collector << " " if o.left
98
+ collector << SPACE if o.left
99
99
  collector = inject_join o.right, collector, ' '
100
100
  end
101
101
  collector
102
102
  end
103
103
 
104
+ def visit_Arel_Nodes_InnerJoin o, collector
105
+ collector << "INNER JOIN "
106
+ collector = visit o.left, collector
107
+ collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
108
+ if o.right
109
+ collector << SPACE
110
+ visit(o.right, collector)
111
+ else
112
+ collector
113
+ end
114
+ end
115
+
104
116
  def visit_Arel_Nodes_OuterJoin o, collector
105
117
  collector << "LEFT OUTER JOIN "
106
118
  collector = visit o.left, collector
107
119
  collector = visit_Arel_Nodes_SelectStatement_SQLServer_Lock collector, space: true
108
- collector << " "
120
+ collector << SPACE
109
121
  visit o.right, collector
110
122
  end
111
123
 
@@ -190,7 +202,8 @@ module Arel
190
202
 
191
203
  def primary_Key_From_Table t
192
204
  return unless t
193
- column_name = schema_cache.primary_keys(t.name) || column_cache(t.name).first.try(:second).try(:name)
205
+ column_name = @connection.schema_cache.primary_keys(t.name) ||
206
+ @connection.schema_cache.columns_hash(t.name).first.try(:second).try(:name)
194
207
  column_name ? t[column_name] : nil
195
208
  end
196
209
 
@@ -1,3 +1,2 @@
1
1
  require 'arel'
2
- require 'arel/visitors/bind_visitor'
3
2
  require 'arel/visitors/sqlserver'
@@ -0,0 +1,22 @@
1
+ module JdbcMssqlDriverLoader
2
+ def self.check_and_maybe_load_driver
3
+ driver_name = "com.microsoft.sqlserver.jdbc.SQLServerDriver"
4
+ if (Java::JavaClass.for_name(driver_name) rescue nil)
5
+ driver = Java::ComMicrosoftSqlserverJdbc::SQLServerDriver.new
6
+ which = driver
7
+ .getClass().getClassLoader().loadClass(driver_name)
8
+ .getProtectionDomain().getCodeSource().getLocation().to_s
9
+ warn "You already required a mssql jdbc driver (#{which}), skipping gem jdbc-mssql"
10
+
11
+ major_version = driver.major_version
12
+ required_major_version = 8
13
+ if major_version < required_major_version
14
+ raise "MSSQL jdbc driver version is to old (given major version #{major_version} < required major version #{required_major_version})"
15
+ end
16
+ else
17
+ require "jdbc/mssql"
18
+ end
19
+ end
20
+
21
+ check_and_maybe_load_driver
22
+ end
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -x
4
+ set -e
5
+
6
+ FREETDS_VERSION=1.00.21
7
+
8
+ wget http://www.freetds.org/files/stable/freetds-$FREETDS_VERSION.tar.gz
9
+ tar -xzf freetds-$FREETDS_VERSION.tar.gz
10
+ cd freetds-$FREETDS_VERSION
11
+ ./configure --prefix=/opt/local \
12
+ --with-openssl=/opt/local \
13
+ --with-tdsver=7.3
14
+ make
15
+ make install
16
+ cd ..
17
+ rm -rf freetds-$FREETDS_VERSION
18
+ rm freetds-$FREETDS_VERSION.tar.gz