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.
- checksums.yaml +7 -0
- data/.github/issue_template.md +22 -0
- data/.github/workflows/ci.yml +32 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +69 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Dockerfile.ci +14 -0
- data/Gemfile +26 -0
- data/LICENSE.txt +21 -0
- data/README.md +104 -0
- data/RUNNING_UNIT_TESTS.md +38 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/activerecord-sqlserver-adapter-odbc-extended.gemspec +34 -0
- data/compose.ci.yaml +15 -0
- data/lib/active_record/connection_adapters/extended_sqlserver_adapter.rb +204 -0
- data/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb +41 -0
- data/lib/active_record/connection_adapters/sqlserver/odbc_database_statements.rb +234 -0
- data/lib/active_record/connection_adapters/sqlserver/type/binary_ext.rb +25 -0
- data/lib/activerecord-sqlserver-adapter-odbc-extended.rb +12 -0
- data/test/appveyor/dbsetup.ps1 +27 -0
- data/test/appveyor/dbsetup.sql +11 -0
- data/test/cases/active_schema_test_sqlserver.rb +127 -0
- data/test/cases/adapter_test_sqlserver.rb +648 -0
- data/test/cases/change_column_collation_test_sqlserver.rb +33 -0
- data/test/cases/change_column_null_test_sqlserver.rb +44 -0
- data/test/cases/coerced_tests.rb +2796 -0
- data/test/cases/column_test_sqlserver.rb +848 -0
- data/test/cases/connection_test_sqlserver.rb +138 -0
- data/test/cases/dbconsole.rb +19 -0
- data/test/cases/disconnected_test_sqlserver.rb +42 -0
- data/test/cases/eager_load_too_many_ids_test_sqlserver.rb +18 -0
- data/test/cases/enum_test_sqlserver.rb +49 -0
- data/test/cases/execute_procedure_test_sqlserver.rb +57 -0
- data/test/cases/fetch_test_sqlserver.rb +88 -0
- data/test/cases/fully_qualified_identifier_test_sqlserver.rb +72 -0
- data/test/cases/helper_sqlserver.rb +61 -0
- data/test/cases/migration_test_sqlserver.rb +144 -0
- data/test/cases/order_test_sqlserver.rb +153 -0
- data/test/cases/pessimistic_locking_test_sqlserver.rb +102 -0
- data/test/cases/primary_keys_test_sqlserver.rb +103 -0
- data/test/cases/rake_test_sqlserver.rb +198 -0
- data/test/cases/schema_dumper_test_sqlserver.rb +296 -0
- data/test/cases/schema_test_sqlserver.rb +111 -0
- data/test/cases/trigger_test_sqlserver.rb +51 -0
- data/test/cases/utils_test_sqlserver.rb +129 -0
- data/test/cases/uuid_test_sqlserver.rb +54 -0
- data/test/cases/view_test_sqlserver.rb +58 -0
- data/test/config.yml +38 -0
- data/test/debug.rb +16 -0
- data/test/fixtures/1px.gif +0 -0
- data/test/migrations/create_clients_and_change_column_collation.rb +19 -0
- data/test/migrations/create_clients_and_change_column_null.rb +25 -0
- data/test/migrations/transaction_table/1_table_will_never_be_created.rb +11 -0
- data/test/models/sqlserver/alien.rb +5 -0
- data/test/models/sqlserver/booking.rb +5 -0
- data/test/models/sqlserver/composite_pk.rb +9 -0
- data/test/models/sqlserver/customers_view.rb +5 -0
- data/test/models/sqlserver/datatype.rb +5 -0
- data/test/models/sqlserver/datatype_migration.rb +10 -0
- data/test/models/sqlserver/dollar_table_name.rb +5 -0
- data/test/models/sqlserver/edge_schema.rb +13 -0
- data/test/models/sqlserver/fk_has_fk.rb +5 -0
- data/test/models/sqlserver/fk_has_pk.rb +5 -0
- data/test/models/sqlserver/natural_pk_data.rb +6 -0
- data/test/models/sqlserver/natural_pk_int_data.rb +5 -0
- data/test/models/sqlserver/no_pk_data.rb +5 -0
- data/test/models/sqlserver/object_default.rb +5 -0
- data/test/models/sqlserver/quoted_table.rb +9 -0
- data/test/models/sqlserver/quoted_view_1.rb +5 -0
- data/test/models/sqlserver/quoted_view_2.rb +5 -0
- data/test/models/sqlserver/sst_memory.rb +5 -0
- data/test/models/sqlserver/sst_string_collation.rb +3 -0
- data/test/models/sqlserver/string_default.rb +5 -0
- data/test/models/sqlserver/string_defaults_big_view.rb +5 -0
- data/test/models/sqlserver/string_defaults_view.rb +5 -0
- data/test/models/sqlserver/table_with_spaces.rb +5 -0
- data/test/models/sqlserver/tinyint_pk.rb +5 -0
- data/test/models/sqlserver/trigger.rb +17 -0
- data/test/models/sqlserver/trigger_history.rb +5 -0
- data/test/models/sqlserver/upper.rb +5 -0
- data/test/models/sqlserver/uppered.rb +5 -0
- data/test/models/sqlserver/uuid.rb +5 -0
- data/test/schema/datatypes/2012.sql +56 -0
- data/test/schema/enable-in-memory-oltp.sql +81 -0
- data/test/schema/sqlserver_specific_schema.rb +363 -0
- data/test/support/coerceable_test_sqlserver.rb +55 -0
- data/test/support/connection_reflection.rb +32 -0
- data/test/support/core_ext/query_cache.rb +38 -0
- data/test/support/load_schema_sqlserver.rb +29 -0
- data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic.dump +0 -0
- data/test/support/marshal_compatibility_fixtures/SQLServer/rails_6_1_topic_associations.dump +0 -0
- data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic.dump +0 -0
- data/test/support/marshal_compatibility_fixtures/SQLServer/rails_7_1_topic_associations.dump +0 -0
- data/test/support/minitest_sqlserver.rb +3 -0
- data/test/support/paths_sqlserver.rb +50 -0
- data/test/support/query_assertions.rb +49 -0
- data/test/support/rake_helpers.rb +46 -0
- data/test/support/table_definition_sqlserver.rb +24 -0
- data/test/support/test_in_memory_oltp.rb +17 -0
- 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
|