activerecord-sqlserver-adapter 6.1.2.1 → 7.2.4
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/.devcontainer/Dockerfile +30 -0
- data/.devcontainer/boot.sh +22 -0
- data/.devcontainer/devcontainer.json +38 -0
- data/.devcontainer/docker-compose.yml +42 -0
- data/.github/workflows/ci.yml +7 -4
- data/.gitignore +3 -1
- data/CHANGELOG.md +19 -42
- data/Dockerfile.ci +3 -3
- data/Gemfile +6 -1
- data/MIT-LICENSE +1 -1
- data/README.md +113 -27
- data/RUNNING_UNIT_TESTS.md +27 -14
- data/Rakefile +2 -6
- data/VERSION +1 -1
- data/activerecord-sqlserver-adapter.gemspec +3 -3
- data/appveyor.yml +4 -6
- data/docker-compose.ci.yml +2 -1
- data/lib/active_record/connection_adapters/sqlserver/core_ext/abstract_adapter.rb +20 -0
- data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +6 -4
- data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +5 -23
- data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +10 -7
- data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +2 -0
- data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +12 -2
- data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +24 -16
- data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +0 -31
- data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +143 -155
- data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +5 -5
- data/lib/active_record/connection_adapters/sqlserver/quoting.rb +57 -56
- data/lib/active_record/connection_adapters/sqlserver/savepoints.rb +26 -0
- data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +14 -12
- data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +11 -0
- data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +213 -57
- data/lib/active_record/connection_adapters/sqlserver/showplan.rb +3 -3
- data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +13 -2
- data/lib/active_record/connection_adapters/sqlserver/transaction.rb +4 -6
- data/lib/active_record/connection_adapters/sqlserver/type/data.rb +19 -1
- data/lib/active_record/connection_adapters/sqlserver/type/date.rb +1 -1
- data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +1 -1
- data/lib/active_record/connection_adapters/sqlserver/type/time.rb +1 -1
- data/lib/active_record/connection_adapters/sqlserver/utils.rb +21 -10
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +187 -187
- data/lib/active_record/connection_adapters/sqlserver_column.rb +1 -0
- data/lib/active_record/tasks/sqlserver_database_tasks.rb +42 -33
- data/lib/arel/visitors/sqlserver.rb +77 -34
- data/test/cases/active_schema_test_sqlserver.rb +127 -0
- data/test/cases/adapter_test_sqlserver.rb +114 -26
- data/test/cases/coerced_tests.rb +1121 -340
- data/test/cases/column_test_sqlserver.rb +67 -64
- data/test/cases/connection_test_sqlserver.rb +3 -6
- data/test/cases/dbconsole.rb +19 -0
- data/test/cases/disconnected_test_sqlserver.rb +8 -5
- data/test/cases/eager_load_too_many_ids_test_sqlserver.rb +18 -0
- data/test/cases/enum_test_sqlserver.rb +49 -0
- data/test/cases/execute_procedure_test_sqlserver.rb +9 -5
- data/test/cases/fetch_test_sqlserver.rb +19 -0
- data/test/cases/helper_sqlserver.rb +11 -5
- data/test/cases/index_test_sqlserver.rb +8 -6
- data/test/cases/json_test_sqlserver.rb +1 -1
- data/test/cases/lateral_test_sqlserver.rb +2 -2
- data/test/cases/migration_test_sqlserver.rb +19 -1
- data/test/cases/optimizer_hints_test_sqlserver.rb +21 -12
- data/test/cases/pessimistic_locking_test_sqlserver.rb +8 -7
- data/test/cases/primary_keys_test_sqlserver.rb +2 -2
- data/test/cases/rake_test_sqlserver.rb +10 -5
- data/test/cases/schema_dumper_test_sqlserver.rb +155 -109
- data/test/cases/schema_test_sqlserver.rb +64 -1
- data/test/cases/showplan_test_sqlserver.rb +7 -7
- data/test/cases/specific_schema_test_sqlserver.rb +17 -13
- data/test/cases/transaction_test_sqlserver.rb +13 -8
- data/test/cases/trigger_test_sqlserver.rb +20 -0
- data/test/cases/utils_test_sqlserver.rb +2 -2
- data/test/cases/uuid_test_sqlserver.rb +8 -0
- data/test/cases/view_test_sqlserver.rb +58 -0
- data/test/config.yml +1 -2
- data/test/migrations/transaction_table/1_table_will_never_be_created.rb +1 -1
- data/test/models/sqlserver/alien.rb +5 -0
- data/test/models/sqlserver/table_with_spaces.rb +5 -0
- data/test/models/sqlserver/trigger.rb +8 -0
- data/test/schema/sqlserver_specific_schema.rb +54 -6
- data/test/support/coerceable_test_sqlserver.rb +4 -4
- data/test/support/connection_reflection.rb +3 -9
- data/test/support/core_ext/query_cache.rb +7 -1
- data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic.dump +0 -0
- 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
- data/test/support/query_assertions.rb +49 -0
- data/test/support/rake_helpers.rb +3 -1
- data/test/support/table_definition_sqlserver.rb +24 -0
- data/test/support/test_in_memory_oltp.rb +2 -2
- metadata +41 -17
- data/lib/active_record/sqlserver_base.rb +0 -18
- data/test/cases/scratchpad_test_sqlserver.rb +0 -8
- data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic.dump +0 -0
- data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic_associations.dump +0 -0
- data/test/support/sql_counter_sqlserver.rb +0 -29
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
module SQLServer
|
6
|
+
module Savepoints
|
7
|
+
def current_savepoint_name
|
8
|
+
current_transaction.savepoint_name
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_savepoint(name = current_savepoint_name)
|
12
|
+
internal_execute("SAVE TRANSACTION #{name}", "TRANSACTION")
|
13
|
+
end
|
14
|
+
|
15
|
+
def exec_rollback_to_savepoint(name = current_savepoint_name)
|
16
|
+
internal_execute("ROLLBACK TRANSACTION #{name}", "TRANSACTION")
|
17
|
+
end
|
18
|
+
|
19
|
+
# SQL Server does require save-points to be explicitly released.
|
20
|
+
# See https://stackoverflow.com/questions/3101312/sql-server-2008-no-release-savepoint-for-current-transaction
|
21
|
+
def release_savepoint(_name)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -34,27 +34,29 @@ module ActiveRecord
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def visit_CreateIndexDefinition(o)
|
37
|
-
|
38
|
-
|
39
|
-
o.if_not_exists = false
|
40
|
-
|
41
|
-
sql = super
|
37
|
+
index = o.index
|
42
38
|
|
43
|
-
|
44
|
-
|
45
|
-
|
39
|
+
sql = []
|
40
|
+
sql << "IF NOT EXISTS (SELECT name FROM sysindexes WHERE name = '#{o.index.name}')" if o.if_not_exists
|
41
|
+
sql << "CREATE"
|
42
|
+
sql << "UNIQUE" if index.unique
|
43
|
+
sql << index.type.upcase if index.type
|
44
|
+
sql << "INDEX"
|
45
|
+
sql << "#{quote_column_name(index.name)} ON #{quote_table_name(index.table)}"
|
46
|
+
sql << "(#{quoted_columns(index)})"
|
47
|
+
sql << "WHERE #{index.where}" if index.where
|
46
48
|
|
47
|
-
sql
|
49
|
+
sql.join(" ")
|
48
50
|
end
|
49
51
|
|
50
52
|
def add_column_options!(sql, options)
|
51
53
|
sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
|
52
|
-
if options[:null] == false
|
53
|
-
sql << " NOT NULL"
|
54
|
-
end
|
55
54
|
if options[:collation].present?
|
56
55
|
sql << " COLLATE #{options[:collation]}"
|
57
56
|
end
|
57
|
+
if options[:null] == false
|
58
|
+
sql << " NOT NULL"
|
59
|
+
end
|
58
60
|
if options[:is_identity] == true
|
59
61
|
sql << " IDENTITY(1,1)"
|
60
62
|
end
|
@@ -39,6 +39,17 @@ module ActiveRecord
|
|
39
39
|
def default_primary_key?(column)
|
40
40
|
super && column.is_identity?
|
41
41
|
end
|
42
|
+
|
43
|
+
def schemas(stream)
|
44
|
+
schema_names = @connection.schema_names
|
45
|
+
|
46
|
+
if schema_names.any?
|
47
|
+
schema_names.sort.each do |name|
|
48
|
+
stream.puts " create_schema #{name.inspect}"
|
49
|
+
end
|
50
|
+
stream.puts
|
51
|
+
end
|
52
|
+
end
|
42
53
|
end
|
43
54
|
end
|
44
55
|
end
|
@@ -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,16 +118,21 @@ 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
|
-
|
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}'"
|
135
|
+
rename_table_indexes(table_name, new_name, **options)
|
131
136
|
end
|
132
137
|
|
133
138
|
def remove_column(table_name, column_name, type = nil, **options)
|
@@ -137,12 +142,21 @@ 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 = {})
|
144
149
|
sql_commands = []
|
145
150
|
indexes = []
|
151
|
+
|
152
|
+
if type == :datetime
|
153
|
+
# If no precision then default it to 6.
|
154
|
+
options[:precision] = 6 unless options.key?(:precision)
|
155
|
+
|
156
|
+
# If there is precision then column must be of type 'datetime2'.
|
157
|
+
type = :datetime2 unless options[:precision].nil?
|
158
|
+
end
|
159
|
+
|
146
160
|
column_object = schema_cache.columns(table_name).find { |c| c.name.to_s == column_name.to_s }
|
147
161
|
without_constraints = options.key?(:default) || options.key?(:limit)
|
148
162
|
default = if !options.key?(:default) && column_object
|
@@ -150,25 +164,30 @@ module ActiveRecord
|
|
150
164
|
else
|
151
165
|
options[:default]
|
152
166
|
end
|
167
|
+
|
153
168
|
if without_constraints || (column_object && column_object.type != type.to_sym)
|
154
169
|
remove_default_constraint(table_name, column_name)
|
155
170
|
indexes = indexes(table_name).select { |index| index.columns.include?(column_name.to_s) }
|
156
171
|
remove_indexes(table_name, column_name)
|
157
172
|
end
|
173
|
+
|
158
174
|
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?
|
159
175
|
alter_command = "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])}"
|
160
176
|
alter_command += " COLLATE #{options[:collation]}" if options[:collation].present?
|
161
177
|
alter_command += " NOT NULL" if !options[:null].nil? && options[:null] == false
|
162
178
|
sql_commands << alter_command
|
179
|
+
|
163
180
|
if without_constraints
|
164
181
|
default = quote_default_expression(default, column_object || column_for(table_name, column_name))
|
165
182
|
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)}"
|
166
183
|
end
|
184
|
+
|
167
185
|
# Add any removed indexes back
|
168
186
|
indexes.each do |index|
|
169
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(', ')})"
|
170
188
|
end
|
171
|
-
|
189
|
+
|
190
|
+
sql_commands.each { |c| execute(c) }
|
172
191
|
clear_cache!
|
173
192
|
end
|
174
193
|
|
@@ -179,7 +198,7 @@ module ActiveRecord
|
|
179
198
|
|
180
199
|
remove_default_constraint(table_name, column_name)
|
181
200
|
default = extract_new_default_value(default_or_changes)
|
182
|
-
|
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)}"
|
183
202
|
clear_cache!
|
184
203
|
end
|
185
204
|
|
@@ -199,23 +218,45 @@ module ActiveRecord
|
|
199
218
|
end
|
200
219
|
|
201
220
|
def remove_index!(table_name, index_name)
|
202
|
-
|
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)
|
203
236
|
end
|
204
237
|
|
205
238
|
def foreign_keys(table_name)
|
206
239
|
identifier = SQLServer::Utils.extract_identifiers(table_name)
|
207
240
|
fk_info = execute_procedure :sp_fkeys, nil, identifier.schema, nil, identifier.object, identifier.schema
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
211
245
|
options = {
|
212
246
|
name: row["FK_NAME"],
|
213
|
-
column: row["FKCOLUMN_NAME"],
|
214
|
-
primary_key: row["PKCOLUMN_NAME"],
|
215
247
|
on_update: extract_foreign_key_action("update", row["FK_NAME"]),
|
216
248
|
on_delete: extract_foreign_key_action("delete", row["FK_NAME"])
|
217
249
|
}
|
218
|
-
|
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)
|
219
260
|
end
|
220
261
|
end
|
221
262
|
|
@@ -226,9 +267,33 @@ module ActiveRecord
|
|
226
267
|
end
|
227
268
|
end
|
228
269
|
|
270
|
+
def check_constraints(table_name)
|
271
|
+
sql = <<~SQL
|
272
|
+
select chk.name AS 'name',
|
273
|
+
chk.definition AS 'expression'
|
274
|
+
from sys.check_constraints chk
|
275
|
+
inner join sys.tables st on chk.parent_object_id = st.object_id
|
276
|
+
where
|
277
|
+
st.name = '#{table_name}'
|
278
|
+
SQL
|
279
|
+
|
280
|
+
chk_info = internal_exec_query(sql, "SCHEMA")
|
281
|
+
|
282
|
+
chk_info.map do |row|
|
283
|
+
options = {
|
284
|
+
name: row["name"]
|
285
|
+
}
|
286
|
+
expression = row["expression"]
|
287
|
+
expression = expression[1..-2] if expression.start_with?("(") && expression.end_with?(")")
|
288
|
+
|
289
|
+
CheckConstraintDefinition.new(table_name, expression, options)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
229
293
|
def type_to_sql(type, limit: nil, precision: nil, scale: nil, **)
|
230
|
-
type_limitable = %w(string integer float char nchar varchar nvarchar).include?(type.to_s)
|
294
|
+
type_limitable = %w(string integer float char nchar varchar nvarchar binary_basic).include?(type.to_s)
|
231
295
|
limit = nil unless type_limitable
|
296
|
+
|
232
297
|
case type.to_s
|
233
298
|
when "integer"
|
234
299
|
case limit
|
@@ -238,6 +303,16 @@ module ActiveRecord
|
|
238
303
|
when 5..8 then "bigint"
|
239
304
|
else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
|
240
305
|
end
|
306
|
+
when "time" # https://learn.microsoft.com/en-us/sql/t-sql/data-types/time-transact-sql
|
307
|
+
column_type_sql = type.to_s
|
308
|
+
if precision
|
309
|
+
if (0..7) === precision
|
310
|
+
column_type_sql << "(#{precision})"
|
311
|
+
else
|
312
|
+
raise(ActiveRecordError, "The time type has precision of #{precision}. The allowed range of precision is from 0 to 7")
|
313
|
+
end
|
314
|
+
end
|
315
|
+
column_type_sql
|
241
316
|
when "datetime2"
|
242
317
|
column_type_sql = super
|
243
318
|
if precision
|
@@ -248,35 +323,59 @@ module ActiveRecord
|
|
248
323
|
end
|
249
324
|
end
|
250
325
|
column_type_sql
|
326
|
+
when "datetimeoffset"
|
327
|
+
column_type_sql = super
|
328
|
+
if precision
|
329
|
+
if (0..7) === precision
|
330
|
+
column_type_sql << "(#{precision})"
|
331
|
+
else
|
332
|
+
raise(ActiveRecordError, "The datetimeoffset type has precision of #{precision}. The allowed range of precision is from 0 to 7")
|
333
|
+
end
|
334
|
+
end
|
335
|
+
column_type_sql
|
251
336
|
else
|
252
337
|
super
|
253
338
|
end
|
254
339
|
end
|
255
340
|
|
341
|
+
# In SQL Server only the first column added should have the `ADD` keyword.
|
342
|
+
def add_timestamps(table_name, **options)
|
343
|
+
fragments = add_timestamps_for_alter(table_name, **options)
|
344
|
+
fragments[1..].each { |fragment| fragment.sub!('ADD ', '') }
|
345
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} #{fragments.join(', ')}"
|
346
|
+
end
|
347
|
+
|
256
348
|
def columns_for_distinct(columns, orders)
|
257
349
|
order_columns = orders.reject(&:blank?).map { |s|
|
258
|
-
s = s
|
350
|
+
s = visitor.compile(s) unless s.is_a?(String)
|
259
351
|
s.gsub(/\s+(?:ASC|DESC)\b/i, "")
|
260
352
|
.gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
|
261
|
-
|
353
|
+
}
|
354
|
+
.reject(&:blank?)
|
355
|
+
.reject { |s| columns.include?(s) }
|
356
|
+
|
357
|
+
order_columns_aliased = order_columns.map.with_index { |column, i| "#{column} AS alias_#{i}" }
|
262
358
|
|
263
|
-
(
|
359
|
+
(order_columns_aliased << super).join(", ")
|
264
360
|
end
|
265
361
|
|
266
362
|
def update_table_definition(table_name, base)
|
267
363
|
SQLServer::Table.new(table_name, base)
|
268
364
|
end
|
269
365
|
|
270
|
-
def change_column_null(table_name, column_name,
|
366
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
367
|
+
validate_change_column_null_argument!(null)
|
368
|
+
|
271
369
|
table_id = SQLServer::Utils.extract_identifiers(table_name)
|
272
370
|
column_id = SQLServer::Utils.extract_identifiers(column_name)
|
273
371
|
column = column_for(table_name, column_name)
|
274
|
-
if !
|
275
|
-
|
372
|
+
if !null.nil? && null == false && !default.nil?
|
373
|
+
execute("UPDATE #{table_id} SET #{column_id}=#{quote(default)} WHERE #{column_id} IS NULL")
|
276
374
|
end
|
277
375
|
sql = "ALTER TABLE #{table_id} ALTER COLUMN #{column_id} #{type_to_sql column.type, limit: column.limit, precision: column.precision, scale: column.scale}"
|
278
|
-
sql += " NOT NULL" if !
|
279
|
-
|
376
|
+
sql += " NOT NULL" if !null.nil? && null == false
|
377
|
+
|
378
|
+
execute sql
|
280
379
|
end
|
281
380
|
|
282
381
|
def create_schema_dumper(options)
|
@@ -298,19 +397,37 @@ module ActiveRecord
|
|
298
397
|
execute "DROP SCHEMA [#{schema_name}]"
|
299
398
|
end
|
300
399
|
|
400
|
+
# Returns an array of schema names.
|
401
|
+
def schema_names
|
402
|
+
sql = <<~SQL.squish
|
403
|
+
SELECT name
|
404
|
+
FROM sys.schemas
|
405
|
+
WHERE
|
406
|
+
name NOT LIKE 'db_%' AND
|
407
|
+
name NOT IN ('INFORMATION_SCHEMA', 'sys', 'guest')
|
408
|
+
SQL
|
409
|
+
|
410
|
+
query_values(sql, "SCHEMA")
|
411
|
+
end
|
412
|
+
|
301
413
|
private
|
302
414
|
|
303
415
|
def data_source_sql(name = nil, type: nil)
|
304
|
-
scope = quoted_scope
|
416
|
+
scope = quoted_scope(name, type: type)
|
305
417
|
|
306
|
-
|
307
|
-
|
418
|
+
table_schema = lowercase_schema_reflection_sql('TABLE_SCHEMA')
|
419
|
+
table_name = lowercase_schema_reflection_sql('TABLE_NAME')
|
420
|
+
database = scope[:database].present? ? "#{scope[:database]}." : ""
|
308
421
|
table_catalog = scope[:database].present? ? quote(scope[:database]) : "DB_NAME()"
|
309
422
|
|
310
|
-
sql = "SELECT
|
423
|
+
sql = "SELECT "
|
424
|
+
sql += " CASE"
|
425
|
+
sql += " WHEN #{table_schema} = 'dbo' THEN #{table_name}"
|
426
|
+
sql += " ELSE CONCAT(#{table_schema}, '.', #{table_name})"
|
427
|
+
sql += " END"
|
311
428
|
sql += " FROM #{database}INFORMATION_SCHEMA.TABLES WITH (NOLOCK)"
|
312
429
|
sql += " WHERE TABLE_CATALOG = #{table_catalog}"
|
313
|
-
sql += " AND TABLE_SCHEMA = #{quote(scope[:schema])}"
|
430
|
+
sql += " AND TABLE_SCHEMA = #{quote(scope[:schema])}" if scope[:schema]
|
314
431
|
sql += " AND TABLE_NAME = #{quote(scope[:name])}" if scope[:name]
|
315
432
|
sql += " AND TABLE_TYPE = #{quote(scope[:type])}" if scope[:type]
|
316
433
|
sql += " ORDER BY #{table_name}"
|
@@ -319,9 +436,10 @@ module ActiveRecord
|
|
319
436
|
|
320
437
|
def quoted_scope(name = nil, type: nil)
|
321
438
|
identifier = SQLServer::Utils.extract_identifiers(name)
|
439
|
+
|
322
440
|
{}.tap do |scope|
|
323
441
|
scope[:database] = identifier.database if identifier.database
|
324
|
-
scope[:schema] = identifier.schema || "dbo"
|
442
|
+
scope[:schema] = identifier.schema || "dbo" if name.present?
|
325
443
|
scope[:name] = identifier.object if identifier.object
|
326
444
|
scope[:type] = type if type
|
327
445
|
end
|
@@ -346,7 +464,7 @@ module ActiveRecord
|
|
346
464
|
datetime2: { name: "datetime2" },
|
347
465
|
datetimeoffset: { name: "datetimeoffset" },
|
348
466
|
smalldatetime: { name: "smalldatetime" },
|
349
|
-
timestamp: { name: "
|
467
|
+
timestamp: { name: "datetime2(6)" },
|
350
468
|
time: { name: "time" },
|
351
469
|
char: { name: "char" },
|
352
470
|
varchar: { name: "varchar", limit: 8000 },
|
@@ -371,14 +489,25 @@ module ActiveRecord
|
|
371
489
|
view_exists = view_exists?(table_name)
|
372
490
|
view_tblnm = view_table_name(table_name) if view_exists
|
373
491
|
|
492
|
+
if view_exists
|
493
|
+
sql = <<~SQL
|
494
|
+
SELECT LOWER(c.COLUMN_NAME) AS [name], c.COLUMN_DEFAULT AS [default]
|
495
|
+
FROM #{database}.INFORMATION_SCHEMA.COLUMNS c
|
496
|
+
WHERE c.TABLE_NAME = #{quote(view_tblnm)}
|
497
|
+
SQL
|
498
|
+
results = internal_exec_query(sql, "SCHEMA")
|
499
|
+
default_functions = results.each.with_object({}) { |row, out| out[row["name"]] = row["default"] }.compact
|
500
|
+
end
|
501
|
+
|
374
502
|
sql = column_definitions_sql(database, identifier)
|
375
503
|
|
376
504
|
binds = []
|
377
505
|
nv128 = SQLServer::Type::UnicodeVarchar.new limit: 128
|
378
506
|
binds << Relation::QueryAttribute.new("TABLE_NAME", identifier.object, nv128)
|
379
507
|
binds << Relation::QueryAttribute.new("TABLE_SCHEMA", identifier.schema, nv128) unless identifier.schema.blank?
|
380
|
-
results =
|
381
|
-
|
508
|
+
results = internal_exec_query(sql, "SCHEMA", binds)
|
509
|
+
|
510
|
+
columns = results.map do |ci|
|
382
511
|
ci = ci.symbolize_keys
|
383
512
|
ci[:_type] = ci[:type]
|
384
513
|
ci[:table_name] = view_tblnm || table_name
|
@@ -402,13 +531,8 @@ module ActiveRecord
|
|
402
531
|
ci[:default_function] = begin
|
403
532
|
default = ci[:default_value]
|
404
533
|
if default.nil? && view_exists
|
405
|
-
|
406
|
-
|
407
|
-
FROM #{database}.INFORMATION_SCHEMA.COLUMNS c
|
408
|
-
WHERE
|
409
|
-
c.TABLE_NAME = '#{view_tblnm}'
|
410
|
-
AND c.COLUMN_NAME = '#{views_real_column_name(table_name, ci[:name])}'
|
411
|
-
}.squish, "SCHEMA"
|
534
|
+
view_column = views_real_column_name(table_name, ci[:name]).downcase
|
535
|
+
default = default_functions[view_column] if view_column.present?
|
412
536
|
end
|
413
537
|
case default
|
414
538
|
when nil
|
@@ -437,6 +561,13 @@ module ActiveRecord
|
|
437
561
|
ci[:is_identity] = ci[:is_identity].to_i == 1 unless [TrueClass, FalseClass].include?(ci[:is_identity].class)
|
438
562
|
ci
|
439
563
|
end
|
564
|
+
|
565
|
+
# Since Rails 7, it's expected that all adapter raise error when table doesn't exists.
|
566
|
+
# I'm not aware of the possibility of tables without columns on SQL Server (postgres have those).
|
567
|
+
# Raise error if the method return an empty array
|
568
|
+
columns.tap do |result|
|
569
|
+
raise ActiveRecord::StatementInvalid, "Table '#{table_name}' doesn't exist" if result.empty?
|
570
|
+
end
|
440
571
|
end
|
441
572
|
|
442
573
|
def column_definitions_sql(database, identifier)
|
@@ -507,10 +638,17 @@ module ActiveRecord
|
|
507
638
|
}.gsub(/[ \t\r\n]+/, " ").strip
|
508
639
|
end
|
509
640
|
|
641
|
+
def remove_columns_for_alter(table_name, *column_names, **options)
|
642
|
+
first, *rest = column_names
|
643
|
+
|
644
|
+
# return an array like this [DROP COLUMN col_1, col_2, col_3]. Abstract adapter joins fragments with ", "
|
645
|
+
[remove_column_for_alter(table_name, first)] + rest.map { |column_name| quote_column_name(column_name) }
|
646
|
+
end
|
647
|
+
|
510
648
|
def remove_check_constraints(table_name, column_name)
|
511
649
|
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"
|
512
650
|
constraints.each do |constraint|
|
513
|
-
|
651
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
|
514
652
|
end
|
515
653
|
end
|
516
654
|
|
@@ -519,7 +657,7 @@ module ActiveRecord
|
|
519
657
|
execute_procedure(:sp_helpconstraint, table_name, "nomsg").flatten.select do |row|
|
520
658
|
row["constraint_type"] == "DEFAULT on column #{column_name}"
|
521
659
|
end.each do |row|
|
522
|
-
|
660
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{row['constraint_name']}"
|
523
661
|
end
|
524
662
|
end
|
525
663
|
|
@@ -531,17 +669,31 @@ module ActiveRecord
|
|
531
669
|
|
532
670
|
# === SQLServer Specific (Misc Helpers) ========================= #
|
533
671
|
|
672
|
+
# Parses just the table name from the SQL. Table name does not include database/schema/etc.
|
534
673
|
def get_table_name(sql)
|
535
|
-
tn =
|
536
|
-
Regexp.last_match[3] || Regexp.last_match[4]
|
537
|
-
elsif sql =~ /FROM\s+([^\(\s]+)\s*/i
|
538
|
-
Regexp.last_match[1]
|
539
|
-
else
|
540
|
-
nil
|
541
|
-
end
|
674
|
+
tn = get_raw_table_name(sql)
|
542
675
|
SQLServer::Utils.extract_identifiers(tn).object
|
543
676
|
end
|
544
677
|
|
678
|
+
# Parses the raw table name that is used in the SQL. Table name could include database/schema/etc.
|
679
|
+
def get_raw_table_name(sql)
|
680
|
+
return if sql.blank?
|
681
|
+
|
682
|
+
s = sql.gsub(/^\s*EXEC sp_executesql N'/i, "")
|
683
|
+
|
684
|
+
if s.match?(/^\s*INSERT INTO.*/i)
|
685
|
+
s.split(/INSERT INTO/i)[1]
|
686
|
+
.split(/OUTPUT INSERTED/i)[0]
|
687
|
+
.split(/(DEFAULT)?\s+VALUES/i)[0]
|
688
|
+
.split(/\bSELECT\b(?![^\[]*\])/i)[0]
|
689
|
+
.match(/\s*([^(]*)/i)[0]
|
690
|
+
elsif s.match?(/^\s*UPDATE\s+.*/i)
|
691
|
+
s.match(/UPDATE\s+([^\(\s]+)\s*/i)[1]
|
692
|
+
else
|
693
|
+
s.match(/FROM[\s|\(]+((\[[^\(\]]+\])|[^\(\s]+)\s*/i)[1]
|
694
|
+
end.strip
|
695
|
+
end
|
696
|
+
|
545
697
|
def default_constraint_name(table_name, column_name)
|
546
698
|
"DF_#{table_name}_#{column_name}"
|
547
699
|
end
|
@@ -561,18 +713,21 @@ module ActiveRecord
|
|
561
713
|
@view_information ||= {}
|
562
714
|
@view_information[table_name] ||= begin
|
563
715
|
identifier = SQLServer::Utils.extract_identifiers(table_name)
|
564
|
-
|
716
|
+
information_query_table = identifier.database.present? ? "[#{identifier.database}].[INFORMATION_SCHEMA].[VIEWS]" : "[INFORMATION_SCHEMA].[VIEWS]"
|
717
|
+
view_info = select_one "SELECT * FROM #{information_query_table} WITH (NOLOCK) WHERE TABLE_NAME = #{quote(identifier.object)}", "SCHEMA"
|
718
|
+
|
565
719
|
if view_info
|
566
720
|
view_info = view_info.with_indifferent_access
|
567
721
|
if view_info[:VIEW_DEFINITION].blank? || view_info[:VIEW_DEFINITION].length == 4000
|
568
722
|
view_info[:VIEW_DEFINITION] = begin
|
569
|
-
|
723
|
+
select_values("EXEC sp_helptext #{identifier.object_quoted}", "SCHEMA").join
|
570
724
|
rescue
|
571
725
|
warn "No view definition found, possible permissions problem.\nPlease run GRANT VIEW DEFINITION TO your_user;"
|
572
726
|
nil
|
573
|
-
|
727
|
+
end
|
574
728
|
end
|
575
729
|
end
|
730
|
+
|
576
731
|
view_info
|
577
732
|
end
|
578
733
|
end
|
@@ -581,7 +736,8 @@ module ActiveRecord
|
|
581
736
|
view_definition = view_information(table_name)[:VIEW_DEFINITION]
|
582
737
|
return column_name unless view_definition
|
583
738
|
|
584
|
-
|
739
|
+
# Remove "CREATE VIEW ... AS SELECT ..." and then match the column name.
|
740
|
+
match_data = view_definition.sub(/CREATE\s+VIEW.*AS\s+SELECT\s/, '').match(/([\w-]*)\s+AS\s+#{column_name}\W/im)
|
585
741
|
match_data ? match_data[1] : column_name
|
586
742
|
end
|
587
743
|
|
@@ -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
|
@@ -101,13 +101,24 @@ module ActiveRecord
|
|
101
101
|
|
102
102
|
def new_column_definition(name, type, **options)
|
103
103
|
case type
|
104
|
-
when :datetime
|
105
|
-
|
104
|
+
when :datetime, :timestamp
|
105
|
+
# If no precision then default it to 6.
|
106
|
+
options[:precision] = 6 unless options.key?(:precision)
|
107
|
+
|
108
|
+
# If there is precision then column must be of type 'datetime2'.
|
109
|
+
type = :datetime2 unless options[:precision].nil?
|
106
110
|
when :primary_key
|
107
111
|
options[:is_identity] = true
|
108
112
|
end
|
113
|
+
|
109
114
|
super
|
110
115
|
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def valid_column_definition_options
|
120
|
+
super + [:is_identity]
|
121
|
+
end
|
111
122
|
end
|
112
123
|
|
113
124
|
class Table < ActiveRecord::ConnectionAdapters::Table
|