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
@@ -4,45 +4,66 @@ module ActiveRecord
|
|
4
4
|
module ConnectionAdapters
|
5
5
|
module SQLServer
|
6
6
|
module DatabaseStatements
|
7
|
-
READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :dbcc, :explain, :save, :select, :set, :rollback, :waitfor) # :nodoc:
|
7
|
+
READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :dbcc, :explain, :save, :select, :set, :rollback, :waitfor, :use) # :nodoc:
|
8
8
|
private_constant :READ_QUERY
|
9
9
|
|
10
10
|
def write_query?(sql) # :nodoc:
|
11
11
|
!READ_QUERY.match?(sql)
|
12
|
+
rescue ArgumentError # Invalid encoding
|
13
|
+
!READ_QUERY.match?(sql.b)
|
14
|
+
end
|
15
|
+
|
16
|
+
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
|
17
|
+
log(sql, name, async: async) do |notification_payload|
|
18
|
+
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
|
19
|
+
result = if id_insert_table_name = query_requires_identity_insert?(sql)
|
20
|
+
with_identity_insert_enabled(id_insert_table_name, conn) { internal_raw_execute(sql, conn, perform_do: true) }
|
21
|
+
else
|
22
|
+
internal_raw_execute(sql, conn, perform_do: true)
|
23
|
+
end
|
24
|
+
verified!
|
25
|
+
notification_payload[:row_count] = result
|
26
|
+
result
|
27
|
+
end
|
28
|
+
end
|
12
29
|
end
|
13
30
|
|
14
|
-
def
|
15
|
-
|
16
|
-
raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
|
17
|
-
end
|
31
|
+
def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false, allow_retry: false)
|
32
|
+
sql = transform_query(sql)
|
18
33
|
|
19
|
-
|
34
|
+
check_if_write_query(sql)
|
20
35
|
mark_transaction_written_if_write(sql)
|
21
36
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
do_execute(sql, name)
|
37
|
+
unless without_prepared_statement?(binds)
|
38
|
+
types, params = sp_executesql_types_and_parameters(binds)
|
39
|
+
sql = sp_executesql_sql(sql, types, params, name)
|
26
40
|
end
|
27
|
-
end
|
28
41
|
|
29
|
-
|
30
|
-
|
31
|
-
|
42
|
+
log(sql, name, binds, async: async) do |notification_payload|
|
43
|
+
with_raw_connection do |conn|
|
44
|
+
result = if id_insert_table_name = query_requires_identity_insert?(sql)
|
45
|
+
# If the table name is a view, we need to get the base table name for enabling identity insert.
|
46
|
+
id_insert_table_name = view_table_name(id_insert_table_name) if view_exists?(id_insert_table_name)
|
47
|
+
|
48
|
+
with_identity_insert_enabled(id_insert_table_name, conn) do
|
49
|
+
internal_exec_sql_query(sql, conn)
|
50
|
+
end
|
51
|
+
else
|
52
|
+
internal_exec_sql_query(sql, conn)
|
53
|
+
end
|
54
|
+
|
55
|
+
verified!
|
56
|
+
notification_payload[:row_count] = result.count
|
57
|
+
result
|
58
|
+
end
|
32
59
|
end
|
33
|
-
|
34
|
-
materialize_transactions
|
35
|
-
mark_transaction_written_if_write(sql)
|
36
|
-
|
37
|
-
sp_executesql(sql, name, binds, prepare: prepare)
|
38
60
|
end
|
39
61
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
62
|
+
def internal_exec_sql_query(sql, conn)
|
63
|
+
handle = internal_raw_execute(sql, conn)
|
64
|
+
handle_to_names_and_values(handle, ar_result: true)
|
65
|
+
ensure
|
66
|
+
finish_statement_handle(handle)
|
46
67
|
end
|
47
68
|
|
48
69
|
def exec_delete(sql, name, binds)
|
@@ -56,7 +77,7 @@ module ActiveRecord
|
|
56
77
|
end
|
57
78
|
|
58
79
|
def begin_db_transaction
|
59
|
-
|
80
|
+
internal_execute("BEGIN TRANSACTION", "TRANSACTION", allow_retry: true, materialize_transactions: false)
|
60
81
|
end
|
61
82
|
|
62
83
|
def transaction_isolation_levels
|
@@ -64,33 +85,20 @@ module ActiveRecord
|
|
64
85
|
end
|
65
86
|
|
66
87
|
def begin_isolated_db_transaction(isolation)
|
67
|
-
set_transaction_isolation_level
|
88
|
+
set_transaction_isolation_level(transaction_isolation_levels.fetch(isolation))
|
68
89
|
begin_db_transaction
|
69
90
|
end
|
70
91
|
|
71
92
|
def set_transaction_isolation_level(isolation_level)
|
72
|
-
|
93
|
+
internal_execute("SET TRANSACTION ISOLATION LEVEL #{isolation_level}", "TRANSACTION", allow_retry: true, materialize_transactions: false)
|
73
94
|
end
|
74
95
|
|
75
96
|
def commit_db_transaction
|
76
|
-
|
97
|
+
internal_execute("COMMIT TRANSACTION", "TRANSACTION", allow_retry: false, materialize_transactions: true)
|
77
98
|
end
|
78
99
|
|
79
100
|
def exec_rollback_db_transaction
|
80
|
-
|
81
|
-
end
|
82
|
-
|
83
|
-
include Savepoints
|
84
|
-
|
85
|
-
def create_savepoint(name = current_savepoint_name)
|
86
|
-
do_execute "SAVE TRANSACTION #{name}", "TRANSACTION"
|
87
|
-
end
|
88
|
-
|
89
|
-
def exec_rollback_to_savepoint(name = current_savepoint_name)
|
90
|
-
do_execute "ROLLBACK TRANSACTION #{name}", "TRANSACTION"
|
91
|
-
end
|
92
|
-
|
93
|
-
def release_savepoint(name = current_savepoint_name)
|
101
|
+
internal_execute("IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION", "TRANSACTION", allow_retry: false, materialize_transactions: true)
|
94
102
|
end
|
95
103
|
|
96
104
|
def case_sensitive_comparison(attribute, value)
|
@@ -145,7 +153,12 @@ module ActiveRecord
|
|
145
153
|
sql = +"INSERT #{insert.into}"
|
146
154
|
|
147
155
|
if returning = insert.send(:insert_all).returning
|
148
|
-
|
156
|
+
returning_sql = if returning.is_a?(String)
|
157
|
+
returning
|
158
|
+
else
|
159
|
+
returning.map { |column| "INSERTED.#{quote_column_name(column)}" }.join(", ")
|
160
|
+
end
|
161
|
+
sql << " OUTPUT #{returning_sql}"
|
149
162
|
end
|
150
163
|
|
151
164
|
sql << " #{insert.values_list}"
|
@@ -155,42 +168,44 @@ module ActiveRecord
|
|
155
168
|
# === SQLServer Specific ======================================== #
|
156
169
|
|
157
170
|
def execute_procedure(proc_name, *variables)
|
158
|
-
materialize_transactions
|
159
|
-
|
160
171
|
vars = if variables.any? && variables.first.is_a?(Hash)
|
161
172
|
variables.first.map { |k, v| "@#{k} = #{quote(v)}" }
|
162
173
|
else
|
163
174
|
variables.map { |v| quote(v) }
|
164
175
|
end.join(", ")
|
165
176
|
sql = "EXEC #{proc_name} #{vars}".strip
|
166
|
-
|
167
|
-
log(sql,
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
options = { as: :hash, cache_rows: true, timezone: ActiveRecord
|
177
|
+
|
178
|
+
log(sql, "Execute Procedure") do |notification_payload|
|
179
|
+
with_raw_connection do |conn|
|
180
|
+
result = internal_raw_execute(sql, conn)
|
181
|
+
verified!
|
182
|
+
options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc }
|
183
|
+
|
172
184
|
result.each(options) do |row|
|
173
185
|
r = row.with_indifferent_access
|
174
186
|
yield(r) if block_given?
|
175
187
|
end
|
176
|
-
|
188
|
+
|
189
|
+
result = result.each.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row }
|
190
|
+
notification_payload[:row_count] = result.count
|
191
|
+
result
|
177
192
|
end
|
178
193
|
end
|
179
194
|
end
|
180
195
|
|
181
|
-
def with_identity_insert_enabled(table_name)
|
196
|
+
def with_identity_insert_enabled(table_name, conn)
|
182
197
|
table_name = quote_table_name(table_name)
|
183
|
-
set_identity_insert(table_name, true)
|
198
|
+
set_identity_insert(table_name, conn, true)
|
184
199
|
yield
|
185
200
|
ensure
|
186
|
-
set_identity_insert(table_name, false)
|
201
|
+
set_identity_insert(table_name, conn, false)
|
187
202
|
end
|
188
203
|
|
189
204
|
def use_database(database = nil)
|
190
205
|
return if sqlserver_azure?
|
191
206
|
|
192
|
-
name = SQLServer::Utils.extract_identifiers(database || @
|
193
|
-
|
207
|
+
name = SQLServer::Utils.extract_identifiers(database || @connection_parameters[:database]).quoted
|
208
|
+
execute("USE #{name}", "SCHEMA") unless name.blank?
|
194
209
|
end
|
195
210
|
|
196
211
|
def user_options
|
@@ -254,59 +269,56 @@ module ActiveRecord
|
|
254
269
|
|
255
270
|
protected
|
256
271
|
|
257
|
-
def sql_for_insert(sql, pk, binds)
|
272
|
+
def sql_for_insert(sql, pk, binds, returning)
|
258
273
|
if pk.nil?
|
259
274
|
table_name = query_requires_identity_insert?(sql)
|
260
275
|
pk = primary_key(table_name)
|
261
276
|
end
|
262
277
|
|
263
278
|
sql = if pk && use_output_inserted? && !database_prefix_remote_server?
|
264
|
-
quoted_pk = SQLServer::Utils.extract_identifiers(pk).quoted
|
265
279
|
table_name ||= get_table_name(sql)
|
266
280
|
exclude_output_inserted = exclude_output_inserted_table_name?(table_name, sql)
|
267
281
|
|
268
282
|
if exclude_output_inserted
|
269
|
-
|
283
|
+
pk_and_types = Array(pk).map do |subkey|
|
284
|
+
{
|
285
|
+
quoted: SQLServer::Utils.extract_identifiers(subkey).quoted,
|
286
|
+
id_sql_type: exclude_output_inserted_id_sql_type(subkey, exclude_output_inserted)
|
287
|
+
}
|
288
|
+
end
|
289
|
+
|
270
290
|
<<~SQL.squish
|
271
|
-
DECLARE @ssaIdInsertTable table (#{
|
272
|
-
#{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{
|
273
|
-
SELECT CAST(#{
|
291
|
+
DECLARE @ssaIdInsertTable table (#{pk_and_types.map { |pk_and_type| "#{pk_and_type[:quoted]} #{pk_and_type[:id_sql_type]}"}.join(", ") });
|
292
|
+
#{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT #{ pk_and_types.map { |pk_and_type| "INSERTED.#{pk_and_type[:quoted]}" }.join(", ") } INTO @ssaIdInsertTable"}
|
293
|
+
SELECT #{pk_and_types.map {|pk_and_type| "CAST(#{pk_and_type[:quoted]} AS #{pk_and_type[:id_sql_type]}) #{pk_and_type[:quoted]}"}.join(", ")} FROM @ssaIdInsertTable
|
274
294
|
SQL
|
275
295
|
else
|
276
|
-
|
296
|
+
returning_columns = returning || Array(pk)
|
297
|
+
|
298
|
+
if returning_columns.any?
|
299
|
+
returning_columns_statements = returning_columns.map { |c| " INSERTED.#{SQLServer::Utils.extract_identifiers(c).quoted}" }
|
300
|
+
sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i), " OUTPUT" + returning_columns_statements.join(",")
|
301
|
+
else
|
302
|
+
sql
|
303
|
+
end
|
277
304
|
end
|
278
305
|
else
|
279
306
|
"#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"
|
280
307
|
end
|
281
|
-
|
308
|
+
|
309
|
+
[sql, binds]
|
282
310
|
end
|
283
311
|
|
284
312
|
# === SQLServer Specific ======================================== #
|
285
313
|
|
286
|
-
def set_identity_insert(table_name, enable
|
287
|
-
|
314
|
+
def set_identity_insert(table_name, conn, enable)
|
315
|
+
internal_raw_execute("SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}", conn , perform_do: true)
|
288
316
|
rescue Exception
|
289
317
|
raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
|
290
318
|
end
|
291
319
|
|
292
320
|
# === SQLServer Specific (Executing) ============================ #
|
293
321
|
|
294
|
-
def do_execute(sql, name = "SQL")
|
295
|
-
materialize_transactions
|
296
|
-
mark_transaction_written_if_write(sql)
|
297
|
-
|
298
|
-
log(sql, name) { raw_connection_do(sql) }
|
299
|
-
end
|
300
|
-
|
301
|
-
def sp_executesql(sql, name, binds, options = {})
|
302
|
-
options[:ar_result] = true if options[:fetch] != :rows
|
303
|
-
unless without_prepared_statement?(binds)
|
304
|
-
types, params = sp_executesql_types_and_parameters(binds)
|
305
|
-
sql = sp_executesql_sql(sql, types, params, name)
|
306
|
-
end
|
307
|
-
raw_select sql, name, binds, options
|
308
|
-
end
|
309
|
-
|
310
322
|
def sp_executesql_types_and_parameters(binds)
|
311
323
|
types, params = [], []
|
312
324
|
binds.each_with_index do |attr, index|
|
@@ -319,10 +331,17 @@ module ActiveRecord
|
|
319
331
|
end
|
320
332
|
|
321
333
|
def sp_executesql_sql_type(attr)
|
322
|
-
|
334
|
+
if attr.respond_to?(:type)
|
335
|
+
return attr.type.sqlserver_type if attr.type.respond_to?(:sqlserver_type)
|
323
336
|
|
324
|
-
|
325
|
-
|
337
|
+
if attr.type.is_a?(ActiveRecord::Encryption::EncryptedAttributeType) && attr.type.instance_variable_get(:@cast_type).respond_to?(:sqlserver_type)
|
338
|
+
return attr.type.instance_variable_get(:@cast_type).sqlserver_type
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
value = active_model_attribute?(attr) ? attr.value_for_database : attr
|
343
|
+
|
344
|
+
if value.is_a?(Numeric)
|
326
345
|
value > 2_147_483_647 ? "bigint".freeze : "int".freeze
|
327
346
|
else
|
328
347
|
"nvarchar(max)".freeze
|
@@ -330,15 +349,20 @@ module ActiveRecord
|
|
330
349
|
end
|
331
350
|
|
332
351
|
def sp_executesql_sql_param(attr)
|
352
|
+
return quote(attr) unless active_model_attribute?(attr)
|
353
|
+
|
333
354
|
case value = attr.value_for_database
|
334
|
-
when Type::Binary::Data,
|
335
|
-
ActiveRecord::Type::SQLServer::Data
|
355
|
+
when Type::Binary::Data, ActiveRecord::Type::SQLServer::Data
|
336
356
|
quote(value)
|
337
357
|
else
|
338
358
|
quote(type_cast(value))
|
339
359
|
end
|
340
360
|
end
|
341
361
|
|
362
|
+
def active_model_attribute?(type)
|
363
|
+
type.is_a?(::ActiveModel::Attribute)
|
364
|
+
end
|
365
|
+
|
342
366
|
def sp_executesql_sql(sql, types, params, name)
|
343
367
|
if name == "EXPLAIN"
|
344
368
|
params.each.with_index do |param, index|
|
@@ -351,17 +375,7 @@ module ActiveRecord
|
|
351
375
|
sql = "EXEC sp_executesql #{quote(sql)}"
|
352
376
|
sql += ", #{types}, #{params}" unless params.empty?
|
353
377
|
end
|
354
|
-
sql
|
355
|
-
end
|
356
|
-
|
357
|
-
def raw_connection_do(sql)
|
358
|
-
case @connection_options[:mode]
|
359
|
-
when :dblib
|
360
|
-
result = ensure_established_connection! { dblib_execute(sql) }
|
361
|
-
result.do
|
362
|
-
end
|
363
|
-
ensure
|
364
|
-
@update_sql = false
|
378
|
+
sql.freeze
|
365
379
|
end
|
366
380
|
|
367
381
|
# === SQLServer Specific (Identity Inserts) ===================== #
|
@@ -383,23 +397,23 @@ module ActiveRecord
|
|
383
397
|
self.class.exclude_output_inserted_table_names[table_name]
|
384
398
|
end
|
385
399
|
|
386
|
-
def
|
387
|
-
|
400
|
+
def exclude_output_inserted_id_sql_type(pk, exclude_output_inserted)
|
401
|
+
return "bigint" if exclude_output_inserted.is_a?(TrueClass)
|
402
|
+
return exclude_output_inserted[pk.to_sym] if exclude_output_inserted.is_a?(Hash)
|
403
|
+
exclude_output_inserted
|
388
404
|
end
|
389
405
|
|
390
406
|
def query_requires_identity_insert?(sql)
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
false
|
398
|
-
end
|
407
|
+
return false unless insert_sql?(sql)
|
408
|
+
|
409
|
+
raw_table_name = get_raw_table_name(sql)
|
410
|
+
id_column = identity_columns(raw_table_name).first
|
411
|
+
|
412
|
+
id_column && sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)[^(]+\([^)]*\b(#{id_column.name})\b,?[^)]*\)/i ? SQLServer::Utils.extract_identifiers(raw_table_name).quoted : false
|
399
413
|
end
|
400
414
|
|
401
415
|
def insert_sql?(sql)
|
402
|
-
!(sql =~
|
416
|
+
!(sql =~ /\A\s*(INSERT|EXEC sp_executesql N'INSERT)/i).nil?
|
403
417
|
end
|
404
418
|
|
405
419
|
def identity_columns(table_name)
|
@@ -408,68 +422,42 @@ module ActiveRecord
|
|
408
422
|
|
409
423
|
# === SQLServer Specific (Selecting) ============================ #
|
410
424
|
|
411
|
-
def
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
def _raw_select(sql, options = {})
|
416
|
-
handle = raw_connection_run(sql)
|
417
|
-
handle_to_names_and_values(handle, options)
|
425
|
+
def _raw_select(sql, conn)
|
426
|
+
handle = internal_raw_execute(sql, conn)
|
427
|
+
handle_to_names_and_values(handle, fetch: :rows)
|
418
428
|
ensure
|
419
429
|
finish_statement_handle(handle)
|
420
430
|
end
|
421
431
|
|
422
|
-
def raw_connection_run(sql)
|
423
|
-
case @connection_options[:mode]
|
424
|
-
when :dblib
|
425
|
-
ensure_established_connection! { dblib_execute(sql) }
|
426
|
-
end
|
427
|
-
end
|
428
|
-
|
429
|
-
def handle_more_results?(handle)
|
430
|
-
case @connection_options[:mode]
|
431
|
-
when :dblib
|
432
|
-
end
|
433
|
-
end
|
434
|
-
|
435
432
|
def handle_to_names_and_values(handle, options = {})
|
436
|
-
case @connection_options[:mode]
|
437
|
-
when :dblib
|
438
|
-
handle_to_names_and_values_dblib(handle, options)
|
439
|
-
end
|
440
|
-
end
|
441
|
-
|
442
|
-
def handle_to_names_and_values_dblib(handle, options = {})
|
443
433
|
query_options = {}.tap do |qo|
|
444
|
-
qo[:timezone] = ActiveRecord
|
434
|
+
qo[:timezone] = ActiveRecord.default_timezone || :utc
|
445
435
|
qo[:as] = (options[:ar_result] || options[:fetch] == :rows) ? :array : :hash
|
446
436
|
end
|
447
437
|
results = handle.each(query_options)
|
448
|
-
|
438
|
+
|
439
|
+
columns = handle.fields
|
440
|
+
# If query returns multiple result sets, only return the columns of the last one.
|
441
|
+
columns = columns.last if columns.any? && columns.all? { |e| e.is_a?(Array) }
|
442
|
+
columns = columns.map(&:downcase) if lowercase_schema_reflection
|
443
|
+
|
449
444
|
options[:ar_result] ? ActiveRecord::Result.new(columns, results) : results
|
450
445
|
end
|
451
446
|
|
452
447
|
def finish_statement_handle(handle)
|
453
|
-
|
454
|
-
when :dblib
|
455
|
-
handle.cancel if handle
|
456
|
-
end
|
448
|
+
handle.cancel if handle
|
457
449
|
handle
|
458
450
|
end
|
459
451
|
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
raise TinyTds::Error, "failed to execute statement" if
|
452
|
+
# TinyTDS returns false instead of raising an exception if connection fails.
|
453
|
+
# Getting around this by raising an exception ourselves while PR
|
454
|
+
# https://github.com/rails-sqlserver/tiny_tds/pull/469 is not released.
|
455
|
+
def internal_raw_execute(sql, conn, perform_do: false)
|
456
|
+
result = conn.execute(sql).tap do |_result|
|
457
|
+
raise TinyTds::Error, "failed to execute statement" if _result.is_a?(FalseClass)
|
466
458
|
end
|
467
|
-
end
|
468
459
|
|
469
|
-
|
470
|
-
raise TinyTds::Error, 'SQL Server client is not connected' unless @connection
|
471
|
-
|
472
|
-
yield
|
460
|
+
perform_do ? result.do : result
|
473
461
|
end
|
474
462
|
end
|
475
463
|
end
|
@@ -8,13 +8,13 @@ module ActiveRecord
|
|
8
8
|
name = SQLServer::Utils.extract_identifiers(database)
|
9
9
|
db_options = create_database_options(options)
|
10
10
|
edition_options = create_database_edition_options(options)
|
11
|
-
|
11
|
+
execute "CREATE DATABASE #{name} #{db_options} #{edition_options}"
|
12
12
|
end
|
13
13
|
|
14
14
|
def drop_database(database)
|
15
15
|
name = SQLServer::Utils.extract_identifiers(database)
|
16
|
-
|
17
|
-
|
16
|
+
execute "ALTER DATABASE #{name} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
|
17
|
+
execute "DROP DATABASE #{name}"
|
18
18
|
end
|
19
19
|
|
20
20
|
def current_database
|
@@ -33,7 +33,7 @@ module ActiveRecord
|
|
33
33
|
|
34
34
|
def create_database_options(options = {})
|
35
35
|
keys = [:collate]
|
36
|
-
copts = @
|
36
|
+
copts = @connection_parameters
|
37
37
|
options = {
|
38
38
|
collate: copts[:collation]
|
39
39
|
}.merge(options.symbolize_keys).select { |_, v|
|
@@ -46,7 +46,7 @@ module ActiveRecord
|
|
46
46
|
|
47
47
|
def create_database_edition_options(options = {})
|
48
48
|
keys = [:maxsize, :edition, :service_objective]
|
49
|
-
copts = @
|
49
|
+
copts = @connection_parameters
|
50
50
|
edition_options = {
|
51
51
|
maxsize: copts[:azure_maxsize],
|
52
52
|
edition: copts[:azure_edition],
|
@@ -4,18 +4,62 @@ module ActiveRecord
|
|
4
4
|
module ConnectionAdapters
|
5
5
|
module SQLServer
|
6
6
|
module Quoting
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
QUOTED_COLUMN_NAMES = Concurrent::Map.new # :nodoc:
|
10
|
+
QUOTED_TABLE_NAMES = Concurrent::Map.new # :nodoc:
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def column_name_matcher
|
14
|
+
/
|
15
|
+
\A
|
16
|
+
(
|
17
|
+
(?:
|
18
|
+
# [database_name].[database_owner].[table_name].[column_name] | function(one or no argument)
|
19
|
+
((?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+|\[\w+\]) | \w+\((?:|\g<2>)\))
|
20
|
+
)
|
21
|
+
(?:\s+AS\s+(?:\w+|\[\w+\]))?
|
22
|
+
)
|
23
|
+
(?:\s*,\s*\g<1>)*
|
24
|
+
\z
|
25
|
+
/ix
|
26
|
+
end
|
27
|
+
|
28
|
+
def column_name_with_order_matcher
|
29
|
+
/
|
30
|
+
\A
|
31
|
+
(
|
32
|
+
(?:
|
33
|
+
# [database_name].[database_owner].[table_name].[column_name] | function(one or no argument)
|
34
|
+
((?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+|\[\w+\]) | \w+\((?:|\g<2>)\))
|
35
|
+
)
|
36
|
+
(?:\s+COLLATE\s+\w+)?
|
37
|
+
(?:\s+ASC|\s+DESC)?
|
38
|
+
(?:\s+NULLS\s+(?:FIRST|LAST))?
|
39
|
+
)
|
40
|
+
(?:\s*,\s*\g<1>)*
|
41
|
+
\z
|
42
|
+
/ix
|
43
|
+
end
|
44
|
+
|
45
|
+
def quote_column_name(name)
|
46
|
+
QUOTED_COLUMN_NAMES[name] ||= SQLServer::Utils.extract_identifiers(name).quoted
|
47
|
+
end
|
48
|
+
|
49
|
+
def quote_table_name(name)
|
50
|
+
QUOTED_TABLE_NAMES[name] ||= SQLServer::Utils.extract_identifiers(name).quoted
|
51
|
+
end
|
52
|
+
end
|
10
53
|
|
11
54
|
def fetch_type_metadata(sql_type, sqlserver_options = {})
|
12
55
|
cast_type = lookup_cast_type(sql_type)
|
56
|
+
|
13
57
|
simple_type = SqlTypeMetadata.new(
|
14
|
-
sql_type:
|
15
|
-
type:
|
16
|
-
limit:
|
58
|
+
sql_type: sql_type,
|
59
|
+
type: cast_type.type,
|
60
|
+
limit: cast_type.limit,
|
17
61
|
precision: cast_type.precision,
|
18
|
-
scale:
|
62
|
+
scale: cast_type.scale
|
19
63
|
)
|
20
64
|
|
21
65
|
SQLServer::TypeMetadata.new(simple_type, **sqlserver_options)
|
@@ -33,13 +77,9 @@ module ActiveRecord
|
|
33
77
|
SQLServer::Utils.quote_string_single_national(s)
|
34
78
|
end
|
35
79
|
|
36
|
-
def quote_column_name(name)
|
37
|
-
SQLServer::Utils.extract_identifiers(name).quoted
|
38
|
-
end
|
39
|
-
|
40
80
|
def quote_default_expression(value, column)
|
41
81
|
cast_type = lookup_cast_type(column.sql_type)
|
42
|
-
if cast_type.type == :uuid && value
|
82
|
+
if cast_type.type == :uuid && value.is_a?(String) && value.include?('()')
|
43
83
|
value
|
44
84
|
else
|
45
85
|
super
|
@@ -47,7 +87,7 @@ module ActiveRecord
|
|
47
87
|
end
|
48
88
|
|
49
89
|
def quoted_true
|
50
|
-
|
90
|
+
'1'
|
51
91
|
end
|
52
92
|
|
53
93
|
def unquoted_true
|
@@ -55,7 +95,7 @@ module ActiveRecord
|
|
55
95
|
end
|
56
96
|
|
57
97
|
def quoted_false
|
58
|
-
|
98
|
+
'0'
|
59
99
|
end
|
60
100
|
|
61
101
|
def unquoted_false
|
@@ -72,59 +112,20 @@ module ActiveRecord
|
|
72
112
|
end
|
73
113
|
end
|
74
114
|
|
75
|
-
def
|
76
|
-
COLUMN_NAME
|
77
|
-
end
|
78
|
-
|
79
|
-
def column_name_with_order_matcher
|
80
|
-
COLUMN_NAME_WITH_ORDER
|
81
|
-
end
|
82
|
-
|
83
|
-
COLUMN_NAME = /
|
84
|
-
\A
|
85
|
-
(
|
86
|
-
(?:
|
87
|
-
# [database_name].[database_owner].[table_name].[column_name] | function(one or no argument)
|
88
|
-
((?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+|\[\w+\])) | \w+\((?:|\g<2>)\)
|
89
|
-
)
|
90
|
-
(?:\s+AS\s+(?:\w+|\[\w+\]))?
|
91
|
-
)
|
92
|
-
(?:\s*,\s*\g<1>)*
|
93
|
-
\z
|
94
|
-
/ix
|
95
|
-
|
96
|
-
COLUMN_NAME_WITH_ORDER = /
|
97
|
-
\A
|
98
|
-
(
|
99
|
-
(?:
|
100
|
-
# [database_name].[database_owner].[table_name].[column_name] | function(one or no argument)
|
101
|
-
((?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+\.|\[\w+\]\.)?(?:\w+|\[\w+\])) | \w+\((?:|\g<2>)\)
|
102
|
-
)
|
103
|
-
(?:\s+ASC|\s+DESC)?
|
104
|
-
(?:\s+NULLS\s+(?:FIRST|LAST))?
|
105
|
-
)
|
106
|
-
(?:\s*,\s*\g<1>)*
|
107
|
-
\z
|
108
|
-
/ix
|
109
|
-
|
110
|
-
private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
|
111
|
-
|
112
|
-
private
|
113
|
-
|
114
|
-
def _quote(value)
|
115
|
+
def quote(value)
|
115
116
|
case value
|
116
117
|
when Type::Binary::Data
|
117
118
|
"0x#{value.hex}"
|
118
119
|
when ActiveRecord::Type::SQLServer::Data
|
119
120
|
value.quoted
|
120
121
|
when String, ActiveSupport::Multibyte::Chars
|
121
|
-
"#{
|
122
|
+
"N#{super}"
|
122
123
|
else
|
123
124
|
super
|
124
125
|
end
|
125
126
|
end
|
126
127
|
|
127
|
-
def
|
128
|
+
def type_cast(value)
|
128
129
|
case value
|
129
130
|
when ActiveRecord::Type::SQLServer::Data
|
130
131
|
value.to_s
|