activerecord-sqlserver-adapter 5.2.1 → 7.0.0.0

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 (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