activerecord-sqlserver-adapter 7.0.6 → 7.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +3 -3
  3. data/CHANGELOG.md +2 -88
  4. data/Gemfile +3 -0
  5. data/README.md +16 -11
  6. data/Rakefile +2 -6
  7. data/VERSION +1 -1
  8. data/activerecord-sqlserver-adapter.gemspec +1 -1
  9. data/lib/active_record/connection_adapters/sqlserver/core_ext/abstract_adapter.rb +20 -0
  10. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +29 -6
  11. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +4 -4
  12. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +10 -2
  13. data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +15 -3
  14. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +0 -31
  15. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +87 -131
  16. data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +5 -5
  17. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +3 -2
  18. data/lib/active_record/connection_adapters/sqlserver/savepoints.rb +24 -0
  19. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +71 -58
  20. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +3 -3
  21. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +6 -0
  22. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +4 -6
  23. data/lib/active_record/connection_adapters/sqlserver/type/data.rb +10 -0
  24. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +81 -118
  25. data/lib/active_record/connection_adapters/sqlserver_column.rb +1 -0
  26. data/lib/active_record/sqlserver_base.rb +1 -10
  27. data/lib/active_record/tasks/sqlserver_database_tasks.rb +5 -2
  28. data/lib/arel/visitors/sqlserver.rb +0 -33
  29. data/test/cases/adapter_test_sqlserver.rb +8 -7
  30. data/test/cases/coerced_tests.rb +558 -248
  31. data/test/cases/column_test_sqlserver.rb +6 -6
  32. data/test/cases/connection_test_sqlserver.rb +3 -6
  33. data/test/cases/disconnected_test_sqlserver.rb +5 -8
  34. data/test/cases/execute_procedure_test_sqlserver.rb +1 -1
  35. data/test/cases/rake_test_sqlserver.rb +1 -1
  36. data/test/cases/schema_dumper_test_sqlserver.rb +2 -2
  37. data/test/cases/view_test_sqlserver.rb +6 -10
  38. data/test/config.yml +1 -2
  39. data/test/support/connection_reflection.rb +2 -8
  40. data/test/support/core_ext/query_cache.rb +7 -1
  41. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic_associations.dump +0 -0
  42. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic.dump +0 -0
  43. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic_associations.dump +0 -0
  44. metadata +14 -8
@@ -23,10 +23,10 @@ module ActiveRecord
23
23
  pktable = fkdata["PKTABLE_NAME"]
24
24
  pkcolmn = fkdata["PKCOLUMN_NAME"]
25
25
  remove_foreign_key fktable, name: fkdata["FK_NAME"]
26
- 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
+ execute "DELETE FROM #{quote_table_name(fktable)} WHERE #{quote_column_name(fkcolmn)} IN ( SELECT #{quote_column_name(pkcolmn)} FROM #{quote_table_name(pktable)} )"
27
27
  end
28
28
  end
29
- if options[:if_exists] && @version_year < 2016
29
+ if options[:if_exists] && version_year < 2016
30
30
  execute "IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = #{quote(table_name)}) DROP TABLE #{quote_table_name(table_name)}", "SCHEMA"
31
31
  else
32
32
  super
@@ -39,12 +39,12 @@ module ActiveRecord
39
39
  data.reduce([]) do |indexes, index|
40
40
  index = index.with_indifferent_access
41
41
 
42
- if index[:index_description] =~ /primary key/
42
+ if index[:index_description].match?(/primary key/)
43
43
  indexes
44
44
  else
45
45
  name = index[:index_name]
46
- unique = index[:index_description] =~ /unique/
47
- where = select_value("SELECT [filter_definition] FROM sys.indexes WHERE name = #{quote(name)}")
46
+ unique = index[:index_description].match?(/unique/)
47
+ where = select_value("SELECT [filter_definition] FROM sys.indexes WHERE name = #{quote(name)}", "SCHEMA")
48
48
  orders = {}
49
49
  columns = []
50
50
 
@@ -118,15 +118,20 @@ module ActiveRecord
118
118
  AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY'
119
119
  ORDER BY KCU.ORDINAL_POSITION ASC
120
120
  }.gsub(/[[:space:]]/, " ")
121
+
121
122
  binds = []
122
123
  nv128 = SQLServer::Type::UnicodeVarchar.new limit: 128
123
124
  binds << Relation::QueryAttribute.new("TABLE_NAME", identifier.object, nv128)
124
125
  binds << Relation::QueryAttribute.new("TABLE_SCHEMA", identifier.schema, nv128) unless identifier.schema.blank?
125
- sp_executesql(sql, "SCHEMA", binds).map { |r| r["name"] }
126
+
127
+ internal_exec_query(sql, "SCHEMA", binds).map { |row| row["name"] }
126
128
  end
127
129
 
128
- def rename_table(table_name, new_name)
129
- do_execute "EXEC sp_rename '#{table_name}', '#{new_name}'"
130
+ def rename_table(table_name, new_name, **options)
131
+ validate_table_length!(new_name) unless options[:_uses_legacy_table_name]
132
+ schema_cache.clear_data_source_cache!(table_name.to_s)
133
+ schema_cache.clear_data_source_cache!(new_name.to_s)
134
+ execute "EXEC sp_rename '#{table_name}', '#{new_name}'"
130
135
  rename_table_indexes(table_name, new_name)
131
136
  end
132
137
 
@@ -137,7 +142,7 @@ module ActiveRecord
137
142
  remove_check_constraints(table_name, column_name)
138
143
  remove_default_constraint(table_name, column_name)
139
144
  remove_indexes(table_name, column_name)
140
- do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
145
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
141
146
  end
142
147
 
143
148
  def change_column(table_name, column_name, type, options = {})
@@ -182,7 +187,7 @@ module ActiveRecord
182
187
  sql_commands << "CREATE INDEX #{quote_table_name(index.name)} ON #{quote_table_name(table_name)} (#{index.columns.map { |c| quote_column_name(c) }.join(', ')})"
183
188
  end
184
189
 
185
- sql_commands.each { |c| do_execute(c) }
190
+ sql_commands.each { |c| execute(c) }
186
191
  clear_cache!
187
192
  end
188
193
 
@@ -193,7 +198,7 @@ module ActiveRecord
193
198
 
194
199
  remove_default_constraint(table_name, column_name)
195
200
  default = extract_new_default_value(default_or_changes)
196
- do_execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote_default_expression(default, column)} FOR #{quote_column_name(column_name)}"
201
+ execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote_default_expression(default, column)} FOR #{quote_column_name(column_name)}"
197
202
  clear_cache!
198
203
  end
199
204
 
@@ -213,23 +218,45 @@ module ActiveRecord
213
218
  end
214
219
 
215
220
  def remove_index!(table_name, index_name)
216
- do_execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
221
+ execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
222
+ end
223
+
224
+ def build_change_column_definition(table_name, column_name, type, **options) # :nodoc:
225
+ td = create_table_definition(table_name)
226
+ cd = td.new_column_definition(column_name, type, **options)
227
+ ChangeColumnDefinition.new(cd, column_name)
228
+ end
229
+
230
+ def build_change_column_default_definition(table_name, column_name, default_or_changes) # :nodoc:
231
+ column = column_for(table_name, column_name)
232
+ return unless column
233
+
234
+ default = extract_new_default_value(default_or_changes)
235
+ ChangeColumnDefaultDefinition.new(column, default)
217
236
  end
218
237
 
219
238
  def foreign_keys(table_name)
220
239
  identifier = SQLServer::Utils.extract_identifiers(table_name)
221
240
  fk_info = execute_procedure :sp_fkeys, nil, identifier.schema, nil, identifier.object, identifier.schema
222
- fk_info.map do |row|
223
- from_table = identifier.object
224
- to_table = row["PKTABLE_NAME"]
241
+
242
+ grouped_fk = fk_info.group_by { |row| row["FK_NAME"] }.values.each { |group| group.sort_by! { |row| row["KEY_SEQ"] } }
243
+ grouped_fk.map do |group|
244
+ row = group.first
225
245
  options = {
226
246
  name: row["FK_NAME"],
227
- column: row["FKCOLUMN_NAME"],
228
- primary_key: row["PKCOLUMN_NAME"],
229
247
  on_update: extract_foreign_key_action("update", row["FK_NAME"]),
230
248
  on_delete: extract_foreign_key_action("delete", row["FK_NAME"])
231
249
  }
232
- ForeignKeyDefinition.new from_table, to_table, options
250
+
251
+ if group.one?
252
+ options[:column] = row["FKCOLUMN_NAME"]
253
+ options[:primary_key] = row["PKCOLUMN_NAME"]
254
+ else
255
+ options[:column] = group.map { |row| row["FKCOLUMN_NAME"] }
256
+ options[:primary_key] = group.map { |row| row["PKCOLUMN_NAME"] }
257
+ end
258
+
259
+ ForeignKeyDefinition.new(identifier.object, row["PKTABLE_NAME"], options)
233
260
  end
234
261
  end
235
262
 
@@ -240,29 +267,6 @@ module ActiveRecord
240
267
  end
241
268
  end
242
269
 
243
- def check_constraints(table_name)
244
- sql = <<~SQL
245
- select chk.name AS 'name',
246
- chk.definition AS 'expression'
247
- from sys.check_constraints chk
248
- inner join sys.tables st on chk.parent_object_id = st.object_id
249
- where
250
- st.name = '#{table_name}'
251
- SQL
252
-
253
- chk_info = exec_query(sql, "SCHEMA")
254
-
255
- chk_info.map do |row|
256
- options = {
257
- name: row["name"]
258
- }
259
- expression = row["expression"]
260
- expression = expression[1..-2] if expression.start_with?("(") && expression.end_with?(")")
261
-
262
- CheckConstraintDefinition.new(table_name, expression, options)
263
- end
264
- end
265
-
266
270
  def type_to_sql(type, limit: nil, precision: nil, scale: nil, **)
267
271
  type_limitable = %w(string integer float char nchar varchar nvarchar).include?(type.to_s)
268
272
  limit = nil unless type_limitable
@@ -291,6 +295,13 @@ module ActiveRecord
291
295
  end
292
296
  end
293
297
 
298
+ # In SQL Server only the first column added should have the `ADD` keyword.
299
+ def add_timestamps(table_name, **options)
300
+ fragments = add_timestamps_for_alter(table_name, **options)
301
+ fragments[1..].each { |fragment| fragment.sub!('ADD ', '') }
302
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{fragments.join(', ')}"
303
+ end
304
+
294
305
  def columns_for_distinct(columns, orders)
295
306
  order_columns = orders.reject(&:blank?).map { |s|
296
307
  s = s.to_sql unless s.is_a?(String)
@@ -305,16 +316,19 @@ module ActiveRecord
305
316
  SQLServer::Table.new(table_name, base)
306
317
  end
307
318
 
308
- def change_column_null(table_name, column_name, allow_null, default = nil)
319
+ def change_column_null(table_name, column_name, null, default = nil)
320
+ validate_change_column_null_argument!(null)
321
+
309
322
  table_id = SQLServer::Utils.extract_identifiers(table_name)
310
323
  column_id = SQLServer::Utils.extract_identifiers(column_name)
311
324
  column = column_for(table_name, column_name)
312
- if !allow_null.nil? && allow_null == false && !default.nil?
313
- do_execute("UPDATE #{table_id} SET #{column_id}=#{quote(default)} WHERE #{column_id} IS NULL")
325
+ if !null.nil? && null == false && !default.nil?
326
+ execute("UPDATE #{table_id} SET #{column_id}=#{quote(default)} WHERE #{column_id} IS NULL")
314
327
  end
315
328
  sql = "ALTER TABLE #{table_id} ALTER COLUMN #{column_id} #{type_to_sql column.type, limit: column.limit, precision: column.precision, scale: column.scale}"
316
- sql += " NOT NULL" if !allow_null.nil? && allow_null == false
317
- do_execute sql
329
+ sql += " NOT NULL" if !null.nil? && null == false
330
+
331
+ execute sql
318
332
  end
319
333
 
320
334
  def create_schema_dumper(options)
@@ -410,12 +424,13 @@ module ActiveRecord
410
424
  view_tblnm = view_table_name(table_name) if view_exists
411
425
 
412
426
  if view_exists
413
- results = sp_executesql %{
427
+ sql = <<~SQL
414
428
  SELECT LOWER(c.COLUMN_NAME) AS [name], c.COLUMN_DEFAULT AS [default]
415
429
  FROM #{database}.INFORMATION_SCHEMA.COLUMNS c
416
430
  WHERE c.TABLE_NAME = #{quote(view_tblnm)}
417
- }.squish, "SCHEMA", []
418
- default_functions = results.each.with_object({}) {|row, out| out[row["name"]] = row["default"] }.compact
431
+ SQL
432
+ results = internal_exec_query(sql, "SCHEMA")
433
+ default_functions = results.each.with_object({}) { |row, out| out[row["name"]] = row["default"] }.compact
419
434
  end
420
435
 
421
436
  sql = column_definitions_sql(database, identifier)
@@ -424,7 +439,8 @@ module ActiveRecord
424
439
  nv128 = SQLServer::Type::UnicodeVarchar.new limit: 128
425
440
  binds << Relation::QueryAttribute.new("TABLE_NAME", identifier.object, nv128)
426
441
  binds << Relation::QueryAttribute.new("TABLE_SCHEMA", identifier.schema, nv128) unless identifier.schema.blank?
427
- results = sp_executesql(sql, "SCHEMA", binds)
442
+ results = internal_exec_query(sql, "SCHEMA", binds)
443
+
428
444
  columns = results.map do |ci|
429
445
  ci = ci.symbolize_keys
430
446
  ci[:_type] = ci[:type]
@@ -566,7 +582,7 @@ module ActiveRecord
566
582
  def remove_check_constraints(table_name, column_name)
567
583
  constraints = select_values "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE where TABLE_NAME = '#{quote_string(table_name)}' and COLUMN_NAME = '#{quote_string(column_name)}'", "SCHEMA"
568
584
  constraints.each do |constraint|
569
- do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
585
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
570
586
  end
571
587
  end
572
588
 
@@ -575,7 +591,7 @@ module ActiveRecord
575
591
  execute_procedure(:sp_helpconstraint, table_name, "nomsg").flatten.select do |row|
576
592
  row["constraint_type"] == "DEFAULT on column #{column_name}"
577
593
  end.each do |row|
578
- do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{row['constraint_name']}"
594
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{row['constraint_name']}"
579
595
  end
580
596
  end
581
597
 
@@ -624,19 +640,17 @@ module ActiveRecord
624
640
  identifier = SQLServer::Utils.extract_identifiers(table_name)
625
641
  information_query_table = identifier.database.present? ? "[#{identifier.database}].[INFORMATION_SCHEMA].[VIEWS]" : "[INFORMATION_SCHEMA].[VIEWS]"
626
642
  view_info = select_one "SELECT * FROM #{information_query_table} WITH (NOLOCK) WHERE TABLE_NAME = #{quote(identifier.object)}", "SCHEMA"
627
-
628
643
  if view_info
629
644
  view_info = view_info.with_indifferent_access
630
645
  if view_info[:VIEW_DEFINITION].blank? || view_info[:VIEW_DEFINITION].length == 4000
631
646
  view_info[:VIEW_DEFINITION] = begin
632
- select_values("EXEC sp_helptext #{identifier.object_quoted}", "SCHEMA").join
647
+ select_values("EXEC sp_helptext #{identifier.object_quoted}", "SCHEMA").join
633
648
  rescue
634
649
  warn "No view definition found, possible permissions problem.\nPlease run GRANT VIEW DEFINITION TO your_user;"
635
650
  nil
636
- end
651
+ end
637
652
  end
638
653
  end
639
-
640
654
  view_info
641
655
  end
642
656
  end
@@ -645,8 +659,7 @@ module ActiveRecord
645
659
  view_definition = view_information(table_name)[:VIEW_DEFINITION]
646
660
  return column_name unless view_definition
647
661
 
648
- # Remove "CREATE VIEW ... AS SELECT ..." and then match the column name.
649
- match_data = view_definition.sub(/CREATE\s+VIEW.*AS\s+SELECT\s/, '').match(/([\w-]*)\s+AS\s+#{column_name}\W/im)
662
+ match_data = view_definition.match(/CREATE\s+VIEW.*AS\s+SELECT.*\W([\w-]*)\s+AS\s+#{column_name}/im)
650
663
  match_data ? match_data[1] : column_name
651
664
  end
652
665
 
@@ -12,9 +12,9 @@ module ActiveRecord
12
12
  OPTION_XML = "SHOWPLAN_XML"
13
13
  OPTIONS = [OPTION_ALL, OPTION_TEXT, OPTION_XML]
14
14
 
15
- def explain(arel, binds = [])
15
+ def explain(arel, binds = [], options = [])
16
16
  sql = to_sql(arel)
17
- result = with_showplan_on { sp_executesql(sql, "EXPLAIN", binds) }
17
+ result = with_showplan_on { internal_exec_query(sql, "EXPLAIN", binds) }
18
18
  printer = showplan_printer.new(result)
19
19
  printer.pp
20
20
  end
@@ -30,7 +30,7 @@ module ActiveRecord
30
30
 
31
31
  def set_showplan_option(enable = true)
32
32
  sql = "SET #{showplan_option} #{enable ? 'ON' : 'OFF'}"
33
- raw_connection_do(sql)
33
+ raw_execute(sql, "SCHEMA")
34
34
  rescue Exception
35
35
  raise ActiveRecordError, "#{showplan_option} could not be turned #{enable ? 'ON' : 'OFF'}, perhaps you do not have SHOWPLAN permissions?"
36
36
  end
@@ -113,6 +113,12 @@ module ActiveRecord
113
113
 
114
114
  super
115
115
  end
116
+
117
+ private
118
+
119
+ def valid_column_definition_options
120
+ super + [:is_identity]
121
+ end
116
122
  end
117
123
 
118
124
  class Table < ActiveRecord::ConnectionAdapters::Table
@@ -5,14 +5,12 @@ require "active_record/connection_adapters/abstract/transaction"
5
5
  module ActiveRecord
6
6
  module ConnectionAdapters
7
7
  module SQLServerTransaction
8
- private
8
+ delegate :sqlserver?, to: :connection, prefix: true
9
9
 
10
- def sqlserver?
11
- connection.respond_to?(:sqlserver?) && connection.sqlserver?
12
- end
10
+ private
13
11
 
14
12
  def current_isolation_level
15
- return unless sqlserver?
13
+ return unless connection_sqlserver?
16
14
 
17
15
  level = connection.user_options_isolation_level
18
16
  # When READ_COMMITTED_SNAPSHOT is set to ON,
@@ -50,7 +48,7 @@ module ActiveRecord
50
48
  private
51
49
 
52
50
  def reset_starting_isolation_level
53
- if sqlserver? && starting_isolation_level
51
+ if connection_sqlserver? && starting_isolation_level
54
52
  connection.set_transaction_isolation_level(starting_isolation_level)
55
53
  end
56
54
  end
@@ -36,6 +36,16 @@ module ActiveRecord
36
36
  self.class == other.class && value == other.value
37
37
  end
38
38
  alias :== :eql?
39
+
40
+ def self.from_msgpack_ext(string)
41
+ type, value = string.chomp!("msgpack_ext").split(',')
42
+
43
+ Data.new(value, type.constantize)
44
+ end
45
+
46
+ def to_msgpack_ext
47
+ [type.class.to_s, value].join(',') + "msgpack_ext"
48
+ end
39
49
  end
40
50
  end
41
51
  end