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
@@ -0,0 +1,26 @@
1
+ require 'active_record/relation'
2
+ require 'active_record/version'
3
+
4
+ module ActiveRecord
5
+ module ConnectionAdapters
6
+ module SQLServer
7
+ module CoreExt
8
+ module QueryMethods
9
+
10
+ private
11
+
12
+ # Copy of original from Rails master. This patch can be removed when adapter supports Rails 6.
13
+ def table_name_matches?(from)
14
+ table_name = Regexp.escape(table.name)
15
+ quoted_table_name = Regexp.escape(connection.quote_table_name(table.name))
16
+ /(?:\A|(?<!FROM)\s)(?:\b#{table_name}\b|#{quoted_table_name})(?!\.)/i.match?(from.to_s)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ ActiveSupport.on_load(:active_record) do
25
+ ActiveRecord::Relation.include(ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::QueryMethods)
26
+ end
@@ -2,7 +2,6 @@ module ActiveRecord
2
2
  module ConnectionAdapters
3
3
  module SQLServer
4
4
  module DatabaseLimits
5
-
6
5
  def table_alias_length
7
6
  128
8
7
  end
@@ -32,7 +31,7 @@ module ActiveRecord
32
31
  end
33
32
 
34
33
  def in_clause_length
35
- 65_536
34
+ 10_000
36
35
  end
37
36
 
38
37
  def sql_query_length
@@ -43,6 +42,18 @@ module ActiveRecord
43
42
  256
44
43
  end
45
44
 
45
+ private
46
+
47
+ # The max number of binds is 2100, but because sp_executesql takes
48
+ # the first 2 params as the query string and the list of types,
49
+ # we have only 2098 spaces left
50
+ def bind_params_length
51
+ 2_098
52
+ end
53
+
54
+ def insert_rows_length
55
+ 1_000
56
+ end
46
57
  end
47
58
  end
48
59
  end
@@ -3,10 +3,6 @@ module ActiveRecord
3
3
  module SQLServer
4
4
  module DatabaseStatements
5
5
 
6
- def select_rows(sql, name = nil, binds = [])
7
- sp_executesql sql, name, binds, fetch: :rows
8
- end
9
-
10
6
  def execute(sql, name = nil)
11
7
  if id_insert_table_name = query_requires_identity_insert?(sql)
12
8
  with_identity_insert_enabled(id_insert_table_name) { do_execute(sql, name) }
@@ -19,26 +15,22 @@ module ActiveRecord
19
15
  sp_executesql(sql, name, binds, prepare: prepare)
20
16
  end
21
17
 
22
- def exec_insert(sql, name, binds, pk = nil, _sequence_name = nil)
18
+ def exec_insert(sql, name = nil, binds = [], pk = nil, _sequence_name = nil)
23
19
  if id_insert_table_name = exec_insert_requires_identity?(sql, pk, binds)
24
- with_identity_insert_enabled(id_insert_table_name) { exec_query(sql, name, binds) }
20
+ with_identity_insert_enabled(id_insert_table_name) { super(sql, name, binds, pk) }
25
21
  else
26
- exec_query(sql, name, binds)
22
+ super(sql, name, binds, pk)
27
23
  end
28
24
  end
29
25
 
30
26
  def exec_delete(sql, name, binds)
31
- sql << '; SELECT @@ROWCOUNT AS AffectedRows'
32
- super.rows.first.first
27
+ sql = sql.dup << '; SELECT @@ROWCOUNT AS AffectedRows'
28
+ super(sql, name, binds).rows.first.first
33
29
  end
34
30
 
35
31
  def exec_update(sql, name, binds)
36
- sql << '; SELECT @@ROWCOUNT AS AffectedRows'
37
- super.rows.first.first
38
- end
39
-
40
- def supports_statement_cache?
41
- true
32
+ sql = sql.dup << '; SELECT @@ROWCOUNT AS AffectedRows'
33
+ super(sql, name, binds).rows.first.first
42
34
  end
43
35
 
44
36
  def begin_db_transaction
@@ -80,18 +72,56 @@ module ActiveRecord
80
72
  end
81
73
 
82
74
  def case_sensitive_comparison(table, attribute, column, value)
83
- if value && value.acts_like?(:string)
84
- table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new))
75
+ if column.collation && !column.case_sensitive?
76
+ table[attribute].eq(Arel::Nodes::Bin.new(value))
85
77
  else
86
78
  super
87
79
  end
88
80
  end
89
81
 
82
+ # We should propose this change to Rails team
83
+ def insert_fixtures_set(fixture_set, tables_to_delete = [])
84
+ fixture_inserts = []
85
+
86
+ fixture_set.each do |table_name, fixtures|
87
+ fixtures.each_slice(insert_rows_length) do |batch|
88
+ fixture_inserts << build_fixture_sql(batch, table_name)
89
+ end
90
+ end
91
+
92
+ table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}".dup }
93
+ total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts))
94
+
95
+ disable_referential_integrity do
96
+ transaction(requires_new: true) do
97
+ total_sql.each do |sql|
98
+ execute sql, "Fixtures Load"
99
+ yield if block_given?
100
+ end
101
+ end
102
+ end
103
+ end
104
+
90
105
  def can_perform_case_insensitive_comparison_for?(column)
91
- column.type == :string
106
+ column.type == :string && (!column.collation || column.case_sensitive?)
92
107
  end
93
108
  private :can_perform_case_insensitive_comparison_for?
94
109
 
110
+ def combine_multi_statements(total_sql)
111
+ total_sql
112
+ end
113
+ private :combine_multi_statements
114
+
115
+ def default_insert_value(column)
116
+ if column.is_identity?
117
+ table_name = quote(quote_table_name(column.table_name))
118
+ Arel.sql("IDENT_CURRENT(#{table_name}) + IDENT_INCR(#{table_name})")
119
+ else
120
+ super
121
+ end
122
+ end
123
+ private :default_insert_value
124
+
95
125
  # === SQLServer Specific ======================================== #
96
126
 
97
127
  def execute_procedure(proc_name, *variables)
@@ -196,9 +226,20 @@ module ActiveRecord
196
226
  table_name = query_requires_identity_insert?(sql)
197
227
  pk = primary_key(table_name)
198
228
  end
199
- sql = if pk && self.class.use_output_inserted && !database_prefix_remote_server?
229
+ sql = if pk && use_output_inserted? && !database_prefix_remote_server?
200
230
  quoted_pk = SQLServer::Utils.extract_identifiers(pk).quoted
201
- sql.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk}"
231
+ table_name ||= get_table_name(sql)
232
+ exclude_output_inserted = exclude_output_inserted_table_name?(table_name, sql)
233
+ if exclude_output_inserted
234
+ id_sql_type = exclude_output_inserted.is_a?(TrueClass) ? 'bigint' : exclude_output_inserted
235
+ <<-SQL.strip_heredoc
236
+ DECLARE @ssaIdInsertTable table (#{quoted_pk} #{id_sql_type});
237
+ #{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk} INTO @ssaIdInsertTable"}
238
+ SELECT CAST(#{quoted_pk} AS #{id_sql_type}) FROM @ssaIdInsertTable
239
+ SQL
240
+ else
241
+ sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk}"
242
+ end
202
243
  else
203
244
  "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
204
245
  end
@@ -231,6 +272,8 @@ module ActiveRecord
231
272
  def sp_executesql_types_and_parameters(binds)
232
273
  types, params = [], []
233
274
  binds.each_with_index do |attr, index|
275
+ attr = attr.value if attr.is_a?(Arel::Nodes::BindParam)
276
+
234
277
  types << "@#{index} #{sp_executesql_sql_type(attr)}"
235
278
  params << sp_executesql_sql_param(attr)
236
279
  end
@@ -241,19 +284,19 @@ module ActiveRecord
241
284
  return attr.type.sqlserver_type if attr.type.respond_to?(:sqlserver_type)
242
285
  case value = attr.value_for_database
243
286
  when Numeric
244
- 'int'.freeze
287
+ value > 2_147_483_647 ? 'bigint'.freeze : 'int'.freeze
245
288
  else
246
289
  'nvarchar(max)'.freeze
247
290
  end
248
291
  end
249
292
 
250
293
  def sp_executesql_sql_param(attr)
251
- case attr.value_for_database
294
+ case value = attr.value_for_database
252
295
  when Type::Binary::Data,
253
296
  ActiveRecord::Type::SQLServer::Data
254
- quote(attr.value_for_database)
297
+ quote(value)
255
298
  else
256
- quote(type_cast(attr.value_for_database))
299
+ quote(type_cast(value))
257
300
  end
258
301
  end
259
302
 
@@ -261,7 +304,7 @@ module ActiveRecord
261
304
  if name == 'EXPLAIN'
262
305
  params.each.with_index do |param, index|
263
306
  substitute_at_finder = /(@#{index})(?=(?:[^']|'[^']*')*$)/ # Finds unquoted @n values.
264
- sql.sub! substitute_at_finder, param.to_s
307
+ sql = sql.sub substitute_at_finder, param.to_s
265
308
  end
266
309
  else
267
310
  types = quote(types.join(', '))
@@ -275,7 +318,14 @@ module ActiveRecord
275
318
  def raw_connection_do(sql)
276
319
  case @connection_options[:mode]
277
320
  when :dblib
278
- @connection.execute(sql).do
321
+ result = @connection.execute(sql)
322
+
323
+ # TinyTDS returns false instead of raising an exception if connection fails.
324
+ # Getting around this by raising an exception ourselves while this PR
325
+ # https://github.com/rails-sqlserver/tiny_tds/pull/469 is not released.
326
+ raise TinyTds::Error, "failed to execute statement" if result.is_a?(FalseClass)
327
+
328
+ result.do
279
329
  end
280
330
  ensure
281
331
  @update_sql = false
@@ -283,15 +333,31 @@ module ActiveRecord
283
333
 
284
334
  # === SQLServer Specific (Identity Inserts) ===================== #
285
335
 
336
+ def use_output_inserted?
337
+ self.class.use_output_inserted
338
+ end
339
+
340
+ def exclude_output_inserted_table_names?
341
+ !self.class.exclude_output_inserted_table_names.empty?
342
+ end
343
+
344
+ def exclude_output_inserted_table_name?(table_name, sql)
345
+ return false unless exclude_output_inserted_table_names?
346
+ table_name ||= get_table_name(sql)
347
+ return false unless table_name
348
+ self.class.exclude_output_inserted_table_names[table_name]
349
+ end
350
+
286
351
  def exec_insert_requires_identity?(sql, pk, binds)
287
- query_requires_identity_insert?(sql) if pk && binds.map(&:name).include?(pk)
352
+ query_requires_identity_insert?(sql)
288
353
  end
289
354
 
290
355
  def query_requires_identity_insert?(sql)
291
356
  if insert_sql?(sql)
292
357
  table_name = get_table_name(sql)
293
358
  id_column = identity_columns(table_name).first
294
- id_column && sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)[^(]+\([^)]*\b(#{id_column.name})\b,?[^)]*\)/i ? quote_table_name(table_name) : false
359
+ # id_column && sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)[^(]+\([^)]*\b(#{id_column.name})\b,?[^)]*\)/i ? quote_table_name(table_name) : false
360
+ id_column && sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)[^(]+\([^)]*\b(#{id_column.name})\b,?[^)]*\)/i ? table_name : false
295
361
  else
296
362
  false
297
363
  end
@@ -12,6 +12,7 @@ module ActiveRecord
12
12
 
13
13
  def drop_database(database)
14
14
  name = SQLServer::Utils.extract_identifiers(database)
15
+ do_execute "ALTER DATABASE #{name} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
15
16
  do_execute "DROP DATABASE #{name}"
16
17
  end
17
18
 
@@ -90,18 +90,6 @@ module ActiveRecord
90
90
  @connection.configure_connection
91
91
  end
92
92
 
93
- # @Overwrite
94
- # Had some special logic and skipped using gem's internal query methods
95
- def select_rows(sql, name = nil, binds = [])
96
-
97
- # In some cases the limit is converted to a `TOP(1)` but the bind parameter is still in the array
98
- if !binds.empty? && sql.include?('TOP(1)')
99
- binds = binds.delete_if {|b| b.name == 'LIMIT' }
100
- end
101
-
102
- exec_query(sql, name, binds).rows
103
- end
104
-
105
93
  # Have to reset this because the default arjdbc functionality is to return false unless a level is passed in
106
94
  def supports_transaction_isolation?
107
95
  true
@@ -152,25 +140,17 @@ module ActiveRecord
152
140
  [sql, binds, pk, sequence_name]
153
141
  end
154
142
 
143
+ # @Override original version uses driver specific query command
144
+ def sqlserver_version
145
+ @sqlserver_version ||= select_value('SELECT @@version').to_s
146
+ end
147
+
155
148
  # @Override
156
149
  def translate_exception(exception, message)
157
150
  return ActiveRecord::ValueTooLong.new(message) if exception.message.include?('java.sql.DataTruncation')
158
151
  super
159
152
  end
160
153
 
161
- # @Overwrite
162
- # Made it so we don't use the internal calls from the gem
163
- def version_year
164
- return @version_year if defined?(@version_year)
165
- @version_year = begin
166
- vstring = select_value('SELECT @@version').to_s
167
- return 2016 if vstring =~ /vNext/
168
- /SQL Server (\d+)/.match(vstring).to_a.last.to_s.to_i
169
- rescue Exception => e
170
- 2016
171
- end
172
- end
173
-
174
154
  private
175
155
 
176
156
  def _quote(value)
@@ -8,7 +8,8 @@ module ActiveRecord
8
8
  def visit_TableDefinition(o)
9
9
  if o.as
10
10
  table_name = quote_table_name(o.temporary ? "##{o.name}" : o.name)
11
- projections, source = @conn.to_sql(o.as).match(%r{SELECT\s+(.*)?\s+FROM\s+(.*)?}).captures
11
+ query = o.as.respond_to?(:to_sql) ? o.as.to_sql : o.as
12
+ projections, source = query.match(%r{SELECT\s+(.*)?\s+FROM\s+(.*)?}).captures
12
13
  select_into = "SELECT #{projections} INTO #{table_name} FROM #{source}"
13
14
  else
14
15
  o.instance_variable_set :@as, nil
@@ -16,6 +17,20 @@ module ActiveRecord
16
17
  end
17
18
  end
18
19
 
20
+ def add_column_options!(sql, options)
21
+ sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
22
+ if options[:null] == false
23
+ sql << " NOT NULL"
24
+ end
25
+ if options[:is_identity] == true
26
+ sql << " IDENTITY(1,1)"
27
+ end
28
+ if options[:primary_key] == true
29
+ sql << " PRIMARY KEY"
30
+ end
31
+ sql
32
+ end
33
+
19
34
  def action_sql(action, dependency)
20
35
  case dependency
21
36
  when :restrict
@@ -28,6 +43,14 @@ module ActiveRecord
28
43
  end
29
44
  end
30
45
 
46
+ def options_include_default?(options)
47
+ super || options_primary_key_with_nil_default?(options)
48
+ end
49
+
50
+ def options_primary_key_with_nil_default?(options)
51
+ options[:primary_key] && options.include?(:default) && options[:default].nil?
52
+ end
53
+
31
54
  end
32
55
  end
33
56
  end
@@ -1,13 +1,34 @@
1
1
  module ActiveRecord
2
2
  module ConnectionAdapters
3
3
  module SQLServer
4
- module SchemaDumper
4
+ class SchemaDumper < ConnectionAdapters::SchemaDumper
5
+
6
+ SQLSEVER_NO_LIMIT_TYPES = [
7
+ 'text',
8
+ 'ntext',
9
+ 'varchar(max)',
10
+ 'nvarchar(max)',
11
+ 'varbinary(max)'
12
+ ].freeze
5
13
 
6
14
  private
7
15
 
16
+ def explicit_primary_key_default?(column)
17
+ column.is_primary? && !column.is_identity?
18
+ end
19
+
20
+ def schema_limit(column)
21
+ return if SQLSEVER_NO_LIMIT_TYPES.include?(column.sql_type)
22
+ super
23
+ end
24
+
8
25
  def schema_collation(column)
9
26
  return unless column.collation
10
- column.collation if column.collation != collation
27
+ column.collation if column.collation != @connection.collation
28
+ end
29
+
30
+ def default_primary_key?(column)
31
+ super && column.is_primary? && column.is_identity?
11
32
  end
12
33
 
13
34
  end
@@ -7,35 +7,6 @@ module ActiveRecord
7
7
  @native_database_types ||= initialize_native_database_types.freeze
8
8
  end
9
9
 
10
- def tables(name = nil)
11
- ActiveSupport::Deprecation.warn 'Passing arguments to #tables is deprecated without replacement.' if name
12
- tables_sql('BASE TABLE')
13
- end
14
-
15
- def table_exists?(table_name)
16
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
17
- #table_exists? currently checks both tables and views.
18
- This behavior is deprecated and will be changed with Rails 5.1 to only check tables.
19
- Use #data_source_exists? instead.
20
- MSG
21
- data_source_exists?(table_name)
22
- end
23
-
24
- def data_source_exists?(table_name)
25
- return false if table_name.blank?
26
- unquoted_table_name = SQLServer::Utils.extract_identifiers(table_name).object
27
- super(unquoted_table_name)
28
- end
29
-
30
- def views
31
- tables_sql('VIEW')
32
- end
33
-
34
- def view_exists?(table_name)
35
- identifier = SQLServer::Utils.extract_identifiers(table_name)
36
- super(identifier.object)
37
- end
38
-
39
10
  def create_table(table_name, comment: nil, **options)
40
11
  res = super
41
12
  clear_cache!
@@ -43,29 +14,50 @@ module ActiveRecord
43
14
  end
44
15
 
45
16
  def drop_table(table_name, options = {})
46
- if options[:if_exists] && @version_year != 2016
17
+ # Mimic CASCADE option as best we can.
18
+ if options[:force] == :cascade
19
+ execute_procedure(:sp_fkeys, pktable_name: table_name).each do |fkdata|
20
+ fktable = fkdata['FKTABLE_NAME']
21
+ fkcolmn = fkdata['FKCOLUMN_NAME']
22
+ pktable = fkdata['PKTABLE_NAME']
23
+ pkcolmn = fkdata['PKCOLUMN_NAME']
24
+ remove_foreign_key fktable, name: fkdata['FK_NAME']
25
+ do_execute "DELETE FROM #{quote_table_name(fktable)} WHERE #{quote_column_name(fkcolmn)} IN ( SELECT #{quote_column_name(pkcolmn)} FROM #{quote_table_name(pktable)} )"
26
+ end
27
+ end
28
+ if options[:if_exists] && @version_year < 2016
47
29
  execute "IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = #{quote(table_name)}) DROP TABLE #{quote_table_name(table_name)}"
48
30
  else
49
31
  super
50
32
  end
51
33
  end
52
34
 
53
- def indexes(table_name, name = nil)
54
- data = (select("EXEC sp_helpindex #{quote(table_name)}", name) || []) rescue [] # JDBC returns nil instead of an array or erring out for no results
35
+ def indexes(table_name)
36
+ data = (select("EXEC sp_helpindex #{quote(table_name)}", "SCHEMA") || []) rescue [] # JDBC returns nil instead of an array or erring out for no results
55
37
  data.reduce([]) do |indexes, index|
56
38
  index = index.with_indifferent_access
39
+
57
40
  if index[:index_description] =~ /primary key/
58
41
  indexes
59
42
  else
60
43
  name = index[:index_name]
61
44
  unique = index[:index_description] =~ /unique/
62
45
  where = select_value("SELECT [filter_definition] FROM sys.indexes WHERE name = #{quote(name)}")
63
- columns = index[:index_keys].split(',').map do |column|
46
+ orders = {}
47
+ columns = []
48
+
49
+ index[:index_keys].split(',').each do |column|
64
50
  column.strip!
65
- column.gsub! '(-)', '' if column.ends_with?('(-)')
66
- column
51
+
52
+ if column.ends_with?('(-)')
53
+ column.gsub! '(-)', ''
54
+ orders[column] = :desc
55
+ end
56
+
57
+ columns << column
67
58
  end
68
- indexes << IndexDefinition.new(table_name, name, unique, columns, nil, nil, where)
59
+
60
+ indexes << IndexDefinition.new(table_name, name, unique, columns, where: where, orders: orders)
69
61
  end
70
62
  end
71
63
  end
@@ -103,10 +95,36 @@ module ActiveRecord
103
95
  end
104
96
 
105
97
  def primary_keys(table_name)
106
- primaries = schema_cache.columns(table_name).select(&:is_primary?).map(&:name)
98
+ primaries = primary_keys_select(table_name)
107
99
  primaries.present? ? primaries : identity_columns(table_name).map(&:name)
108
100
  end
109
101
 
102
+ def primary_keys_select(table_name)
103
+ identifier = database_prefix_identifier(table_name)
104
+ database = identifier.fully_qualified_database_quoted
105
+ sql = %{
106
+ SELECT KCU.COLUMN_NAME AS [name]
107
+ FROM #{database}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
108
+ LEFT OUTER JOIN #{database}.INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC
109
+ ON KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
110
+ AND KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
111
+ AND KCU.CONSTRAINT_CATALOG = TC.CONSTRAINT_CATALOG
112
+ AND KCU.CONSTRAINT_SCHEMA = TC.CONSTRAINT_SCHEMA
113
+ AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY'
114
+ WHERE KCU.TABLE_NAME = #{prepared_statements ? COLUMN_DEFINITION_BIND_STRING_0 : quote(identifier.object)}
115
+ AND KCU.TABLE_SCHEMA = #{identifier.schema.blank? ? 'schema_name()' : (prepared_statements ? COLUMN_DEFINITION_BIND_STRING_1 : quote(identifier.schema))}
116
+ AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY'
117
+ ORDER BY KCU.ORDINAL_POSITION ASC
118
+ }.gsub(/[[:space:]]/, ' ')
119
+ binds = []
120
+ if prepared_statements
121
+ nv128 = SQLServer::Type::UnicodeVarchar.new limit: 128
122
+ binds << Relation::QueryAttribute.new('TABLE_NAME', identifier.object, nv128)
123
+ binds << Relation::QueryAttribute.new('TABLE_SCHEMA', identifier.schema, nv128) unless identifier.schema.blank?
124
+ end
125
+ sp_executesql(sql, 'SCHEMA', binds).map { |r| r['name'] }
126
+ end
127
+
110
128
  def rename_table(table_name, new_name)
111
129
  do_execute "EXEC sp_rename '#{table_name}', '#{new_name}'"
112
130
  rename_table_indexes(table_name, new_name)
@@ -124,22 +142,30 @@ module ActiveRecord
124
142
  sql_commands = []
125
143
  indexes = []
126
144
  column_object = schema_cache.columns(table_name).find { |c| c.name.to_s == column_name.to_s }
127
- if options_include_default?(options) || (column_object && column_object.type != type.to_sym)
145
+ without_constraints = options.key?(:default) || options.key?(:limit)
146
+ default = if !options.key?(:default) && column_object
147
+ column_object.default
148
+ else
149
+ options[:default]
150
+ end
151
+ if without_constraints || (column_object && column_object.type != type.to_sym)
128
152
  remove_default_constraint(table_name, column_name)
129
153
  indexes = indexes(table_name).select { |index| index.columns.include?(column_name.to_s) }
130
154
  remove_indexes(table_name, column_name)
131
155
  end
132
156
  sql_commands << "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(options[:default], column_object)} WHERE #{quote_column_name(column_name)} IS NULL" if !options[:null].nil? && options[:null] == false && !options[:default].nil?
133
- sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
134
- sql_commands[-1] << ' NOT NULL' if !options[:null].nil? && options[:null] == false
135
- if options_include_default?(options)
136
- sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote_default_expression(options[:default], column_object)} FOR #{quote_column_name(column_name)}"
157
+ sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, limit: options[:limit], precision: options[:precision], scale: options[:scale])}"
158
+ sql_commands.last << ' NOT NULL' if !options[:null].nil? && options[:null] == false
159
+ if without_constraints
160
+ default = quote_default_expression(default, column_object || column_for(table_name, column_name))
161
+ sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{default} FOR #{quote_column_name(column_name)}"
137
162
  end
138
163
  # Add any removed indexes back
139
164
  indexes.each do |index|
140
165
  sql_commands << "CREATE INDEX #{quote_table_name(index.name)} ON #{quote_table_name(table_name)} (#{index.columns.map { |c| quote_column_name(c) }.join(', ')})"
141
166
  end
142
167
  sql_commands.each { |c| do_execute(c) }
168
+ clear_cache!
143
169
  end
144
170
 
145
171
  def change_column_default(table_name, column_name, default_or_changes)
@@ -194,13 +220,14 @@ module ActiveRecord
194
220
  end
195
221
  end
196
222
 
197
- def type_to_sql(type, limit = nil, precision = nil, scale = nil)
223
+ def type_to_sql(type, limit: nil, precision: nil, scale: nil, **)
198
224
  type_limitable = %w(string integer float char nchar varchar nvarchar).include?(type.to_s)
199
225
  limit = nil unless type_limitable
200
226
  case type.to_s
201
227
  when 'integer'
202
228
  case limit
203
- when 1..2 then 'smallint'
229
+ when 1 then 'tinyint'
230
+ when 2 then 'smallint'
204
231
  when 3..4, nil then 'integer'
205
232
  when 5..8 then 'bigint'
206
233
  else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
@@ -226,7 +253,8 @@ module ActiveRecord
226
253
  s.gsub(/\s+(?:ASC|DESC)\b/i, '')
227
254
  .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, '')
228
255
  }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
229
- [super, *order_columns].join(', ')
256
+
257
+ (order_columns << super).join(", ")
230
258
  end
231
259
 
232
260
  def update_table_definition(table_name, base)
@@ -236,23 +264,48 @@ module ActiveRecord
236
264
  def change_column_null(table_name, column_name, allow_null, default = nil)
237
265
  table_id = SQLServer::Utils.extract_identifiers(table_name)
238
266
  column_id = SQLServer::Utils.extract_identifiers(column_name)
239
- column = detect_column_for! table_name, column_name
267
+ column = column_for(table_name, column_name)
240
268
  if !allow_null.nil? && allow_null == false && !default.nil?
241
269
  do_execute("UPDATE #{table_id} SET #{column_id}=#{quote(default)} WHERE #{column_id} IS NULL")
242
270
  end
243
- sql = "ALTER TABLE #{table_id} ALTER COLUMN #{column_id} #{type_to_sql column.type, column.limit, column.precision, column.scale}"
271
+ sql = "ALTER TABLE #{table_id} ALTER COLUMN #{column_id} #{type_to_sql column.type, limit: column.limit, precision: column.precision, scale: column.scale}"
244
272
  sql << ' NOT NULL' if !allow_null.nil? && allow_null == false
245
273
  do_execute sql
246
274
  end
247
275
 
276
+ def create_schema_dumper(options)
277
+ SQLServer::SchemaDumper.create(self, options)
278
+ end
279
+
280
+ private
248
281
 
249
- protected
282
+ def data_source_sql(name = nil, type: nil)
283
+ scope = quoted_scope name, type: type
284
+ table_name = lowercase_schema_reflection_sql 'TABLE_NAME'
285
+ sql = "SELECT #{table_name}"
286
+ sql << ' FROM INFORMATION_SCHEMA.TABLES WITH (NOLOCK)'
287
+ sql << ' WHERE TABLE_CATALOG = DB_NAME()'
288
+ sql << " AND TABLE_SCHEMA = #{quote(scope[:schema])}"
289
+ sql << " AND TABLE_NAME = #{quote(scope[:name])}" if scope[:name]
290
+ sql << " AND TABLE_TYPE = #{quote(scope[:type])}" if scope[:type]
291
+ sql << " ORDER BY #{table_name}"
292
+ sql
293
+ end
294
+
295
+ def quoted_scope(name = nil, type: nil)
296
+ identifier = SQLServer::Utils.extract_identifiers(name)
297
+ {}.tap do |scope|
298
+ scope[:schema] = identifier.schema || 'dbo'
299
+ scope[:name] = identifier.object if identifier.object
300
+ scope[:type] = type if type
301
+ end
302
+ end
250
303
 
251
304
  # === SQLServer Specific ======================================== #
252
305
 
253
306
  def initialize_native_database_types
254
307
  {
255
- primary_key: 'int NOT NULL IDENTITY(1,1) PRIMARY KEY',
308
+ primary_key: 'bigint NOT NULL IDENTITY(1,1) PRIMARY KEY',
256
309
  primary_key_nonclustered: 'int NOT NULL IDENTITY(1,1) PRIMARY KEY NONCLUSTERED',
257
310
  integer: { name: 'int', limit: 4 },
258
311
  bigint: { name: 'bigint' },
@@ -286,21 +339,11 @@ module ActiveRecord
286
339
  }
287
340
  end
288
341
 
289
- def tables_sql(type)
290
- tn = lowercase_schema_reflection_sql 'TABLE_NAME'
291
- sql = "SELECT #{tn} FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = '#{type}' ORDER BY TABLE_NAME"
292
- select_values sql, 'SCHEMA'
293
- end
294
-
295
342
  COLUMN_DEFINITION_BIND_STRING_0 = defined?(JRUBY_VERSION) ? '?' : '@0'
296
343
  COLUMN_DEFINITION_BIND_STRING_1 = defined?(JRUBY_VERSION) ? '?' : '@1'
297
344
 
298
345
  def column_definitions(table_name)
299
- identifier = if database_prefix_remote_server?
300
- SQLServer::Utils.extract_identifiers("#{database_prefix}#{table_name}")
301
- else
302
- SQLServer::Utils.extract_identifiers(table_name)
303
- end
346
+ identifier = database_prefix_identifier(table_name)
304
347
  database = identifier.fully_qualified_database_quoted
305
348
  view_exists = view_exists?(table_name)
306
349
  view_tblnm = view_table_name(table_name) if view_exists
@@ -386,11 +429,13 @@ module ActiveRecord
386
429
  ci[:default_function] = begin
387
430
  default = ci[:default_value]
388
431
  if default.nil? && view_exists
389
- default = select_value "
432
+ default = select_value %{
390
433
  SELECT c.COLUMN_DEFAULT
391
434
  FROM #{database}.INFORMATION_SCHEMA.COLUMNS c
392
- WHERE c.TABLE_NAME = '#{view_tblnm}'
393
- AND c.COLUMN_NAME = '#{views_real_column_name(table_name, ci[:name])}'".squish, 'SCHEMA'
435
+ WHERE
436
+ c.TABLE_NAME = '#{view_tblnm}'
437
+ AND c.COLUMN_NAME = '#{views_real_column_name(table_name, ci[:name])}'
438
+ }.squish, 'SCHEMA'
394
439
  end
395
440
  case default
396
441
  when nil
@@ -409,7 +454,7 @@ module ActiveRecord
409
454
  else ci[:type]
410
455
  end
411
456
  value = default.match(/\A\((.*)\)\Z/m)[1]
412
- value = select_value "SELECT CAST(#{value} AS #{type}) AS value", 'SCHEMA'
457
+ value = select_value("SELECT CAST(#{value} AS #{type}) AS value", 'SCHEMA')
413
458
  [value, nil]
414
459
  end
415
460
  end
@@ -460,13 +505,6 @@ module ActiveRecord
460
505
  "DF_#{table_name}_#{column_name}"
461
506
  end
462
507
 
463
- def detect_column_for!(table_name, column_name)
464
- unless column = schema_cache.columns(table_name).find { |c| c.name == column_name.to_s }
465
- raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
466
- end
467
- column
468
- end
469
-
470
508
  def lowercase_schema_reflection_sql(node)
471
509
  lowercase_schema_reflection ? "LOWER(#{node})" : node
472
510
  end
@@ -482,7 +520,7 @@ module ActiveRecord
482
520
  @view_information ||= {}
483
521
  @view_information[table_name] ||= begin
484
522
  identifier = SQLServer::Utils.extract_identifiers(table_name)
485
- view_info = select_one "SELECT * FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = '#{identifier.object}'", 'SCHEMA'
523
+ view_info = select_one "SELECT * FROM INFORMATION_SCHEMA.VIEWS WITH (NOLOCK) WHERE TABLE_NAME = #{quote(identifier.object)}", 'SCHEMA'
486
524
  if view_info
487
525
  view_info = view_info.with_indifferent_access
488
526
  if view_info[:VIEW_DEFINITION].blank? || view_info[:VIEW_DEFINITION].length == 4000
@@ -505,8 +543,6 @@ module ActiveRecord
505
543
  match_data ? match_data[1] : column_name
506
544
  end
507
545
 
508
- private
509
-
510
546
  def create_table_definition(*args)
511
547
  SQLServer::TableDefinition.new(*args)
512
548
  end