activerecord-sqlserver-adapter 5.2.1 → 7.0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +9 -0
  3. data/.github/issue_template.md +23 -0
  4. data/.github/workflows/ci.yml +29 -0
  5. data/.gitignore +1 -0
  6. data/.rubocop.yml +29 -0
  7. data/CHANGELOG.md +17 -27
  8. data/{Dockerfile → Dockerfile.ci} +1 -1
  9. data/Gemfile +49 -41
  10. data/Guardfile +9 -8
  11. data/MIT-LICENSE +1 -1
  12. data/README.md +65 -42
  13. data/RUNNING_UNIT_TESTS.md +3 -0
  14. data/Rakefile +14 -16
  15. data/VERSION +1 -1
  16. data/activerecord-sqlserver-adapter.gemspec +25 -14
  17. data/appveyor.yml +22 -17
  18. data/docker-compose.ci.yml +7 -5
  19. data/guides/RELEASING.md +11 -0
  20. data/lib/active_record/connection_adapters/sqlserver/core_ext/active_record.rb +2 -4
  21. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +5 -4
  22. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +10 -14
  23. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +12 -5
  24. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +2 -0
  25. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +10 -7
  26. data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +30 -0
  27. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +9 -4
  28. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +117 -52
  29. data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +9 -12
  30. data/lib/active_record/connection_adapters/sqlserver/errors.rb +2 -3
  31. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +51 -14
  32. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +40 -6
  33. data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +18 -10
  34. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +235 -167
  35. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_table.rb +4 -2
  36. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_xml.rb +3 -1
  37. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +8 -8
  38. data/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb +36 -7
  39. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +43 -45
  40. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +8 -10
  41. data/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb +3 -3
  42. data/lib/active_record/connection_adapters/sqlserver/type/binary.rb +5 -4
  43. data/lib/active_record/connection_adapters/sqlserver/type/boolean.rb +3 -3
  44. data/lib/active_record/connection_adapters/sqlserver/type/char.rb +7 -4
  45. data/lib/active_record/connection_adapters/sqlserver/type/data.rb +5 -3
  46. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +7 -5
  47. data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +8 -8
  48. data/lib/active_record/connection_adapters/sqlserver/type/datetime2.rb +2 -2
  49. data/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb +2 -2
  50. data/lib/active_record/connection_adapters/sqlserver/type/decimal.rb +5 -4
  51. data/lib/active_record/connection_adapters/sqlserver/type/decimal_without_scale.rb +22 -0
  52. data/lib/active_record/connection_adapters/sqlserver/type/float.rb +3 -3
  53. data/lib/active_record/connection_adapters/sqlserver/type/integer.rb +3 -3
  54. data/lib/active_record/connection_adapters/sqlserver/type/json.rb +2 -1
  55. data/lib/active_record/connection_adapters/sqlserver/type/money.rb +4 -4
  56. data/lib/active_record/connection_adapters/sqlserver/type/real.rb +3 -3
  57. data/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb +3 -3
  58. data/lib/active_record/connection_adapters/sqlserver/type/small_money.rb +4 -4
  59. data/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb +3 -3
  60. data/lib/active_record/connection_adapters/sqlserver/type/string.rb +2 -2
  61. data/lib/active_record/connection_adapters/sqlserver/type/text.rb +3 -3
  62. data/lib/active_record/connection_adapters/sqlserver/type/time.rb +6 -6
  63. data/lib/active_record/connection_adapters/sqlserver/type/time_value_fractional.rb +8 -9
  64. data/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb +3 -3
  65. data/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb +3 -3
  66. data/lib/active_record/connection_adapters/sqlserver/type/unicode_char.rb +5 -4
  67. data/lib/active_record/connection_adapters/sqlserver/type/unicode_string.rb +2 -2
  68. data/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb +3 -3
  69. data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar.rb +6 -5
  70. data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb +4 -4
  71. data/lib/active_record/connection_adapters/sqlserver/type/uuid.rb +4 -3
  72. data/lib/active_record/connection_adapters/sqlserver/type/varbinary.rb +6 -5
  73. data/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb +4 -4
  74. data/lib/active_record/connection_adapters/sqlserver/type/varchar.rb +6 -5
  75. data/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb +4 -4
  76. data/lib/active_record/connection_adapters/sqlserver/type.rb +38 -35
  77. data/lib/active_record/connection_adapters/sqlserver/utils.rb +26 -12
  78. data/lib/active_record/connection_adapters/sqlserver/version.rb +2 -2
  79. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +271 -180
  80. data/lib/active_record/connection_adapters/sqlserver_column.rb +76 -16
  81. data/lib/active_record/sqlserver_base.rb +11 -9
  82. data/lib/active_record/tasks/sqlserver_database_tasks.rb +38 -39
  83. data/lib/activerecord-sqlserver-adapter.rb +3 -1
  84. data/lib/arel/visitors/sqlserver.rb +177 -56
  85. data/lib/arel_sqlserver.rb +4 -2
  86. data/test/appveyor/dbsetup.ps1 +4 -4
  87. data/test/cases/active_schema_test_sqlserver.rb +55 -0
  88. data/test/cases/adapter_test_sqlserver.rb +258 -173
  89. data/test/cases/change_column_collation_test_sqlserver.rb +33 -0
  90. data/test/cases/change_column_null_test_sqlserver.rb +14 -12
  91. data/test/cases/coerced_tests.rb +1421 -397
  92. data/test/cases/column_test_sqlserver.rb +321 -315
  93. data/test/cases/connection_test_sqlserver.rb +17 -20
  94. data/test/cases/disconnected_test_sqlserver.rb +39 -0
  95. data/test/cases/eager_load_too_many_ids_test_sqlserver.rb +18 -0
  96. data/test/cases/execute_procedure_test_sqlserver.rb +28 -19
  97. data/test/cases/fetch_test_sqlserver.rb +33 -21
  98. data/test/cases/fully_qualified_identifier_test_sqlserver.rb +15 -19
  99. data/test/cases/helper_sqlserver.rb +15 -15
  100. data/test/cases/in_clause_test_sqlserver.rb +63 -0
  101. data/test/cases/index_test_sqlserver.rb +15 -15
  102. data/test/cases/json_test_sqlserver.rb +25 -25
  103. data/test/cases/lateral_test_sqlserver.rb +35 -0
  104. data/test/cases/migration_test_sqlserver.rb +74 -27
  105. data/test/cases/optimizer_hints_test_sqlserver.rb +72 -0
  106. data/test/cases/order_test_sqlserver.rb +59 -53
  107. data/test/cases/pessimistic_locking_test_sqlserver.rb +27 -33
  108. data/test/cases/primary_keys_test_sqlserver.rb +103 -0
  109. data/test/cases/rake_test_sqlserver.rb +70 -45
  110. data/test/cases/schema_dumper_test_sqlserver.rb +124 -109
  111. data/test/cases/schema_test_sqlserver.rb +20 -26
  112. data/test/cases/scratchpad_test_sqlserver.rb +4 -4
  113. data/test/cases/showplan_test_sqlserver.rb +28 -35
  114. data/test/cases/specific_schema_test_sqlserver.rb +68 -65
  115. data/test/cases/transaction_test_sqlserver.rb +18 -20
  116. data/test/cases/trigger_test_sqlserver.rb +14 -13
  117. data/test/cases/utils_test_sqlserver.rb +70 -70
  118. data/test/cases/uuid_test_sqlserver.rb +13 -14
  119. data/test/debug.rb +8 -6
  120. data/test/migrations/create_clients_and_change_column_collation.rb +19 -0
  121. data/test/migrations/create_clients_and_change_column_null.rb +3 -1
  122. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +4 -4
  123. data/test/models/sqlserver/booking.rb +3 -1
  124. data/test/models/sqlserver/composite_pk.rb +9 -0
  125. data/test/models/sqlserver/customers_view.rb +3 -1
  126. data/test/models/sqlserver/datatype.rb +2 -0
  127. data/test/models/sqlserver/datatype_migration.rb +2 -0
  128. data/test/models/sqlserver/dollar_table_name.rb +3 -1
  129. data/test/models/sqlserver/edge_schema.rb +3 -3
  130. data/test/models/sqlserver/fk_has_fk.rb +3 -1
  131. data/test/models/sqlserver/fk_has_pk.rb +3 -1
  132. data/test/models/sqlserver/natural_pk_data.rb +4 -2
  133. data/test/models/sqlserver/natural_pk_int_data.rb +3 -1
  134. data/test/models/sqlserver/no_pk_data.rb +3 -1
  135. data/test/models/sqlserver/object_default.rb +3 -1
  136. data/test/models/sqlserver/quoted_table.rb +4 -2
  137. data/test/models/sqlserver/quoted_view_1.rb +3 -1
  138. data/test/models/sqlserver/quoted_view_2.rb +3 -1
  139. data/test/models/sqlserver/sst_memory.rb +3 -1
  140. data/test/models/sqlserver/sst_string_collation.rb +3 -0
  141. data/test/models/sqlserver/string_default.rb +3 -1
  142. data/test/models/sqlserver/string_defaults_big_view.rb +3 -1
  143. data/test/models/sqlserver/string_defaults_view.rb +3 -1
  144. data/test/models/sqlserver/tinyint_pk.rb +3 -1
  145. data/test/models/sqlserver/trigger.rb +4 -2
  146. data/test/models/sqlserver/trigger_history.rb +3 -1
  147. data/test/models/sqlserver/upper.rb +3 -1
  148. data/test/models/sqlserver/uppered.rb +3 -1
  149. data/test/models/sqlserver/uuid.rb +3 -1
  150. data/test/schema/sqlserver_specific_schema.rb +56 -21
  151. data/test/support/coerceable_test_sqlserver.rb +19 -13
  152. data/test/support/connection_reflection.rb +3 -2
  153. data/test/support/core_ext/query_cache.rb +4 -1
  154. data/test/support/load_schema_sqlserver.rb +5 -5
  155. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic.dump +0 -0
  156. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic_associations.dump +0 -0
  157. data/test/support/minitest_sqlserver.rb +3 -1
  158. data/test/support/paths_sqlserver.rb +11 -11
  159. data/test/support/rake_helpers.rb +15 -10
  160. data/test/support/sql_counter_sqlserver.rb +16 -15
  161. data/test/support/test_in_memory_oltp.rb +9 -7
  162. metadata +47 -13
  163. data/.travis.yml +0 -25
  164. data/lib/active_record/connection_adapters/sqlserver/core_ext/query_methods.rb +0 -26
@@ -1,9 +1,27 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module ConnectionAdapters
3
5
  module SQLServer
4
6
  module DatabaseStatements
7
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(:begin, :commit, :dbcc, :explain, :save, :select, :set, :rollback, :waitfor) # :nodoc:
8
+ private_constant :READ_QUERY
9
+
10
+ def write_query?(sql) # :nodoc:
11
+ !READ_QUERY.match?(sql)
12
+ rescue ArgumentError # Invalid encoding
13
+ !READ_QUERY.match?(sql.b)
14
+ end
5
15
 
6
16
  def execute(sql, name = nil)
17
+ sql = transform_query(sql)
18
+ if preventing_writes? && write_query?(sql)
19
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
20
+ end
21
+
22
+ materialize_transactions
23
+ mark_transaction_written_if_write(sql)
24
+
7
25
  if id_insert_table_name = query_requires_identity_insert?(sql)
8
26
  with_identity_insert_enabled(id_insert_table_name) { do_execute(sql, name) }
9
27
  else
@@ -11,8 +29,16 @@ module ActiveRecord
11
29
  end
12
30
  end
13
31
 
14
- def exec_query(sql, name = 'SQL', binds = [], prepare: false)
15
- sp_executesql(sql, name, binds, prepare: prepare)
32
+ def exec_query(sql, name = "SQL", binds = [], prepare: false, async: false)
33
+ sql = transform_query(sql)
34
+ if preventing_writes? && write_query?(sql)
35
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
36
+ end
37
+
38
+ materialize_transactions
39
+ mark_transaction_written_if_write(sql)
40
+
41
+ sp_executesql(sql, name, binds, prepare: prepare, async: async)
16
42
  end
17
43
 
18
44
  def exec_insert(sql, name = nil, binds = [], pk = nil, _sequence_name = nil)
@@ -24,17 +50,17 @@ module ActiveRecord
24
50
  end
25
51
 
26
52
  def exec_delete(sql, name, binds)
27
- sql = sql.dup << '; SELECT @@ROWCOUNT AS AffectedRows'
53
+ sql = sql.dup << "; SELECT @@ROWCOUNT AS AffectedRows"
28
54
  super(sql, name, binds).rows.first.first
29
55
  end
30
56
 
31
57
  def exec_update(sql, name, binds)
32
- sql = sql.dup << '; SELECT @@ROWCOUNT AS AffectedRows'
58
+ sql = sql.dup << "; SELECT @@ROWCOUNT AS AffectedRows"
33
59
  super(sql, name, binds).rows.first.first
34
60
  end
35
61
 
36
62
  def begin_db_transaction
37
- do_execute 'BEGIN TRANSACTION'
63
+ do_execute "BEGIN TRANSACTION", "TRANSACTION"
38
64
  end
39
65
 
40
66
  def transaction_isolation_levels
@@ -47,33 +73,35 @@ module ActiveRecord
47
73
  end
48
74
 
49
75
  def set_transaction_isolation_level(isolation_level)
50
- do_execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}"
76
+ do_execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}", "TRANSACTION"
51
77
  end
52
78
 
53
79
  def commit_db_transaction
54
- do_execute 'COMMIT TRANSACTION'
80
+ do_execute "COMMIT TRANSACTION", "TRANSACTION"
55
81
  end
56
82
 
57
83
  def exec_rollback_db_transaction
58
- do_execute 'IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION'
84
+ do_execute "IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION", "TRANSACTION"
59
85
  end
60
86
 
61
87
  include Savepoints
62
88
 
63
89
  def create_savepoint(name = current_savepoint_name)
64
- do_execute "SAVE TRANSACTION #{name}"
90
+ do_execute "SAVE TRANSACTION #{name}", "TRANSACTION"
65
91
  end
66
92
 
67
93
  def exec_rollback_to_savepoint(name = current_savepoint_name)
68
- do_execute "ROLLBACK TRANSACTION #{name}"
94
+ do_execute "ROLLBACK TRANSACTION #{name}", "TRANSACTION"
69
95
  end
70
96
 
71
97
  def release_savepoint(name = current_savepoint_name)
72
98
  end
73
99
 
74
- def case_sensitive_comparison(table, attribute, column, value)
100
+ def case_sensitive_comparison(attribute, value)
101
+ column = column_for_attribute(attribute)
102
+
75
103
  if column.collation && !column.case_sensitive?
76
- table[attribute].eq(Arel::Nodes::Bin.new(value))
104
+ attribute.eq(Arel::Nodes::Bin.new(value))
77
105
  else
78
106
  super
79
107
  end
@@ -89,12 +117,12 @@ module ActiveRecord
89
117
  end
90
118
  end
91
119
 
92
- table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}".dup }
93
- total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts))
120
+ table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}" }
121
+ total_sqls = Array.wrap(table_deletes + fixture_inserts)
94
122
 
95
123
  disable_referential_integrity do
96
124
  transaction(requires_new: true) do
97
- total_sql.each do |sql|
125
+ total_sqls.each do |sql|
98
126
  execute sql, "Fixtures Load"
99
127
  yield if block_given?
100
128
  end
@@ -107,11 +135,6 @@ module ActiveRecord
107
135
  end
108
136
  private :can_perform_case_insensitive_comparison_for?
109
137
 
110
- def combine_multi_statements(total_sql)
111
- total_sql
112
- end
113
- private :combine_multi_statements
114
-
115
138
  def default_insert_value(column)
116
139
  if column.is_identity?
117
140
  table_name = quote(quote_table_name(column.table_name))
@@ -122,21 +145,39 @@ module ActiveRecord
122
145
  end
123
146
  private :default_insert_value
124
147
 
148
+ def build_insert_sql(insert) # :nodoc:
149
+ sql = +"INSERT #{insert.into}"
150
+
151
+ if returning = insert.send(:insert_all).returning
152
+ returning_sql = if returning.is_a?(String)
153
+ returning
154
+ else
155
+ returning.map { |column| "INSERTED.#{quote_column_name(column)}" }.join(", ")
156
+ end
157
+ sql << " OUTPUT #{returning_sql}"
158
+ end
159
+
160
+ sql << " #{insert.values_list}"
161
+ sql
162
+ end
163
+
125
164
  # === SQLServer Specific ======================================== #
126
165
 
127
166
  def execute_procedure(proc_name, *variables)
167
+ materialize_transactions
168
+
128
169
  vars = if variables.any? && variables.first.is_a?(Hash)
129
170
  variables.first.map { |k, v| "@#{k} = #{quote(v)}" }
130
171
  else
131
172
  variables.map { |v| quote(v) }
132
- end.join(', ')
173
+ end.join(", ")
133
174
  sql = "EXEC #{proc_name} #{vars}".strip
134
- name = 'Execute Procedure'
175
+ name = "Execute Procedure"
135
176
  log(sql, name) do
136
177
  case @connection_options[:mode]
137
178
  when :dblib
138
- result = @connection.execute(sql)
139
- options = { as: :hash, cache_rows: true, timezone: ActiveRecord::Base.default_timezone || :utc }
179
+ result = ensure_established_connection! { dblib_execute(sql) }
180
+ options = { as: :hash, cache_rows: true, timezone: ActiveRecord.default_timezone || :utc }
140
181
  result.each(options) do |row|
141
182
  r = row.with_indifferent_access
142
183
  yield(r) if block_given?
@@ -156,20 +197,22 @@ module ActiveRecord
156
197
 
157
198
  def use_database(database = nil)
158
199
  return if sqlserver_azure?
200
+
159
201
  name = SQLServer::Utils.extract_identifiers(database || @connection_options[:database]).quoted
160
202
  do_execute "USE #{name}" unless name.blank?
161
203
  end
162
204
 
163
205
  def user_options
164
206
  return {} if sqlserver_azure?
165
- rows = select_rows('DBCC USEROPTIONS WITH NO_INFOMSGS', 'SCHEMA')
207
+
208
+ rows = select_rows("DBCC USEROPTIONS WITH NO_INFOMSGS", "SCHEMA")
166
209
  rows = rows.first if rows.size == 2 && rows.last.empty?
167
210
  rows.reduce(HashWithIndifferentAccess.new) do |values, row|
168
211
  if row.instance_of? Hash
169
- set_option = row.values[0].gsub(/\s+/, '_')
212
+ set_option = row.values[0].gsub(/\s+/, "_")
170
213
  user_value = row.values[1]
171
- elsif row.instance_of? Array
172
- set_option = row[0].gsub(/\s+/, '_')
214
+ elsif row.instance_of? Array
215
+ set_option = row[0].gsub(/\s+/, "_")
173
216
  user_value = row[1]
174
217
  end
175
218
  values[set_option] = user_value
@@ -179,9 +222,9 @@ module ActiveRecord
179
222
 
180
223
  def user_options_dateformat
181
224
  if sqlserver_azure?
182
- select_value 'SELECT [dateformat] FROM [sys].[syslanguages] WHERE [langid] = @@LANGID', 'SCHEMA'
225
+ select_value "SELECT [dateformat] FROM [sys].[syslanguages] WHERE [langid] = @@LANGID", "SCHEMA"
183
226
  else
184
- user_options['dateformat']
227
+ user_options["dateformat"]
185
228
  end
186
229
  end
187
230
 
@@ -196,43 +239,44 @@ module ActiveRecord
196
239
  WHEN 5 THEN 'SNAPSHOT' END AS [isolation_level]
197
240
  FROM [sys].[dm_exec_sessions]
198
241
  WHERE [session_id] = @@SPID).squish
199
- select_value sql, 'SCHEMA'
242
+ select_value sql, "SCHEMA"
200
243
  else
201
- user_options['isolation_level']
244
+ user_options["isolation_level"]
202
245
  end
203
246
  end
204
247
 
205
248
  def user_options_language
206
249
  if sqlserver_azure?
207
- select_value 'SELECT @@LANGUAGE AS [language]', 'SCHEMA'
250
+ select_value "SELECT @@LANGUAGE AS [language]", "SCHEMA"
208
251
  else
209
- user_options['language']
252
+ user_options["language"]
210
253
  end
211
254
  end
212
255
 
213
256
  def newid_function
214
- select_value 'SELECT NEWID()'
257
+ select_value "SELECT NEWID()"
215
258
  end
216
259
 
217
260
  def newsequentialid_function
218
- select_value 'SELECT NEWSEQUENTIALID()'
261
+ select_value "SELECT NEWSEQUENTIALID()"
219
262
  end
220
263
 
221
-
222
264
  protected
223
265
 
224
- def sql_for_insert(sql, pk, id_value, sequence_name, binds)
266
+ def sql_for_insert(sql, pk, binds)
225
267
  if pk.nil?
226
268
  table_name = query_requires_identity_insert?(sql)
227
269
  pk = primary_key(table_name)
228
270
  end
271
+
229
272
  sql = if pk && use_output_inserted? && !database_prefix_remote_server?
230
273
  quoted_pk = SQLServer::Utils.extract_identifiers(pk).quoted
231
274
  table_name ||= get_table_name(sql)
232
275
  exclude_output_inserted = exclude_output_inserted_table_name?(table_name, sql)
276
+
233
277
  if exclude_output_inserted
234
- id_sql_type = exclude_output_inserted.is_a?(TrueClass) ? 'bigint' : exclude_output_inserted
235
- <<-SQL.strip_heredoc
278
+ id_sql_type = exclude_output_inserted.is_a?(TrueClass) ? "bigint" : exclude_output_inserted
279
+ <<~SQL.squish
236
280
  DECLARE @ssaIdInsertTable table (#{quoted_pk} #{id_sql_type});
237
281
  #{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk} INTO @ssaIdInsertTable"}
238
282
  SELECT CAST(#{quoted_pk} AS #{id_sql_type}) FROM @ssaIdInsertTable
@@ -256,7 +300,10 @@ module ActiveRecord
256
300
 
257
301
  # === SQLServer Specific (Executing) ============================ #
258
302
 
259
- def do_execute(sql, name = 'SQL')
303
+ def do_execute(sql, name = "SQL")
304
+ materialize_transactions
305
+ mark_transaction_written_if_write(sql)
306
+
260
307
  log(sql, name) { raw_connection_do(sql) }
261
308
  end
262
309
 
@@ -282,11 +329,12 @@ module ActiveRecord
282
329
 
283
330
  def sp_executesql_sql_type(attr)
284
331
  return attr.type.sqlserver_type if attr.type.respond_to?(:sqlserver_type)
332
+
285
333
  case value = attr.value_for_database
286
334
  when Numeric
287
- value > 2_147_483_647 ? 'bigint'.freeze : 'int'.freeze
335
+ value > 2_147_483_647 ? "bigint".freeze : "int".freeze
288
336
  else
289
- 'nvarchar(max)'.freeze
337
+ "nvarchar(max)".freeze
290
338
  end
291
339
  end
292
340
 
@@ -301,16 +349,16 @@ module ActiveRecord
301
349
  end
302
350
 
303
351
  def sp_executesql_sql(sql, types, params, name)
304
- if name == 'EXPLAIN'
352
+ if name == "EXPLAIN"
305
353
  params.each.with_index do |param, index|
306
354
  substitute_at_finder = /(@#{index})(?=(?:[^']|'[^']*')*$)/ # Finds unquoted @n values.
307
355
  sql = sql.sub substitute_at_finder, param.to_s
308
356
  end
309
357
  else
310
- types = quote(types.join(', '))
311
- params = params.map.with_index{ |p, i| "@#{i} = #{p}" }.join(', ') # Only p is needed, but with @i helps explain regexp.
358
+ types = quote(types.join(", "))
359
+ params = params.map.with_index { |p, i| "@#{i} = #{p}" }.join(", ") # Only p is needed, but with @i helps explain regexp.
312
360
  sql = "EXEC sp_executesql #{quote(sql)}"
313
- sql << ", #{types}, #{params}" unless params.empty?
361
+ sql += ", #{types}, #{params}" unless params.empty?
314
362
  end
315
363
  sql
316
364
  end
@@ -318,7 +366,8 @@ module ActiveRecord
318
366
  def raw_connection_do(sql)
319
367
  case @connection_options[:mode]
320
368
  when :dblib
321
- @connection.execute(sql).do
369
+ result = ensure_established_connection! { dblib_execute(sql) }
370
+ result.do
322
371
  end
323
372
  ensure
324
373
  @update_sql = false
@@ -336,8 +385,10 @@ module ActiveRecord
336
385
 
337
386
  def exclude_output_inserted_table_name?(table_name, sql)
338
387
  return false unless exclude_output_inserted_table_names?
388
+
339
389
  table_name ||= get_table_name(sql)
340
390
  return false unless table_name
391
+
341
392
  self.class.exclude_output_inserted_table_names[table_name]
342
393
  end
343
394
 
@@ -366,8 +417,8 @@ module ActiveRecord
366
417
 
367
418
  # === SQLServer Specific (Selecting) ============================ #
368
419
 
369
- def raw_select(sql, name = 'SQL', binds = [], options = {})
370
- log(sql, name, binds) { _raw_select(sql, options) }
420
+ def raw_select(sql, name = "SQL", binds = [], options = {})
421
+ log(sql, name, binds, async: options[:async]) { _raw_select(sql, options) }
371
422
  end
372
423
 
373
424
  def _raw_select(sql, options = {})
@@ -380,7 +431,7 @@ module ActiveRecord
380
431
  def raw_connection_run(sql)
381
432
  case @connection_options[:mode]
382
433
  when :dblib
383
- @connection.execute(sql)
434
+ ensure_established_connection! { dblib_execute(sql) }
384
435
  end
385
436
  end
386
437
 
@@ -399,7 +450,7 @@ module ActiveRecord
399
450
 
400
451
  def handle_to_names_and_values_dblib(handle, options = {})
401
452
  query_options = {}.tap do |qo|
402
- qo[:timezone] = ActiveRecord::Base.default_timezone || :utc
453
+ qo[:timezone] = ActiveRecord.default_timezone || :utc
403
454
  qo[:as] = (options[:ar_result] || options[:fetch] == :rows) ? :array : :hash
404
455
  end
405
456
  results = handle.each(query_options)
@@ -415,6 +466,20 @@ module ActiveRecord
415
466
  handle
416
467
  end
417
468
 
469
+ def dblib_execute(sql)
470
+ @connection.execute(sql).tap do |result|
471
+ # TinyTDS returns false instead of raising an exception if connection fails.
472
+ # Getting around this by raising an exception ourselves while this PR
473
+ # https://github.com/rails-sqlserver/tiny_tds/pull/469 is not released.
474
+ raise TinyTds::Error, "failed to execute statement" if result.is_a?(FalseClass)
475
+ end
476
+ end
477
+
478
+ def ensure_established_connection!
479
+ raise TinyTds::Error, 'SQL Server client is not connected' unless @connection
480
+
481
+ yield
482
+ end
418
483
  end
419
484
  end
420
485
  end
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module ConnectionAdapters
3
5
  module SQLServer
4
6
  module DatabaseTasks
5
-
6
7
  def create_database(database, options = {})
7
8
  name = SQLServer::Utils.extract_identifiers(database)
8
9
  db_options = create_database_options(options)
@@ -17,7 +18,7 @@ module ActiveRecord
17
18
  end
18
19
 
19
20
  def current_database
20
- select_value 'SELECT DB_NAME()'
21
+ select_value "SELECT DB_NAME()"
21
22
  end
22
23
 
23
24
  def charset
@@ -30,20 +31,20 @@ module ActiveRecord
30
31
 
31
32
  private
32
33
 
33
- def create_database_options(options={})
34
+ def create_database_options(options = {})
34
35
  keys = [:collate]
35
36
  copts = @connection_options
36
37
  options = {
37
38
  collate: copts[:collation]
38
39
  }.merge(options.symbolize_keys).select { |_, v|
39
40
  v.present?
40
- }.slice(*keys).map { |k,v|
41
+ }.slice(*keys).map { |k, v|
41
42
  "#{k.to_s.upcase} #{v}"
42
- }.join(' ')
43
+ }.join(" ")
43
44
  options
44
45
  end
45
46
 
46
- def create_database_edition_options(options={})
47
+ def create_database_edition_options(options = {})
47
48
  keys = [:maxsize, :edition, :service_objective]
48
49
  copts = @connection_options
49
50
  edition_options = {
@@ -52,17 +53,13 @@ module ActiveRecord
52
53
  service_objective: copts[:azure_service_objective]
53
54
  }.merge(options.symbolize_keys).select { |_, v|
54
55
  v.present?
55
- }.slice(*keys).map { |k,v|
56
+ }.slice(*keys).map { |k, v|
56
57
  "#{k.to_s.upcase} = #{v}"
57
- }.join(', ')
58
+ }.join(", ")
58
59
  edition_options = "( #{edition_options} )" if edition_options.present?
59
60
  edition_options
60
61
  end
61
-
62
62
  end
63
63
  end
64
64
  end
65
65
  end
66
-
67
-
68
-
@@ -1,7 +1,6 @@
1
- module ActiveRecord
1
+ # frozen_string_literal: true
2
2
 
3
+ module ActiveRecord
3
4
  class DeadlockVictim < WrappedDatabaseException
4
5
  end
5
-
6
-
7
6
  end
@@ -1,22 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module ConnectionAdapters
3
5
  module SQLServer
4
6
  module Quoting
5
-
6
- QUOTED_TRUE = '1'.freeze
7
- QUOTED_FALSE = '0'.freeze
8
- QUOTED_STRING_PREFIX = 'N'.freeze
7
+ QUOTED_TRUE = "1".freeze
8
+ QUOTED_FALSE = "0".freeze
9
+ QUOTED_STRING_PREFIX = "N".freeze
9
10
 
10
11
  def fetch_type_metadata(sql_type, sqlserver_options = {})
11
12
  cast_type = lookup_cast_type(sql_type)
12
- SQLServer::SqlTypeMetadata.new(
13
+ simple_type = SqlTypeMetadata.new(
13
14
  sql_type: sql_type,
14
15
  type: cast_type.type,
15
16
  limit: cast_type.limit,
16
17
  precision: cast_type.precision,
17
- scale: cast_type.scale,
18
- sqlserver_options: sqlserver_options
18
+ scale: cast_type.scale
19
19
  )
20
+
21
+ SQLServer::TypeMetadata.new(simple_type, **sqlserver_options)
20
22
  end
21
23
 
22
24
  def quote_string(s)
@@ -61,17 +63,53 @@ module ActiveRecord
61
63
  end
62
64
 
63
65
  def quoted_date(value)
64
- if value.acts_like?(:date)
65
- Type::Date.new.serialize(value)
66
- else value.acts_like?(:time)
66
+ if value.acts_like?(:time)
67
67
  Type::DateTime.new.serialize(value)
68
+ elsif value.acts_like?(:date)
69
+ Type::Date.new.serialize(value)
70
+ else
71
+ value
68
72
  end
69
73
  end
70
74
 
75
+ def column_name_matcher
76
+ COLUMN_NAME
77
+ end
71
78
 
72
- private
79
+ def column_name_with_order_matcher
80
+ COLUMN_NAME_WITH_ORDER
81
+ end
73
82
 
74
- def _quote(value)
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
+ def quote(value)
75
113
  case value
76
114
  when Type::Binary::Data
77
115
  "0x#{value.hex}"
@@ -84,7 +122,7 @@ module ActiveRecord
84
122
  end
85
123
  end
86
124
 
87
- def _type_cast(value)
125
+ def type_cast(value)
88
126
  case value
89
127
  when ActiveRecord::Type::SQLServer::Data
90
128
  value.to_s
@@ -92,7 +130,6 @@ module ActiveRecord
92
130
  super
93
131
  end
94
132
  end
95
-
96
133
  end
97
134
  end
98
135
  end
@@ -1,20 +1,52 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module ConnectionAdapters
3
5
  module SQLServer
4
- class SchemaCreation < AbstractAdapter::SchemaCreation
5
-
6
+ class SchemaCreation < SchemaCreation
6
7
  private
7
8
 
9
+ def supports_index_using?
10
+ false
11
+ end
12
+
8
13
  def visit_TableDefinition(o)
14
+ if_not_exists = o.if_not_exists
15
+
9
16
  if o.as
10
17
  table_name = quote_table_name(o.temporary ? "##{o.name}" : o.name)
11
18
  query = o.as.respond_to?(:to_sql) ? o.as.to_sql : o.as
12
19
  projections, source = query.match(%r{SELECT\s+(.*)?\s+FROM\s+(.*)?}).captures
13
- select_into = "SELECT #{projections} INTO #{table_name} FROM #{source}"
20
+ sql = "SELECT #{projections} INTO #{table_name} FROM #{source}"
14
21
  else
15
22
  o.instance_variable_set :@as, nil
16
- super
23
+ o.instance_variable_set :@if_not_exists, false
24
+ sql = super
25
+ end
26
+
27
+ if if_not_exists
28
+ o.instance_variable_set :@if_not_exists, true
29
+ table_name = o.temporary ? "##{o.name}" : o.name
30
+ sql = "IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='#{table_name}' and xtype='U') #{sql}"
17
31
  end
32
+
33
+ sql
34
+ end
35
+
36
+ def visit_CreateIndexDefinition(o)
37
+ index = o.index
38
+
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
48
+
49
+ sql.join(" ")
18
50
  end
19
51
 
20
52
  def add_column_options!(sql, options)
@@ -22,6 +54,9 @@ module ActiveRecord
22
54
  if options[:null] == false
23
55
  sql << " NOT NULL"
24
56
  end
57
+ if options[:collation].present?
58
+ sql << " COLLATE #{options[:collation]}"
59
+ end
25
60
  if options[:is_identity] == true
26
61
  sql << " IDENTITY(1,1)"
27
62
  end
@@ -34,7 +69,7 @@ module ActiveRecord
34
69
  def action_sql(action, dependency)
35
70
  case dependency
36
71
  when :restrict
37
- raise ArgumentError, <<-MSG.strip_heredoc
72
+ raise ArgumentError, <<~MSG.squish
38
73
  '#{dependency}' is not supported for :on_update or :on_delete.
39
74
  Supported values are: :nullify, :cascade
40
75
  MSG
@@ -50,7 +85,6 @@ module ActiveRecord
50
85
  def options_primary_key_with_nil_default?(options)
51
86
  options[:primary_key] && options.include?(:default) && options[:default].nil?
52
87
  end
53
-
54
88
  end
55
89
  end
56
90
  end