activerecord-sqlserver-adapter 7.0.7 → 7.1.0.beta1

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +3 -2
  3. data/CHANGELOG.md +2 -94
  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 +42 -0
  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 +15 -9
@@ -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