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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +3 -2
- data/CHANGELOG.md +2 -94
- data/Gemfile +3 -0
- data/README.md +16 -11
- data/Rakefile +2 -6
- data/VERSION +1 -1
- data/activerecord-sqlserver-adapter.gemspec +1 -1
- data/lib/active_record/connection_adapters/sqlserver/core_ext/abstract_adapter.rb +20 -0
- data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +42 -0
- data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +4 -4
- data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +10 -2
- data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +15 -3
- data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +0 -31
- data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +87 -131
- data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +5 -5
- data/lib/active_record/connection_adapters/sqlserver/quoting.rb +3 -2
- data/lib/active_record/connection_adapters/sqlserver/savepoints.rb +24 -0
- data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +71 -58
- data/lib/active_record/connection_adapters/sqlserver/showplan.rb +3 -3
- data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +6 -0
- data/lib/active_record/connection_adapters/sqlserver/transaction.rb +4 -6
- data/lib/active_record/connection_adapters/sqlserver/type/data.rb +10 -0
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +81 -118
- data/lib/active_record/connection_adapters/sqlserver_column.rb +1 -0
- data/lib/active_record/sqlserver_base.rb +1 -10
- data/lib/active_record/tasks/sqlserver_database_tasks.rb +5 -2
- data/lib/arel/visitors/sqlserver.rb +0 -33
- data/test/cases/adapter_test_sqlserver.rb +8 -7
- data/test/cases/coerced_tests.rb +558 -248
- data/test/cases/column_test_sqlserver.rb +6 -6
- data/test/cases/connection_test_sqlserver.rb +3 -6
- data/test/cases/disconnected_test_sqlserver.rb +5 -8
- data/test/cases/execute_procedure_test_sqlserver.rb +1 -1
- data/test/cases/rake_test_sqlserver.rb +1 -1
- data/test/cases/schema_dumper_test_sqlserver.rb +2 -2
- data/test/cases/view_test_sqlserver.rb +6 -10
- data/test/config.yml +1 -2
- data/test/support/connection_reflection.rb +2 -8
- data/test/support/core_ext/query_cache.rb +7 -1
- data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic_associations.dump +0 -0
- data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic.dump +0 -0
- data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic_associations.dump +0 -0
- 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
|
-
|
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] &&
|
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]
|
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]
|
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
|
-
|
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
|
-
|
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
|
-
|
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|
|
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
|
-
|
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
|
-
|
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
|
-
|
223
|
-
|
224
|
-
|
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
|
-
|
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,
|
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 !
|
313
|
-
|
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 !
|
317
|
-
|
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
|
-
|
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
|
-
|
418
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 {
|
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
|
-
|
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
|
@@ -5,14 +5,12 @@ require "active_record/connection_adapters/abstract/transaction"
|
|
5
5
|
module ActiveRecord
|
6
6
|
module ConnectionAdapters
|
7
7
|
module SQLServerTransaction
|
8
|
-
|
8
|
+
delegate :sqlserver?, to: :connection, prefix: true
|
9
9
|
|
10
|
-
|
11
|
-
connection.respond_to?(:sqlserver?) && connection.sqlserver?
|
12
|
-
end
|
10
|
+
private
|
13
11
|
|
14
12
|
def current_isolation_level
|
15
|
-
return unless
|
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
|
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
|