activerecord-sqlserver-adapter-odbc-extended 8.0.10

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 (102) hide show
  1. checksums.yaml +7 -0
  2. data/.github/issue_template.md +22 -0
  3. data/.github/workflows/ci.yml +32 -0
  4. data/.gitignore +9 -0
  5. data/.rubocop.yml +69 -0
  6. data/CHANGELOG.md +5 -0
  7. data/CODE_OF_CONDUCT.md +132 -0
  8. data/Dockerfile.ci +14 -0
  9. data/Gemfile +26 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +104 -0
  12. data/RUNNING_UNIT_TESTS.md +38 -0
  13. data/Rakefile +45 -0
  14. data/VERSION +1 -0
  15. data/activerecord-sqlserver-adapter-odbc-extended.gemspec +34 -0
  16. data/compose.ci.yaml +15 -0
  17. data/lib/active_record/connection_adapters/extended_sqlserver_adapter.rb +204 -0
  18. data/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb +41 -0
  19. data/lib/active_record/connection_adapters/sqlserver/odbc_database_statements.rb +234 -0
  20. data/lib/active_record/connection_adapters/sqlserver/type/binary_ext.rb +25 -0
  21. data/lib/activerecord-sqlserver-adapter-odbc-extended.rb +12 -0
  22. data/test/appveyor/dbsetup.ps1 +27 -0
  23. data/test/appveyor/dbsetup.sql +11 -0
  24. data/test/cases/active_schema_test_sqlserver.rb +127 -0
  25. data/test/cases/adapter_test_sqlserver.rb +648 -0
  26. data/test/cases/change_column_collation_test_sqlserver.rb +33 -0
  27. data/test/cases/change_column_null_test_sqlserver.rb +44 -0
  28. data/test/cases/coerced_tests.rb +2796 -0
  29. data/test/cases/column_test_sqlserver.rb +848 -0
  30. data/test/cases/connection_test_sqlserver.rb +138 -0
  31. data/test/cases/dbconsole.rb +19 -0
  32. data/test/cases/disconnected_test_sqlserver.rb +42 -0
  33. data/test/cases/eager_load_too_many_ids_test_sqlserver.rb +18 -0
  34. data/test/cases/enum_test_sqlserver.rb +49 -0
  35. data/test/cases/execute_procedure_test_sqlserver.rb +57 -0
  36. data/test/cases/fetch_test_sqlserver.rb +88 -0
  37. data/test/cases/fully_qualified_identifier_test_sqlserver.rb +72 -0
  38. data/test/cases/helper_sqlserver.rb +61 -0
  39. data/test/cases/migration_test_sqlserver.rb +144 -0
  40. data/test/cases/order_test_sqlserver.rb +153 -0
  41. data/test/cases/pessimistic_locking_test_sqlserver.rb +102 -0
  42. data/test/cases/primary_keys_test_sqlserver.rb +103 -0
  43. data/test/cases/rake_test_sqlserver.rb +198 -0
  44. data/test/cases/schema_dumper_test_sqlserver.rb +296 -0
  45. data/test/cases/schema_test_sqlserver.rb +111 -0
  46. data/test/cases/trigger_test_sqlserver.rb +51 -0
  47. data/test/cases/utils_test_sqlserver.rb +129 -0
  48. data/test/cases/uuid_test_sqlserver.rb +54 -0
  49. data/test/cases/view_test_sqlserver.rb +58 -0
  50. data/test/config.yml +38 -0
  51. data/test/debug.rb +16 -0
  52. data/test/fixtures/1px.gif +0 -0
  53. data/test/migrations/create_clients_and_change_column_collation.rb +19 -0
  54. data/test/migrations/create_clients_and_change_column_null.rb +25 -0
  55. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +11 -0
  56. data/test/models/sqlserver/alien.rb +5 -0
  57. data/test/models/sqlserver/booking.rb +5 -0
  58. data/test/models/sqlserver/composite_pk.rb +9 -0
  59. data/test/models/sqlserver/customers_view.rb +5 -0
  60. data/test/models/sqlserver/datatype.rb +5 -0
  61. data/test/models/sqlserver/datatype_migration.rb +10 -0
  62. data/test/models/sqlserver/dollar_table_name.rb +5 -0
  63. data/test/models/sqlserver/edge_schema.rb +13 -0
  64. data/test/models/sqlserver/fk_has_fk.rb +5 -0
  65. data/test/models/sqlserver/fk_has_pk.rb +5 -0
  66. data/test/models/sqlserver/natural_pk_data.rb +6 -0
  67. data/test/models/sqlserver/natural_pk_int_data.rb +5 -0
  68. data/test/models/sqlserver/no_pk_data.rb +5 -0
  69. data/test/models/sqlserver/object_default.rb +5 -0
  70. data/test/models/sqlserver/quoted_table.rb +9 -0
  71. data/test/models/sqlserver/quoted_view_1.rb +5 -0
  72. data/test/models/sqlserver/quoted_view_2.rb +5 -0
  73. data/test/models/sqlserver/sst_memory.rb +5 -0
  74. data/test/models/sqlserver/sst_string_collation.rb +3 -0
  75. data/test/models/sqlserver/string_default.rb +5 -0
  76. data/test/models/sqlserver/string_defaults_big_view.rb +5 -0
  77. data/test/models/sqlserver/string_defaults_view.rb +5 -0
  78. data/test/models/sqlserver/table_with_spaces.rb +5 -0
  79. data/test/models/sqlserver/tinyint_pk.rb +5 -0
  80. data/test/models/sqlserver/trigger.rb +17 -0
  81. data/test/models/sqlserver/trigger_history.rb +5 -0
  82. data/test/models/sqlserver/upper.rb +5 -0
  83. data/test/models/sqlserver/uppered.rb +5 -0
  84. data/test/models/sqlserver/uuid.rb +5 -0
  85. data/test/schema/datatypes/2012.sql +56 -0
  86. data/test/schema/enable-in-memory-oltp.sql +81 -0
  87. data/test/schema/sqlserver_specific_schema.rb +363 -0
  88. data/test/support/coerceable_test_sqlserver.rb +55 -0
  89. data/test/support/connection_reflection.rb +32 -0
  90. data/test/support/core_ext/query_cache.rb +38 -0
  91. data/test/support/load_schema_sqlserver.rb +29 -0
  92. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic.dump +0 -0
  93. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic_associations.dump +0 -0
  94. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic.dump +0 -0
  95. data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic_associations.dump +0 -0
  96. data/test/support/minitest_sqlserver.rb +3 -0
  97. data/test/support/paths_sqlserver.rb +50 -0
  98. data/test/support/query_assertions.rb +49 -0
  99. data/test/support/rake_helpers.rb +46 -0
  100. data/test/support/table_definition_sqlserver.rb +24 -0
  101. data/test/support/test_in_memory_oltp.rb +17 -0
  102. metadata +240 -0
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ # Extends the ActiveRecord SQL Server adapter with custom behavior
6
+ # and compatibility fixes for ODBC and SQL Server–specific features.
7
+ # Provides overrides for query execution, type casting, and schema handling.
8
+ class SQLServerAdapter
9
+ class << self
10
+ def new_client(config)
11
+ case config[:mode].to_sym
12
+ when :dblib
13
+ dblib_connect(config)
14
+ when :odbc
15
+ odbc_connect(config)
16
+ else
17
+ raise ArgumentError, "Unknown connection mode in #{config.inspect}."
18
+ end
19
+ end
20
+
21
+ def dblib_connect(config)
22
+ TinyTds::Client.new(config)
23
+ rescue TinyTds::Error => e
24
+ raise ActiveRecord::NoDatabaseError if e.message.match(/database .* does not exist/i)
25
+
26
+ raise e.message
27
+ end
28
+
29
+ def odbc_connect(config)
30
+ raise ArgumentError, "Missing :dsn configuration." unless config.key?(:dsn)
31
+
32
+ if config[:dsn].include?(";")
33
+ driver = ODBC::Driver.new.tap do |d|
34
+ d.name = config[:dsn_name] || "Driver1"
35
+ d.attrs = config[:dsn].split("\n").map do |atr|
36
+ atr.split("=")
37
+ end.select { |kv| kv.size == 2 }.each_with_object({}) do |e, a|
38
+ k, v = e
39
+ a[k] = v
40
+ end
41
+ end
42
+
43
+ ODBC::Database.new.drvconnect(driver)
44
+ else
45
+ ODBC.connect config[:dsn], config[:username], config[:password]
46
+ end.tap do |c|
47
+ c.use_time = true
48
+ c.use_utc = ActiveRecord.default_timezone || :utc
49
+ rescue StandardError
50
+ warn "Ruby ODBC v0.99992 or higher is required."
51
+ end
52
+ rescue ODBC::Error => e
53
+ raise ActiveRecord::NoDatabaseError if e.message.match(/database .* does not exist/i)
54
+
55
+ raise e.message
56
+ end
57
+ end
58
+
59
+ def initialize(...)
60
+ super
61
+
62
+ @config[:tds_version] ||= "7.3" if @config[:mode].to_sym == :dblib
63
+ @config[:appname] = self.class.rails_application_name unless @config[:appname]
64
+ @config[:login_timeout] = @config[:login_timeout].present? ? @config[:login_timeout].to_i : nil
65
+ @config[:timeout] = @config[:timeout].present? ? @config[:timeout].to_i / 1000 : nil
66
+ @config[:encoding] = @config[:encoding].present? ? @config[:encoding] : nil
67
+
68
+ @connection_parameters ||= @config
69
+
70
+ apply_mode_specific_behavior
71
+ end
72
+ # === Abstract Adapter (Connection Management) ================== #
73
+
74
+ def active?
75
+ return false unless @raw_connection
76
+
77
+ @connection_parameters[:mode].to_sym == :dblib ? @raw_connection.active? : odbc_connection_active?
78
+ rescue *connection_errors
79
+ false
80
+ end
81
+
82
+ def odbc_connection_active?
83
+ @raw_connection.do("SELECT 1")
84
+ true
85
+ rescue *connection_errors
86
+ false
87
+ end
88
+
89
+ def reconnect
90
+ case @connection_parameters[:mode].to_sym
91
+ when :dblib
92
+ begin
93
+ @raw_connection&.close
94
+ rescue StandardError
95
+ nil
96
+ end
97
+ when :odbc
98
+ begin
99
+ @raw_connection&.disconnect
100
+ rescue StandardError
101
+ nil
102
+ end
103
+ end
104
+
105
+ @raw_connection = nil
106
+ @spid = nil
107
+ @collation = nil
108
+
109
+ connect
110
+ end
111
+
112
+ def disconnect!
113
+ super
114
+
115
+ case @connection_parameters[:mode].to_sym
116
+ when :dblib
117
+ begin
118
+ @raw_connection&.close
119
+ rescue StandardError
120
+ nil
121
+ end
122
+ when :odbc
123
+ begin
124
+ @raw_connection&.disconnect
125
+ rescue StandardError
126
+ nil
127
+ end
128
+ end
129
+
130
+ @raw_connection = nil
131
+ @spid = nil
132
+ @collation = nil
133
+ end
134
+
135
+ # === SQLServer Specific (Connection Management) ================ #
136
+
137
+ protected
138
+
139
+ def connection_errors
140
+ @connection_errors ||= [].tap do |errors|
141
+ errors << TinyTds::Error if defined?(TinyTds::Error)
142
+ errors << ODBC::Error if defined?(ODBC::Error)
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def configure_connection
149
+ send("configure_#{@config[:mode]}_connection")
150
+
151
+ @spid = _raw_select("SELECT @@SPID", @raw_connection).first.first
152
+
153
+ initialize_dateformatter
154
+ use_database
155
+ end
156
+
157
+ def configure_dblib_connection
158
+ if @config[:azure]
159
+ @raw_connection.execute("SET ANSI_NULLS ON").do
160
+ @raw_connection.execute("SET ANSI_NULL_DFLT_ON ON").do
161
+ @raw_connection.execute("SET ANSI_PADDING ON").do
162
+ @raw_connection.execute("SET ANSI_WARNINGS ON").do
163
+ else
164
+ @raw_connection.execute("SET ANSI_DEFAULTS ON").do
165
+ end
166
+
167
+ @raw_connection.execute("SET QUOTED_IDENTIFIER ON").do
168
+ @raw_connection.execute("SET CURSOR_CLOSE_ON_COMMIT OFF").do
169
+ @raw_connection.execute("SET IMPLICIT_TRANSACTIONS OFF").do
170
+ @raw_connection.execute("SET TEXTSIZE 2147483647").do
171
+ @raw_connection.execute("SET CONCAT_NULL_YIELDS_NULL ON").do
172
+ end
173
+
174
+ def configure_odbc_connection
175
+ if @config[:azure]
176
+ @raw_connection.do("SET ANSI_NULLS ON")
177
+ @raw_connection.do("SET ANSI_NULL_DFLT_ON ON")
178
+ @raw_connection.do("SET ANSI_PADDING ON")
179
+ @raw_connection.do("SET ANSI_WARNINGS ON")
180
+ else
181
+ @raw_connection.do("SET ANSI_DEFAULTS ON")
182
+ end
183
+
184
+ @raw_connection.do("SET QUOTED_IDENTIFIER ON")
185
+ @raw_connection.do("SET CURSOR_CLOSE_ON_COMMIT OFF")
186
+ @raw_connection.do("SET IMPLICIT_TRANSACTIONS OFF")
187
+ @raw_connection.do("SET TEXTSIZE 2147483647")
188
+ @raw_connection.do("SET CONCAT_NULL_YIELDS_NULL ON")
189
+
190
+ @raw_connection.do("SET LOCK_TIMEOUT 45000")
191
+ end
192
+
193
+ def apply_mode_specific_behavior
194
+ return if @config[:mode].to_sym != :odbc
195
+
196
+ require "odbc"
197
+ require "active_record/connection_adapters/sqlserver/core_ext/odbc"
198
+ require "active_record/connection_adapters/sqlserver/odbc_database_statements"
199
+
200
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.prepend SQLServer::OdbcDatabaseStatements
201
+ end
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module SQLServer
6
+ module CoreExt
7
+ module ODBC
8
+ # Adds helper methods for handling ODBC statement objects.
9
+ # Provides a safe implementation of +finished?+ to check
10
+ # connection state and handle ODBC errors gracefully.
11
+ module Statement
12
+ def finished?
13
+ connected?
14
+ false
15
+ rescue ::ODBC::Error
16
+ true
17
+ end
18
+ end
19
+
20
+ # Adds helper methods for ODBC database operations.
21
+ # Wraps execution blocks to ensure statement handles are
22
+ # properly released after use, preventing connection leaks.
23
+ module Database
24
+ def run_block(*args)
25
+ sth = run(*args)
26
+
27
+ begin
28
+ yield sth
29
+ ensure
30
+ sth.drop if sth&.connected?
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ ODBC::Statement.include ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::ODBC::Statement
41
+ ODBC::Database.include ActiveRecord::ConnectionAdapters::SQLServer::CoreExt::ODBC::Database
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module SQLServer
6
+ # Removes default SQL Server implementations of +exec_update+ and +exec_delete+
7
+ # so they can be replaced by ODBC-specific versions.
8
+ # Otherwise, when calling +super+ from the OdbcDatabaseStatements module,
9
+ # Ruby’s ancestor chain would still include this module and invoke
10
+ # the original TinyTDS-based methods.
11
+ module DatabaseStatements
12
+ remove_method :exec_update
13
+ remove_method :exec_delete
14
+ end
15
+
16
+ # Module: OdbcDatabaseStatements
17
+ #
18
+ # Provides ODBC-specific SQL execution methods for the SQL Server adapter.
19
+ # This module overrides default DatabaseStatements methods to handle
20
+ # ODBC driver behavior, such as statement handle cleanup and row count
21
+ # retrieval through @@ROWCOUNT.
22
+ #
23
+ # When included, this module replaces the default TinyTDS-based methods
24
+ # removed from +DatabaseStatements+, ensuring correct behavior for ODBC
25
+ # connections and allowing +super+ calls without invoking the removed methods.
26
+ module OdbcDatabaseStatements
27
+ def affected_rows(raw_result)
28
+ return if raw_result.blank?
29
+
30
+ column_name = lowercase_schema_reflection ? "affectedrows" : "AffectedRows"
31
+ raw_result.first[column_name]
32
+ end
33
+
34
+ def internal_exec_sql_query(sql, conn)
35
+ handle = internal_raw_execute(sql, conn)
36
+ handle_to_names_and_values(handle, ar_result: true)
37
+ ensure
38
+ finish_statement_handle(handle)
39
+ end
40
+
41
+ def exec_delete(sql, name = nil, binds = [])
42
+ super || super("SELECT @@ROWCOUNT As AffectedRows", "", [])
43
+ end
44
+
45
+ def exec_update(sql, name = nil, binds = [])
46
+ super || super("SELECT @@ROWCOUNT As AffectedRows", "", [])
47
+ end
48
+
49
+ # === SQLServer Specific ======================================== #
50
+
51
+ def execute_procedure(proc_name, *variables)
52
+ vars = if variables.any? && variables.first.is_a?(Hash)
53
+ variables.first.map { |k, v| "@#{k} = #{quote(v)}" }
54
+ else
55
+ variables.map { |v| quote(v) }
56
+ end.join(", ")
57
+ sql = "EXEC #{proc_name} #{vars}".strip
58
+
59
+ log(sql, "Execute Procedure") do |notification_payload|
60
+ with_raw_connection do |conn|
61
+ result = execute_odbc_procedure(sql, conn)
62
+ notification_payload[:row_count] = result&.count
63
+ result
64
+ end
65
+ end
66
+ end
67
+
68
+ protected
69
+
70
+ def sql_for_insert(sql, pk, binds, returning)
71
+ if pk.nil?
72
+ table_name = query_requires_identity_insert?(sql)
73
+ pk = primary_key(table_name)
74
+ end
75
+
76
+ sql = if pk && use_output_inserted? && !database_prefix_remote_server?
77
+ table_name ||= get_table_name(sql)
78
+ exclude_output_inserted = exclude_output_inserted_table_name?(table_name, sql)
79
+
80
+ if exclude_output_inserted
81
+ pk_and_types = Array(pk).map do |subkey|
82
+ {
83
+ quoted: SQLServer::Utils.extract_identifiers(subkey).quoted,
84
+ id_sql_type: exclude_output_inserted_id_sql_type(subkey, exclude_output_inserted)
85
+ }
86
+ end
87
+
88
+ <<-SQL.strip_heredoc
89
+ SET NOCOUNT ON
90
+ DECLARE @ssaIdInsertTable table (#{pk_and_types.map { |pk_and_type| "#{pk_and_type[:quoted]} #{pk_and_type[:id_sql_type]}" }.join(", ")});
91
+ #{sql.dup.insert sql.index(/ (DEFAULT )?VALUES/), " OUTPUT #{pk_and_types.map { |pk_and_type| "INSERTED.#{pk_and_type[:quoted]}" }.join(", ")} INTO @ssaIdInsertTable"}
92
+ 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;
93
+ SET NOCOUNT OFF
94
+ SQL
95
+ else
96
+ returning_columns = returning || Array(pk)
97
+
98
+ if returning_columns.any?
99
+ returning_columns_statements = returning_columns.map { |c| " INSERTED.#{SQLServer::Utils.extract_identifiers(c).quoted}" }
100
+ sql.dup.insert sql.index(/ (DEFAULT )?VALUES/i),
101
+ " OUTPUT#{returning_columns_statements.join(",")}"
102
+ else
103
+ sql
104
+ end
105
+ end
106
+ else
107
+ table = get_table_name(sql)
108
+ id_column = identity_columns(table.to_s.strip).first
109
+
110
+ if id_column.present?
111
+ sql.sub(/\s*VALUES\s*\(/, " OUTPUT INSERTED.#{id_column.name} VALUES (")
112
+ else
113
+ sql.sub(/\s*VALUES\s*\(/, " OUTPUT CAST(SCOPE_IDENTITY() AS bigint) AS Ident VALUES (")
114
+ end
115
+ end
116
+
117
+ [sql, binds]
118
+ end
119
+
120
+ # === SQLServer Specific ======================================== #
121
+
122
+ def set_identity_insert(table_name, conn, enable)
123
+ internal_raw_execute("SET IDENTITY_INSERT #{table_name} #{enable ? "ON" : "OFF"}", conn, perform_do: true)
124
+ rescue StandardError
125
+ raise ActiveRecordError,
126
+ "IDENTITY_INSERT could not be turned #{enable ? "ON" : "OFF"} for table #{table_name}"
127
+ end
128
+
129
+ def sp_executesql_sql_type(attr)
130
+ if attr.respond_to?(:type)
131
+ type = attr.type.is_a?(ActiveRecord::Normalization::NormalizedValueType) ? attr.type.cast_type : attr.type
132
+ type = type.subtype if type.serialized?
133
+
134
+ return type.sqlserver_type if type.respond_to?(:sqlserver_type)
135
+
136
+ if type.is_a?(ActiveRecord::Encryption::EncryptedAttributeType) &&
137
+ type.instance_variable_get(:@cast_type).respond_to?(:sqlserver_type)
138
+ return type.instance_variable_get(:@cast_type).sqlserver_type
139
+ end
140
+ end
141
+
142
+ value = active_model_attribute?(attr) ? attr.value_for_database : attr
143
+
144
+ if value.is_a?(Numeric)
145
+ if value.is_a?(Integer)
146
+ value > 2_147_483_647 ? "bigint" : "int"
147
+ else
148
+ # For Float, BigDecimal, Rational etc.
149
+ value.is_a?(BigDecimal) ? "decimal(18,6)" : "float"
150
+ end
151
+ else
152
+ "nvarchar(max)"
153
+ end
154
+ end
155
+
156
+ # === SQLServer Specific (Selecting) ============================ #
157
+
158
+ def _raw_select(sql, conn)
159
+ handle = internal_raw_execute(sql, conn)
160
+ handle_to_names_and_values(handle, fetch: :rows)
161
+ ensure
162
+ finish_statement_handle(handle)
163
+ end
164
+
165
+ def handle_to_names_and_values(handle, options = {})
166
+ @raw_connection.use_utc = ActiveRecord.default_timezone || :utc
167
+
168
+ return build_ar_result(handle) if options[:ar_result]
169
+
170
+ fetch_handle_data(handle, options[:fetch])
171
+ end
172
+
173
+ def finish_statement_handle(handle)
174
+ return unless handle
175
+
176
+ handle.drop if handle.respond_to?(:drop) && !handle.finished?
177
+ handle
178
+ end
179
+
180
+ # Executing SQL for ODBC mode
181
+ def internal_raw_execute(sql, raw_connection, perform_do: false)
182
+ return raw_connection.do(sql) if perform_do
183
+
184
+ block_given? ? raw_connection.run_block(sql) { |handle| yield(handle) } : raw_connection.run(sql)
185
+ end
186
+
187
+ private
188
+
189
+ def build_ar_result(handle)
190
+ columns = extract_column_names(handle)
191
+ rows = handle.fetch_all || []
192
+ ActiveRecord::Result.new(columns, rows)
193
+ end
194
+
195
+ def extract_column_names(handle)
196
+ if lowercase_schema_reflection
197
+ handle.columns(true).map { |c| c.name.downcase }
198
+ else
199
+ handle.columns(true).map(&:name)
200
+ end
201
+ end
202
+
203
+ def fetch_handle_data(handle, fetch_mode)
204
+ case fetch_mode
205
+ when :all
206
+ handle.each_hash || []
207
+ when :rows
208
+ handle.fetch_all || []
209
+ end
210
+ end
211
+
212
+ def execute_odbc_procedure(sql, conn)
213
+ results = []
214
+
215
+ internal_raw_execute(sql, conn) do |handle|
216
+ get_rows = lambda do
217
+ rows = handle_to_names_and_values handle, fetch: :all
218
+ results << rows.map!(&:with_indifferent_access)
219
+ end
220
+
221
+ get_rows.call
222
+ get_rows.call while handle_more_results?(handle)
223
+ end
224
+
225
+ results.many? ? results : results.first
226
+ end
227
+
228
+ def handle_more_results?(handle)
229
+ handle.more_results
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module SQLServer
6
+ module Type
7
+ # Extends SQL Server binary type handling in ActiveRecord.
8
+ # Adds a custom +cast_value+ implementation to properly encode or
9
+ # convert binary (varbinary) values from hex strings.
10
+ class Binary
11
+ # Custom override to handle binary casting for SQL Server.
12
+ # Ensures non-frozen strings are forced to binary encoding
13
+ # or packed from hex when needed.
14
+ def cast_value(value)
15
+ if value.instance_of?(::String) && !value.frozen?
16
+ value.force_encoding(Encoding::BINARY) =~ /[^[:xdigit:]]/ ? value : [value].pack("H*")
17
+ else
18
+ value
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "odbc_utf8"
5
+
6
+ # Ensure we only patch after ActiveRecord and the base SQL Server adapter are loaded
7
+ ActiveSupport.on_load(:active_record) do
8
+ require "activerecord-sqlserver-adapter"
9
+ require "active_record/connection_adapters/sqlserver_adapter"
10
+ require "active_record/connection_adapters/sqlserver/type/binary_ext"
11
+ require "active_record/connection_adapters/extended_sqlserver_adapter"
12
+ end
@@ -0,0 +1,27 @@
1
+
2
+ Write-Output "Setting up..."
3
+ [reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") | Out-Null
4
+ [reflection.assembly]::LoadWithPartialName("Microsoft.SqlServer.SqlWmiManagement") | Out-Null
5
+
6
+ Write-Output "Setting variables..."
7
+ $serverName = $env:COMPUTERNAME
8
+ $instanceNames = @('SQL2014')
9
+ $smo = 'Microsoft.SqlServer.Management.Smo.'
10
+ $wmi = new-object ($smo + 'Wmi.ManagedComputer')
11
+
12
+ Write-Output "Configure Instances..."
13
+ foreach ($instanceName in $instanceNames) {
14
+ Write-Output "Instance $instanceName ..."
15
+ Write-Output "Enable TCP/IP and port 1433..."
16
+ $uri = "ManagedComputer[@Name='$serverName']/ServerInstance[@Name='$instanceName']/ServerProtocol[@Name='Tcp']"
17
+ $tcp = $wmi.GetSmoObject($uri)
18
+ $tcp.IsEnabled = $true
19
+ foreach ($ipAddress in $Tcp.IPAddresses) {
20
+ $ipAddress.IPAddressProperties["TcpDynamicPorts"].Value = ""
21
+ $ipAddress.IPAddressProperties["TcpPort"].Value = "1433"
22
+ }
23
+ $tcp.Alter()
24
+ }
25
+
26
+ Set-Service SQLBrowser -StartupType Manual
27
+ Start-Service SQLBrowser
@@ -0,0 +1,11 @@
1
+ CREATE DATABASE [activerecord_unittest];
2
+ CREATE DATABASE [activerecord_unittest2];
3
+ GO
4
+ CREATE LOGIN [rails] WITH PASSWORD = '', CHECK_POLICY = OFF, DEFAULT_DATABASE = [activerecord_unittest];
5
+ GO
6
+ USE [activerecord_unittest];
7
+ CREATE USER [rails] FOR LOGIN [rails];
8
+ GO
9
+ EXEC sp_addrolemember N'db_owner', N'rails';
10
+ EXEC master..sp_addsrvrolemember @loginame = N'rails', @rolename = N'sysadmin'
11
+ GO
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cases/helper_sqlserver"
4
+
5
+ class ActiveSchemaTestSQLServer < ActiveRecord::TestCase
6
+ describe "indexes" do
7
+ before do
8
+ connection.create_table :schema_test_table, force: true, id: false do |t|
9
+ t.column :foo, :string, limit: 100
10
+ t.column :state, :string
11
+ end
12
+ end
13
+
14
+ after do
15
+ connection.drop_table :schema_test_table rescue nil
16
+ end
17
+
18
+ it 'default index' do
19
+ assert_queries_match('CREATE INDEX [index_schema_test_table_on_foo] ON [schema_test_table] ([foo])') do
20
+ connection.add_index :schema_test_table, "foo"
21
+ end
22
+ end
23
+
24
+ it 'unique index' do
25
+ assert_queries_match('CREATE UNIQUE INDEX [index_schema_test_table_on_foo] ON [schema_test_table] ([foo])') do
26
+ connection.add_index :schema_test_table, "foo", unique: true
27
+ end
28
+ end
29
+
30
+ it 'where condition on index' do
31
+ assert_queries_match("CREATE INDEX [index_schema_test_table_on_foo] ON [schema_test_table] ([foo]) WHERE state = 'active'") do
32
+ connection.add_index :schema_test_table, "foo", where: "state = 'active'"
33
+ end
34
+ end
35
+
36
+ it 'if index does not exist' do
37
+ assert_queries_match("IF NOT EXISTS (SELECT name FROM sysindexes WHERE name = 'index_schema_test_table_on_foo') " \
38
+ "CREATE INDEX [index_schema_test_table_on_foo] ON [schema_test_table] ([foo])") do
39
+ connection.add_index :schema_test_table, "foo", if_not_exists: true
40
+ end
41
+ end
42
+
43
+ it 'clustered index' do
44
+ assert_queries_match('CREATE CLUSTERED INDEX [index_schema_test_table_on_foo] ON [schema_test_table] ([foo])') do
45
+ connection.add_index :schema_test_table, "foo", type: :clustered
46
+ end
47
+ end
48
+
49
+ it 'nonclustered index' do
50
+ assert_queries_match('CREATE NONCLUSTERED INDEX [index_schema_test_table_on_foo] ON [schema_test_table] ([foo])') do
51
+ connection.add_index :schema_test_table, "foo", type: :nonclustered
52
+ end
53
+ end
54
+ end
55
+
56
+ describe 'collation' do
57
+ it "create column with NOT NULL and COLLATE" do
58
+ assert_nothing_raised do
59
+ connection.create_table :not_null_with_collation_table, force: true, id: false do |t|
60
+ t.text :not_null_text_with_collation, null: false, collation: "Latin1_General_CS_AS"
61
+ end
62
+ end
63
+ ensure
64
+ connection.drop_table :not_null_with_collation_table rescue nil
65
+ end
66
+ end
67
+
68
+ describe 'datetimeoffset precision' do
69
+ it 'valid precisions are correct' do
70
+ assert_nothing_raised do
71
+ connection.create_table :datetimeoffset_precisions do |t|
72
+ t.datetimeoffset :precision_default
73
+ t.datetimeoffset :precision_5, precision: 5
74
+ t.datetimeoffset :precision_7, precision: 7
75
+ end
76
+ end
77
+
78
+ columns = connection.columns("datetimeoffset_precisions")
79
+
80
+ assert_equal columns.find { |column| column.name == "precision_default" }.precision, 7
81
+ assert_equal columns.find { |column| column.name == "precision_5" }.precision, 5
82
+ assert_equal columns.find { |column| column.name == "precision_7" }.precision, 7
83
+ ensure
84
+ connection.drop_table :datetimeoffset_precisions rescue nil
85
+ end
86
+
87
+ it 'invalid precision raises exception' do
88
+ assert_raise(ActiveRecord::ActiveRecordError) do
89
+ connection.create_table :datetimeoffset_precisions do |t|
90
+ t.datetimeoffset :precision_8, precision: 8
91
+ end
92
+ end
93
+ ensure
94
+ connection.drop_table :datetimeoffset_precisions rescue nil
95
+ end
96
+ end
97
+
98
+ describe 'time precision' do
99
+ it 'valid precisions are correct' do
100
+ assert_nothing_raised do
101
+ connection.create_table :time_precisions do |t|
102
+ t.time :precision_default
103
+ t.time :precision_5, precision: 5
104
+ t.time :precision_7, precision: 7
105
+ end
106
+ end
107
+
108
+ columns = connection.columns("time_precisions")
109
+
110
+ assert_equal columns.find { |column| column.name == "precision_default" }.precision, 7
111
+ assert_equal columns.find { |column| column.name == "precision_5" }.precision, 5
112
+ assert_equal columns.find { |column| column.name == "precision_7" }.precision, 7
113
+ ensure
114
+ connection.drop_table :time_precisions rescue nil
115
+ end
116
+
117
+ it 'invalid precision raises exception' do
118
+ assert_raise(ActiveRecord::ActiveRecordError) do
119
+ connection.create_table :time_precisions do |t|
120
+ t.time :precision_8, precision: 8
121
+ end
122
+ end
123
+ ensure
124
+ connection.drop_table :time_precisions rescue nil
125
+ end
126
+ end
127
+ end