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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +30 -0
  3. data/.devcontainer/boot.sh +22 -0
  4. data/.devcontainer/devcontainer.json +38 -0
  5. data/.devcontainer/docker-compose.yml +42 -0
  6. data/.github/workflows/ci.yml +7 -4
  7. data/.gitignore +3 -1
  8. data/CHANGELOG.md +19 -42
  9. data/Dockerfile.ci +3 -3
  10. data/Gemfile +6 -1
  11. data/MIT-LICENSE +1 -1
  12. data/README.md +113 -27
  13. data/RUNNING_UNIT_TESTS.md +27 -14
  14. data/Rakefile +2 -6
  15. data/VERSION +1 -1
  16. data/activerecord-sqlserver-adapter.gemspec +3 -3
  17. data/appveyor.yml +4 -6
  18. data/docker-compose.ci.yml +2 -1
  19. data/lib/active_record/connection_adapters/sqlserver/core_ext/abstract_adapter.rb +20 -0
  20. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +6 -4
  21. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +5 -23
  22. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +10 -7
  23. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +2 -0
  24. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +12 -2
  25. data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +24 -16
  26. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +0 -31
  27. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +143 -155
  28. data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +5 -5
  29. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +57 -56
  30. data/lib/active_record/connection_adapters/sqlserver/savepoints.rb +26 -0
  31. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +14 -12
  32. data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +11 -0
  33. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +213 -57
  34. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +3 -3
  35. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +13 -2
  36. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +4 -6
  37. data/lib/active_record/connection_adapters/sqlserver/type/data.rb +19 -1
  38. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +1 -1
  39. data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +1 -1
  40. data/lib/active_record/connection_adapters/sqlserver/type/time.rb +1 -1
  41. data/lib/active_record/connection_adapters/sqlserver/utils.rb +21 -10
  42. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +187 -187
  43. data/lib/active_record/connection_adapters/sqlserver_column.rb +1 -0
  44. data/lib/active_record/tasks/sqlserver_database_tasks.rb +42 -33
  45. data/lib/arel/visitors/sqlserver.rb +77 -34
  46. data/test/cases/active_schema_test_sqlserver.rb +127 -0
  47. data/test/cases/adapter_test_sqlserver.rb +114 -26
  48. data/test/cases/coerced_tests.rb +1121 -340
  49. data/test/cases/column_test_sqlserver.rb +67 -64
  50. data/test/cases/connection_test_sqlserver.rb +3 -6
  51. data/test/cases/dbconsole.rb +19 -0
  52. data/test/cases/disconnected_test_sqlserver.rb +8 -5
  53. data/test/cases/eager_load_too_many_ids_test_sqlserver.rb +18 -0
  54. data/test/cases/enum_test_sqlserver.rb +49 -0
  55. data/test/cases/execute_procedure_test_sqlserver.rb +9 -5
  56. data/test/cases/fetch_test_sqlserver.rb +19 -0
  57. data/test/cases/helper_sqlserver.rb +11 -5
  58. data/test/cases/index_test_sqlserver.rb +8 -6
  59. data/test/cases/json_test_sqlserver.rb +1 -1
  60. data/test/cases/lateral_test_sqlserver.rb +2 -2
  61. data/test/cases/migration_test_sqlserver.rb +19 -1
  62. data/test/cases/optimizer_hints_test_sqlserver.rb +21 -12
  63. data/test/cases/pessimistic_locking_test_sqlserver.rb +8 -7
  64. data/test/cases/primary_keys_test_sqlserver.rb +2 -2
  65. data/test/cases/rake_test_sqlserver.rb +10 -5
  66. data/test/cases/schema_dumper_test_sqlserver.rb +155 -109
  67. data/test/cases/schema_test_sqlserver.rb +64 -1
  68. data/test/cases/showplan_test_sqlserver.rb +7 -7
  69. data/test/cases/specific_schema_test_sqlserver.rb +17 -13
  70. data/test/cases/transaction_test_sqlserver.rb +13 -8
  71. data/test/cases/trigger_test_sqlserver.rb +20 -0
  72. data/test/cases/utils_test_sqlserver.rb +2 -2
  73. data/test/cases/uuid_test_sqlserver.rb +8 -0
  74. data/test/cases/view_test_sqlserver.rb +58 -0
  75. data/test/config.yml +1 -2
  76. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +1 -1
  77. data/test/models/sqlserver/alien.rb +5 -0
  78. data/test/models/sqlserver/table_with_spaces.rb +5 -0
  79. data/test/models/sqlserver/trigger.rb +8 -0
  80. data/test/schema/sqlserver_specific_schema.rb +54 -6
  81. data/test/support/coerceable_test_sqlserver.rb +4 -4
  82. data/test/support/connection_reflection.rb +3 -9
  83. data/test/support/core_ext/query_cache.rb +7 -1
  84. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic.dump +0 -0
  85. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic_associations.dump +0 -0
  86. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic.dump +0 -0
  87. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic_associations.dump +0 -0
  88. data/test/support/query_assertions.rb +49 -0
  89. data/test/support/rake_helpers.rb +3 -1
  90. data/test/support/table_definition_sqlserver.rb +24 -0
  91. data/test/support/test_in_memory_oltp.rb +2 -2
  92. metadata +41 -17
  93. data/lib/active_record/sqlserver_base.rb +0 -18
  94. data/test/cases/scratchpad_test_sqlserver.rb +0 -8
  95. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic.dump +0 -0
  96. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_0_topic_associations.dump +0 -0
  97. 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 execute(sql, name = nil)
15
- if preventing_writes? && write_query?(sql)
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
- materialize_transactions
34
+ check_if_write_query(sql)
20
35
  mark_transaction_written_if_write(sql)
21
36
 
22
- if id_insert_table_name = query_requires_identity_insert?(sql)
23
- with_identity_insert_enabled(id_insert_table_name) { do_execute(sql, name) }
24
- else
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
- def exec_query(sql, name = "SQL", binds = [], prepare: false)
30
- if preventing_writes? && write_query?(sql)
31
- raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
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 exec_insert(sql, name = nil, binds = [], pk = nil, _sequence_name = nil)
41
- if id_insert_table_name = exec_insert_requires_identity?(sql, pk, binds)
42
- with_identity_insert_enabled(id_insert_table_name) { super(sql, name, binds, pk) }
43
- else
44
- super(sql, name, binds, pk)
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
- do_execute "BEGIN TRANSACTION", "TRANSACTION"
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 transaction_isolation_levels.fetch(isolation)
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
- do_execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}", "TRANSACTION"
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
- do_execute "COMMIT TRANSACTION", "TRANSACTION"
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
- do_execute "IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION", "TRANSACTION"
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
- sql << " OUTPUT " << returning.map { |column| "INSERTED.#{quote_column_name(column)}" }.join(", ")
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
- name = "Execute Procedure"
167
- log(sql, name) do
168
- case @connection_options[:mode]
169
- when :dblib
170
- result = ensure_established_connection! { dblib_execute(sql) }
171
- options = { as: :hash, cache_rows: true, timezone: ActiveRecord::Base.default_timezone || :utc }
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
- result.each.map { |row| row.is_a?(Hash) ? row.with_indifferent_access : row }
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 || @connection_options[:database]).quoted
193
- do_execute "USE #{name}" unless name.blank?
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
- id_sql_type = exclude_output_inserted.is_a?(TrueClass) ? "bigint" : exclude_output_inserted
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 (#{quoted_pk} #{id_sql_type});
272
- #{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk} INTO @ssaIdInsertTable"}
273
- SELECT CAST(#{quoted_pk} AS #{id_sql_type}) FROM @ssaIdInsertTable
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
- sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk}"
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
- super
308
+
309
+ [sql, binds]
282
310
  end
283
311
 
284
312
  # === SQLServer Specific ======================================== #
285
313
 
286
- def set_identity_insert(table_name, enable = true)
287
- do_execute "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
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
- return attr.type.sqlserver_type if attr.type.respond_to?(:sqlserver_type)
334
+ if attr.respond_to?(:type)
335
+ return attr.type.sqlserver_type if attr.type.respond_to?(:sqlserver_type)
323
336
 
324
- case value = attr.value_for_database
325
- when Numeric
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 exec_insert_requires_identity?(sql, pk, binds)
387
- query_requires_identity_insert?(sql)
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
- if insert_sql?(sql)
392
- table_name = get_table_name(sql)
393
- id_column = identity_columns(table_name).first
394
- # id_column && sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)[^(]+\([^)]*\b(#{id_column.name})\b,?[^)]*\)/i ? quote_table_name(table_name) : false
395
- id_column && sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)[^(]+\([^)]*\b(#{id_column.name})\b,?[^)]*\)/i ? table_name : false
396
- else
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 =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)/i).nil?
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 raw_select(sql, name = "SQL", binds = [], options = {})
412
- log(sql, name, binds) { _raw_select(sql, options) }
413
- end
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::Base.default_timezone || :utc
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
- columns = lowercase_schema_reflection ? handle.fields.map { |c| c.downcase } : handle.fields
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
- case @connection_options[:mode]
454
- when :dblib
455
- handle.cancel if handle
456
- end
448
+ handle.cancel if handle
457
449
  handle
458
450
  end
459
451
 
460
- def dblib_execute(sql)
461
- @connection.execute(sql).tap do |result|
462
- # TinyTDS returns false instead of raising an exception if connection fails.
463
- # Getting around this by raising an exception ourselves while this PR
464
- # https://github.com/rails-sqlserver/tiny_tds/pull/469 is not released.
465
- raise TinyTds::Error, "failed to execute statement" if result.is_a?(FalseClass)
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
- def ensure_established_connection!
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
- do_execute "CREATE DATABASE #{name} #{db_options} #{edition_options}"
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
- do_execute "ALTER DATABASE #{name} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
17
- do_execute "DROP DATABASE #{name}"
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 = @connection_options
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 = @connection_options
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
- QUOTED_TRUE = "1".freeze
8
- QUOTED_FALSE = "0".freeze
9
- QUOTED_STRING_PREFIX = "N".freeze
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: sql_type,
15
- type: cast_type.type,
16
- limit: cast_type.limit,
58
+ sql_type: sql_type,
59
+ type: cast_type.type,
60
+ limit: cast_type.limit,
17
61
  precision: cast_type.precision,
18
- scale: cast_type.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
- QUOTED_TRUE
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
- QUOTED_FALSE
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 column_name_matcher
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
- "#{QUOTED_STRING_PREFIX}#{super}"
122
+ "N#{super}"
122
123
  else
123
124
  super
124
125
  end
125
126
  end
126
127
 
127
- def _type_cast(value)
128
+ def type_cast(value)
128
129
  case value
129
130
  when ActiveRecord::Type::SQLServer::Data
130
131
  value.to_s