activerecord-sqlserver-adapter 5.2.1 → 6.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +9 -0
  3. data/.github/issue_template.md +23 -0
  4. data/.travis.yml +6 -8
  5. data/CHANGELOG.md +22 -32
  6. data/{Dockerfile → Dockerfile.ci} +1 -1
  7. data/Gemfile +42 -41
  8. data/README.md +9 -30
  9. data/RUNNING_UNIT_TESTS.md +3 -0
  10. data/Rakefile +2 -0
  11. data/VERSION +1 -1
  12. data/activerecord-sqlserver-adapter.gemspec +25 -14
  13. data/appveyor.yml +24 -17
  14. data/docker-compose.ci.yml +7 -5
  15. data/guides/RELEASING.md +11 -0
  16. data/lib/active_record/connection_adapters/sqlserver/core_ext/active_record.rb +2 -0
  17. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +2 -0
  18. data/lib/active_record/connection_adapters/sqlserver/core_ext/calculations.rb +2 -0
  19. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +3 -1
  20. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +2 -0
  21. data/lib/active_record/connection_adapters/sqlserver/core_ext/finder_methods.rb +6 -4
  22. data/lib/active_record/connection_adapters/sqlserver/core_ext/preloader.rb +36 -0
  23. data/lib/active_record/connection_adapters/sqlserver/core_ext/query_methods.rb +4 -1
  24. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +9 -0
  25. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +55 -14
  26. data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +2 -0
  27. data/lib/active_record/connection_adapters/sqlserver/errors.rb +2 -0
  28. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +38 -0
  29. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +16 -3
  30. data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +2 -0
  31. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +93 -70
  32. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +2 -0
  33. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_table.rb +2 -0
  34. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_xml.rb +2 -0
  35. data/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb +2 -0
  36. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +42 -40
  37. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +3 -1
  38. data/lib/active_record/connection_adapters/sqlserver/type.rb +2 -0
  39. data/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb +3 -1
  40. data/lib/active_record/connection_adapters/sqlserver/type/binary.rb +5 -2
  41. data/lib/active_record/connection_adapters/sqlserver/type/boolean.rb +3 -1
  42. data/lib/active_record/connection_adapters/sqlserver/type/char.rb +5 -2
  43. data/lib/active_record/connection_adapters/sqlserver/type/data.rb +2 -0
  44. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +3 -1
  45. data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +7 -6
  46. data/lib/active_record/connection_adapters/sqlserver/type/datetime2.rb +2 -0
  47. data/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb +2 -0
  48. data/lib/active_record/connection_adapters/sqlserver/type/decimal.rb +5 -2
  49. data/lib/active_record/connection_adapters/sqlserver/type/float.rb +3 -1
  50. data/lib/active_record/connection_adapters/sqlserver/type/integer.rb +3 -1
  51. data/lib/active_record/connection_adapters/sqlserver/type/json.rb +2 -0
  52. data/lib/active_record/connection_adapters/sqlserver/type/money.rb +4 -2
  53. data/lib/active_record/connection_adapters/sqlserver/type/real.rb +3 -1
  54. data/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb +3 -1
  55. data/lib/active_record/connection_adapters/sqlserver/type/small_money.rb +4 -2
  56. data/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb +3 -1
  57. data/lib/active_record/connection_adapters/sqlserver/type/string.rb +2 -0
  58. data/lib/active_record/connection_adapters/sqlserver/type/text.rb +3 -1
  59. data/lib/active_record/connection_adapters/sqlserver/type/time.rb +5 -4
  60. data/lib/active_record/connection_adapters/sqlserver/type/time_value_fractional.rb +2 -0
  61. data/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb +3 -1
  62. data/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb +3 -1
  63. data/lib/active_record/connection_adapters/sqlserver/type/unicode_char.rb +5 -2
  64. data/lib/active_record/connection_adapters/sqlserver/type/unicode_string.rb +2 -0
  65. data/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb +3 -1
  66. data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar.rb +6 -3
  67. data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb +4 -2
  68. data/lib/active_record/connection_adapters/sqlserver/type/uuid.rb +3 -1
  69. data/lib/active_record/connection_adapters/sqlserver/type/varbinary.rb +6 -3
  70. data/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb +4 -2
  71. data/lib/active_record/connection_adapters/sqlserver/type/varchar.rb +6 -3
  72. data/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb +4 -2
  73. data/lib/active_record/connection_adapters/sqlserver/utils.rb +2 -0
  74. data/lib/active_record/connection_adapters/sqlserver/version.rb +2 -0
  75. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +44 -10
  76. data/lib/active_record/connection_adapters/sqlserver_column.rb +9 -3
  77. data/lib/active_record/sqlserver_base.rb +8 -0
  78. data/lib/active_record/tasks/sqlserver_database_tasks.rb +2 -0
  79. data/lib/activerecord-sqlserver-adapter.rb +2 -0
  80. data/lib/arel/visitors/sqlserver.rb +40 -10
  81. data/lib/arel_sqlserver.rb +2 -0
  82. data/test/appveyor/dbsetup.ps1 +4 -4
  83. data/test/cases/adapter_test_sqlserver.rb +65 -1
  84. data/test/cases/change_column_null_test_sqlserver.rb +2 -0
  85. data/test/cases/coerced_tests.rb +644 -187
  86. data/test/cases/column_test_sqlserver.rb +2 -1
  87. data/test/cases/connection_test_sqlserver.rb +2 -0
  88. data/test/cases/execute_procedure_test_sqlserver.rb +2 -0
  89. data/test/cases/fetch_test_sqlserver.rb +2 -0
  90. data/test/cases/fully_qualified_identifier_test_sqlserver.rb +4 -2
  91. data/test/cases/helper_sqlserver.rb +2 -0
  92. data/test/cases/in_clause_test_sqlserver.rb +36 -0
  93. data/test/cases/index_test_sqlserver.rb +2 -0
  94. data/test/cases/json_test_sqlserver.rb +2 -0
  95. data/test/cases/migration_test_sqlserver.rb +4 -2
  96. data/test/cases/order_test_sqlserver.rb +2 -0
  97. data/test/cases/pessimistic_locking_test_sqlserver.rb +2 -0
  98. data/test/cases/rake_test_sqlserver.rb +2 -0
  99. data/test/cases/schema_dumper_test_sqlserver.rb +3 -1
  100. data/test/cases/schema_test_sqlserver.rb +2 -0
  101. data/test/cases/scratchpad_test_sqlserver.rb +2 -0
  102. data/test/cases/showplan_test_sqlserver.rb +4 -2
  103. data/test/cases/specific_schema_test_sqlserver.rb +2 -0
  104. data/test/cases/transaction_test_sqlserver.rb +2 -1
  105. data/test/cases/trigger_test_sqlserver.rb +2 -1
  106. data/test/cases/utils_test_sqlserver.rb +2 -0
  107. data/test/cases/uuid_test_sqlserver.rb +2 -1
  108. data/test/debug.rb +2 -0
  109. data/test/migrations/create_clients_and_change_column_null.rb +2 -0
  110. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +2 -0
  111. data/test/models/sqlserver/booking.rb +2 -0
  112. data/test/models/sqlserver/customers_view.rb +2 -0
  113. data/test/models/sqlserver/datatype.rb +2 -0
  114. data/test/models/sqlserver/datatype_migration.rb +2 -0
  115. data/test/models/sqlserver/dollar_table_name.rb +2 -0
  116. data/test/models/sqlserver/edge_schema.rb +2 -0
  117. data/test/models/sqlserver/fk_has_fk.rb +2 -0
  118. data/test/models/sqlserver/fk_has_pk.rb +2 -0
  119. data/test/models/sqlserver/natural_pk_data.rb +2 -0
  120. data/test/models/sqlserver/natural_pk_int_data.rb +2 -0
  121. data/test/models/sqlserver/no_pk_data.rb +2 -0
  122. data/test/models/sqlserver/object_default.rb +2 -0
  123. data/test/models/sqlserver/quoted_table.rb +2 -0
  124. data/test/models/sqlserver/quoted_view_1.rb +2 -0
  125. data/test/models/sqlserver/quoted_view_2.rb +2 -0
  126. data/test/models/sqlserver/sst_memory.rb +2 -0
  127. data/test/models/sqlserver/string_default.rb +2 -0
  128. data/test/models/sqlserver/string_defaults_big_view.rb +2 -0
  129. data/test/models/sqlserver/string_defaults_view.rb +2 -0
  130. data/test/models/sqlserver/tinyint_pk.rb +2 -0
  131. data/test/models/sqlserver/trigger.rb +2 -0
  132. data/test/models/sqlserver/trigger_history.rb +2 -0
  133. data/test/models/sqlserver/upper.rb +2 -0
  134. data/test/models/sqlserver/uppered.rb +2 -0
  135. data/test/models/sqlserver/uuid.rb +2 -0
  136. data/test/schema/sqlserver_specific_schema.rb +2 -0
  137. data/test/support/coerceable_test_sqlserver.rb +14 -5
  138. data/test/support/connection_reflection.rb +2 -0
  139. data/test/support/core_ext/query_cache.rb +3 -0
  140. data/test/support/load_schema_sqlserver.rb +2 -0
  141. data/test/support/minitest_sqlserver.rb +2 -0
  142. data/test/support/paths_sqlserver.rb +2 -0
  143. data/test/support/rake_helpers.rb +1 -0
  144. data/test/support/sql_counter_sqlserver.rb +3 -0
  145. data/test/support/test_in_memory_oltp.rb +2 -0
  146. metadata +18 -9
@@ -1,11 +1,13 @@
1
1
  version: "2.2"
2
2
  services:
3
- database:
3
+ sqlserver:
4
4
  image: metaskills/mssql-server-linux-rails
5
5
  ci:
6
6
  environment:
7
- - ACTIVERECORD_UNITTEST_HOST=database
8
- build: .
9
- command: wait-for database:1433 -- bundle exec rake test
7
+ - ACTIVERECORD_UNITTEST_HOST=sqlserver
8
+ build:
9
+ context: .
10
+ dockerfile: Dockerfile.ci
11
+ command: wait-for sqlserver:1433 -- bundle exec rake test
10
12
  depends_on:
11
- - "database"
13
+ - "sqlserver"
@@ -0,0 +1,11 @@
1
+ # Releasing
2
+
3
+ ## Building locally
4
+
5
+ If you want to build the gem to test it locally run `bundle exec rake build`.
6
+
7
+ This command will build the gem in `pkg/activerecord-sqlserver-adapter-A.B.C.gem`, where `A.B.C` is the version in `VERSION` file.
8
+
9
+ ## Releasing to RubyGems
10
+
11
+ Run `bundle exec rake release` to build the gem locally and push the `gem` file to RubyGems.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module ConnectionAdapters
3
5
  module SQLServer
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record/attribute_methods'
2
4
 
3
5
  module ActiveRecord
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record/relation'
2
4
  require 'active_record/version'
3
5
 
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module ConnectionAdapters
3
5
  module SQLServer
4
6
  module CoreExt
5
7
  module Explain
6
8
 
7
- SQLSERVER_STATEMENT_PREFIX = 'EXEC sp_executesql '.freeze
9
+ SQLSERVER_STATEMENT_PREFIX = 'EXEC sp_executesql '
8
10
  SQLSERVER_STATEMENT_REGEXP = /N'(.+)', N'(.+)', (.+)/
9
11
 
10
12
  def exec_explain(queries)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ActiveSupport.on_load(:active_record) do
2
4
  silence_warnings do
3
5
  # Already defined in Rails
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record/relation'
2
4
  require 'active_record/version'
3
5
 
@@ -11,13 +13,13 @@ module ActiveRecord
11
13
 
12
14
  # Same as original except we order by values in distinct select if present.
13
15
  def construct_relation_for_exists(conditions)
14
- if distinct_value && offset_value
15
- relation = limit!(1)
16
+ conditions = sanitize_forbidden_attributes(conditions)
16
17
 
18
+ if distinct_value && offset_value
17
19
  if select_values.present?
18
- relation = relation.order(*select_values)
20
+ relation = order(*select_values).limit!(1)
19
21
  else
20
- relation = relation.except(:order)
22
+ relation = except(:order).limit!(1)
21
23
  end
22
24
  else
23
25
  relation = except(:select, :distinct, :order)._select!(::ActiveRecord::FinderMethods::ONE_AS_ONE).limit!(1)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record/associations/preloader"
4
+
5
+ module ActiveRecord
6
+ module ConnectionAdapters
7
+ module SQLServer
8
+ module CoreExt
9
+ module Preloader
10
+ private
11
+
12
+ def records_for(ids)
13
+ ids.each_slice(in_clause_length).flat_map do |slice|
14
+ scope.where(association_key_name => slice).load do |record|
15
+ # Processing only the first owner
16
+ # because the record is modified but not an owner
17
+ owner = owners_by_key[convert_key(record[association_key_name])].first
18
+ association = owner.association(reflection.name)
19
+ association.set_inverse_instance(record)
20
+ end.records
21
+ end
22
+ end
23
+
24
+ def in_clause_length
25
+ 10_000
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ ActiveSupport.on_load(:active_record) do
34
+ mod = ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::Preloader
35
+ ActiveRecord::Associations::Preloader::Association.prepend(mod)
36
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record/relation'
2
4
  require 'active_record/version'
3
5
 
@@ -9,7 +11,8 @@ module ActiveRecord
9
11
 
10
12
  private
11
13
 
12
- # Copy of original from Rails master. This patch can be removed when adapter supports Rails 6.
14
+ # Copy of original from Rails master.
15
+ # This patch can be removed when adapter supports Rails version greater than 6.0.2.2
13
16
  def table_name_matches?(from)
14
17
  table_name = Regexp.escape(table.name)
15
18
  quoted_table_name = Regexp.escape(connection.quote_table_name(table.name))
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module ConnectionAdapters
3
5
  module SQLServer
@@ -9,10 +11,12 @@ module ActiveRecord
9
11
  def column_name_length
10
12
  128
11
13
  end
14
+ deprecate :column_name_length
12
15
 
13
16
  def table_name_length
14
17
  128
15
18
  end
19
+ deprecate :table_name_length
16
20
 
17
21
  def index_name_length
18
22
  128
@@ -21,14 +25,17 @@ module ActiveRecord
21
25
  def columns_per_table
22
26
  1024
23
27
  end
28
+ deprecate :columns_per_table
24
29
 
25
30
  def indexes_per_table
26
31
  999
27
32
  end
33
+ deprecate :indexes_per_table
28
34
 
29
35
  def columns_per_multicolumn_index
30
36
  16
31
37
  end
38
+ deprecate :columns_per_multicolumn_index
32
39
 
33
40
  def in_clause_length
34
41
  10_000
@@ -37,10 +44,12 @@ module ActiveRecord
37
44
  def sql_query_length
38
45
  65_536 * 4_096
39
46
  end
47
+ deprecate :sql_query_length
40
48
 
41
49
  def joins_per_query
42
50
  256
43
51
  end
52
+ deprecate :joins_per_query
44
53
 
45
54
  private
46
55
 
@@ -1,9 +1,23 @@
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) # :nodoc:
8
+ private_constant :READ_QUERY
9
+
10
+ def write_query?(sql) # :nodoc:
11
+ !READ_QUERY.match?(sql)
12
+ end
5
13
 
6
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
18
+
19
+ materialize_transactions
20
+
7
21
  if id_insert_table_name = query_requires_identity_insert?(sql)
8
22
  with_identity_insert_enabled(id_insert_table_name) { do_execute(sql, name) }
9
23
  else
@@ -12,6 +26,12 @@ module ActiveRecord
12
26
  end
13
27
 
14
28
  def exec_query(sql, name = 'SQL', binds = [], prepare: false)
29
+ if preventing_writes? && write_query?(sql)
30
+ raise ActiveRecord::ReadOnlyError, "Write query attempted while in readonly mode: #{sql}"
31
+ end
32
+
33
+ materialize_transactions
34
+
15
35
  sp_executesql(sql, name, binds, prepare: prepare)
16
36
  end
17
37
 
@@ -71,9 +91,11 @@ module ActiveRecord
71
91
  def release_savepoint(name = current_savepoint_name)
72
92
  end
73
93
 
74
- def case_sensitive_comparison(table, attribute, column, value)
94
+ def case_sensitive_comparison(attribute, value)
95
+ column = column_for_attribute(attribute)
96
+
75
97
  if column.collation && !column.case_sensitive?
76
- table[attribute].eq(Arel::Nodes::Bin.new(value))
98
+ attribute.eq(Arel::Nodes::Bin.new(value))
77
99
  else
78
100
  super
79
101
  end
@@ -89,12 +111,12 @@ module ActiveRecord
89
111
  end
90
112
  end
91
113
 
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))
114
+ table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}" }
115
+ total_sqls = Array.wrap(table_deletes + fixture_inserts)
94
116
 
95
117
  disable_referential_integrity do
96
118
  transaction(requires_new: true) do
97
- total_sql.each do |sql|
119
+ total_sqls.each do |sql|
98
120
  execute sql, "Fixtures Load"
99
121
  yield if block_given?
100
122
  end
@@ -107,11 +129,6 @@ module ActiveRecord
107
129
  end
108
130
  private :can_perform_case_insensitive_comparison_for?
109
131
 
110
- def combine_multi_statements(total_sql)
111
- total_sql
112
- end
113
- private :combine_multi_statements
114
-
115
132
  def default_insert_value(column)
116
133
  if column.is_identity?
117
134
  table_name = quote(quote_table_name(column.table_name))
@@ -122,9 +139,22 @@ module ActiveRecord
122
139
  end
123
140
  private :default_insert_value
124
141
 
142
+ def build_insert_sql(insert) # :nodoc:
143
+ sql = +"INSERT #{insert.into}"
144
+
145
+ if returning = insert.send(:insert_all).returning
146
+ sql << " OUTPUT " << returning.map {|column| "INSERTED.#{quote_column_name(column)}" }.join(", ")
147
+ end
148
+
149
+ sql << " #{insert.values_list}"
150
+ sql
151
+ end
152
+
125
153
  # === SQLServer Specific ======================================== #
126
154
 
127
155
  def execute_procedure(proc_name, *variables)
156
+ materialize_transactions
157
+
128
158
  vars = if variables.any? && variables.first.is_a?(Hash)
129
159
  variables.first.map { |k, v| "@#{k} = #{quote(v)}" }
130
160
  else
@@ -221,18 +251,20 @@ module ActiveRecord
221
251
 
222
252
  protected
223
253
 
224
- def sql_for_insert(sql, pk, id_value, sequence_name, binds)
254
+ def sql_for_insert(sql, pk, binds)
225
255
  if pk.nil?
226
256
  table_name = query_requires_identity_insert?(sql)
227
257
  pk = primary_key(table_name)
228
258
  end
259
+
229
260
  sql = if pk && use_output_inserted? && !database_prefix_remote_server?
230
261
  quoted_pk = SQLServer::Utils.extract_identifiers(pk).quoted
231
262
  table_name ||= get_table_name(sql)
232
263
  exclude_output_inserted = exclude_output_inserted_table_name?(table_name, sql)
264
+
233
265
  if exclude_output_inserted
234
266
  id_sql_type = exclude_output_inserted.is_a?(TrueClass) ? 'bigint' : exclude_output_inserted
235
- <<-SQL.strip_heredoc
267
+ <<~SQL.squish
236
268
  DECLARE @ssaIdInsertTable table (#{quoted_pk} #{id_sql_type});
237
269
  #{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT INSERTED.#{quoted_pk} INTO @ssaIdInsertTable"}
238
270
  SELECT CAST(#{quoted_pk} AS #{id_sql_type}) FROM @ssaIdInsertTable
@@ -257,6 +289,8 @@ module ActiveRecord
257
289
  # === SQLServer Specific (Executing) ============================ #
258
290
 
259
291
  def do_execute(sql, name = 'SQL')
292
+ materialize_transactions
293
+
260
294
  log(sql, name) { raw_connection_do(sql) }
261
295
  end
262
296
 
@@ -310,7 +344,7 @@ module ActiveRecord
310
344
  types = quote(types.join(', '))
311
345
  params = params.map.with_index{ |p, i| "@#{i} = #{p}" }.join(', ') # Only p is needed, but with @i helps explain regexp.
312
346
  sql = "EXEC sp_executesql #{quote(sql)}"
313
- sql << ", #{types}, #{params}" unless params.empty?
347
+ sql += ", #{types}, #{params}" unless params.empty?
314
348
  end
315
349
  sql
316
350
  end
@@ -318,7 +352,14 @@ module ActiveRecord
318
352
  def raw_connection_do(sql)
319
353
  case @connection_options[:mode]
320
354
  when :dblib
321
- @connection.execute(sql).do
355
+ result = @connection.execute(sql)
356
+
357
+ # TinyTDS returns false instead of raising an exception if connection fails.
358
+ # Getting around this by raising an exception ourselves while this PR
359
+ # https://github.com/rails-sqlserver/tiny_tds/pull/469 is not released.
360
+ raise TinyTds::Error, 'failed to execute statement' if result.is_a?(FalseClass)
361
+
362
+ result.do
322
363
  end
323
364
  ensure
324
365
  @update_sql = false
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module ConnectionAdapters
3
5
  module SQLServer
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
 
3
5
  class DeadlockVictim < WrappedDatabaseException
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module ConnectionAdapters
3
5
  module SQLServer
@@ -68,6 +70,42 @@ module ActiveRecord
68
70
  end
69
71
  end
70
72
 
73
+ def column_name_matcher
74
+ COLUMN_NAME
75
+ end
76
+
77
+ def column_name_with_order_matcher
78
+ COLUMN_NAME_WITH_ORDER
79
+ end
80
+
81
+ COLUMN_NAME = /
82
+ \A
83
+ (
84
+ (?:
85
+ # [table_name].[column_name] | function(one or no argument)
86
+ ((?:\w+\.|\[\w+\]\.)?(?:\w+|\[\w+\])) | \w+\((?:|\g<2>)\)
87
+ )
88
+ (?:\s+AS\s+(?:\w+|\[\w+\]))?
89
+ )
90
+ (?:\s*,\s*\g<1>)*
91
+ \z
92
+ /ix
93
+
94
+ COLUMN_NAME_WITH_ORDER = /
95
+ \A
96
+ (
97
+ (?:
98
+ # [table_name].[column_name] | function(one or no argument)
99
+ ((?:\w+\.|\[\w+\]\.)?(?:\w+|\[\w+\])) | \w+\((?:|\g<2>)\)
100
+ )
101
+ (?:\s+ASC|\s+DESC)?
102
+ (?:\s+NULLS\s+(?:FIRST|LAST))?
103
+ )
104
+ (?:\s*,\s*\g<1>)*
105
+ \z
106
+ /ix
107
+
108
+ private_constant :COLUMN_NAME, :COLUMN_NAME_WITH_ORDER
71
109
 
72
110
  private
73
111
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord
2
4
  module ConnectionAdapters
3
5
  module SQLServer
@@ -6,15 +8,26 @@ module ActiveRecord
6
8
  private
7
9
 
8
10
  def visit_TableDefinition(o)
11
+ if_not_exists = o.if_not_exists
12
+
9
13
  if o.as
10
14
  table_name = quote_table_name(o.temporary ? "##{o.name}" : o.name)
11
15
  query = o.as.respond_to?(:to_sql) ? o.as.to_sql : o.as
12
16
  projections, source = query.match(%r{SELECT\s+(.*)?\s+FROM\s+(.*)?}).captures
13
- select_into = "SELECT #{projections} INTO #{table_name} FROM #{source}"
17
+ sql = "SELECT #{projections} INTO #{table_name} FROM #{source}"
14
18
  else
15
19
  o.instance_variable_set :@as, nil
16
- super
20
+ o.instance_variable_set :@if_not_exists, false
21
+ sql = super
17
22
  end
23
+
24
+ if if_not_exists
25
+ o.instance_variable_set :@if_not_exists, true
26
+ table_name = o.temporary ? "##{o.name}" : o.name
27
+ sql = "IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='#{table_name}' and xtype='U') #{sql}"
28
+ end
29
+
30
+ sql
18
31
  end
19
32
 
20
33
  def add_column_options!(sql, options)
@@ -34,7 +47,7 @@ module ActiveRecord
34
47
  def action_sql(action, dependency)
35
48
  case dependency
36
49
  when :restrict
37
- raise ArgumentError, <<-MSG.strip_heredoc
50
+ raise ArgumentError, <<~MSG.squish
38
51
  '#{dependency}' is not supported for :on_update or :on_delete.
39
52
  Supported values are: :nullify, :cascade
40
53
  MSG