activerecord-jdbcsqlserver-adapter 50.0.0 → 52.0.0

Sign up to get free protection for your applications and to get access to all the features.
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