activerecord-jdbcsqlserver-adapter 50.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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.travis.yml +27 -0
- data/CHANGELOG.md +124 -0
- data/CODE_OF_CONDUCT.md +31 -0
- data/Dockerfile +20 -0
- data/Gemfile +77 -0
- data/Guardfile +29 -0
- data/MIT-LICENSE +20 -0
- data/RAILS5-TODO.md +5 -0
- data/README.md +93 -0
- data/RUNNING_UNIT_TESTS.md +96 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/activerecord-jdbcsqlserver-adapter.gemspec +21 -0
- data/appveyor.yml +39 -0
- data/docker-compose.ci.yml +11 -0
- data/lib/active_record/connection_adapters/sqlserver/core_ext/active_record.rb +27 -0
- data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +25 -0
- data/lib/active_record/connection_adapters/sqlserver/core_ext/date_time.rb +58 -0
- data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +47 -0
- data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +4 -0
- data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +49 -0
- data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +362 -0
- data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +67 -0
- data/lib/active_record/connection_adapters/sqlserver/errors.rb +7 -0
- data/lib/active_record/connection_adapters/sqlserver/jdbc_overrides.rb +192 -0
- data/lib/active_record/connection_adapters/sqlserver/quoting.rb +99 -0
- data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +34 -0
- data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +16 -0
- data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +517 -0
- data/lib/active_record/connection_adapters/sqlserver/showplan.rb +66 -0
- data/lib/active_record/connection_adapters/sqlserver/showplan/printer_table.rb +66 -0
- data/lib/active_record/connection_adapters/sqlserver/showplan/printer_xml.rb +22 -0
- data/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb +20 -0
- data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +112 -0
- data/lib/active_record/connection_adapters/sqlserver/transaction.rb +64 -0
- data/lib/active_record/connection_adapters/sqlserver/type.rb +49 -0
- data/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb +19 -0
- data/lib/active_record/connection_adapters/sqlserver/type/binary.rb +21 -0
- data/lib/active_record/connection_adapters/sqlserver/type/boolean.rb +15 -0
- data/lib/active_record/connection_adapters/sqlserver/type/char.rb +32 -0
- data/lib/active_record/connection_adapters/sqlserver/type/data.rb +30 -0
- data/lib/active_record/connection_adapters/sqlserver/type/date.rb +61 -0
- data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +71 -0
- data/lib/active_record/connection_adapters/sqlserver/type/datetime2.rb +17 -0
- data/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb +23 -0
- data/lib/active_record/connection_adapters/sqlserver/type/decimal.rb +21 -0
- data/lib/active_record/connection_adapters/sqlserver/type/float.rb +19 -0
- data/lib/active_record/connection_adapters/sqlserver/type/integer.rb +15 -0
- data/lib/active_record/connection_adapters/sqlserver/type/json.rb +11 -0
- data/lib/active_record/connection_adapters/sqlserver/type/money.rb +25 -0
- data/lib/active_record/connection_adapters/sqlserver/type/real.rb +19 -0
- data/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb +15 -0
- data/lib/active_record/connection_adapters/sqlserver/type/small_money.rb +25 -0
- data/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb +29 -0
- data/lib/active_record/connection_adapters/sqlserver/type/string.rb +12 -0
- data/lib/active_record/connection_adapters/sqlserver/type/text.rb +19 -0
- data/lib/active_record/connection_adapters/sqlserver/type/time.rb +68 -0
- data/lib/active_record/connection_adapters/sqlserver/type/time_value_fractional.rb +93 -0
- data/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb +19 -0
- data/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb +25 -0
- data/lib/active_record/connection_adapters/sqlserver/type/unicode_char.rb +21 -0
- data/lib/active_record/connection_adapters/sqlserver/type/unicode_string.rb +12 -0
- data/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb +19 -0
- data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar.rb +26 -0
- data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb +24 -0
- data/lib/active_record/connection_adapters/sqlserver/type/uuid.rb +36 -0
- data/lib/active_record/connection_adapters/sqlserver/type/varbinary.rb +26 -0
- data/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb +24 -0
- data/lib/active_record/connection_adapters/sqlserver/type/varchar.rb +26 -0
- data/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb +24 -0
- data/lib/active_record/connection_adapters/sqlserver/utils.rb +146 -0
- data/lib/active_record/connection_adapters/sqlserver/version.rb +11 -0
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +445 -0
- data/lib/active_record/connection_adapters/sqlserver_column.rb +28 -0
- data/lib/active_record/jdbc_sqlserver_connection_methods.rb +31 -0
- data/lib/active_record/sqlserver_base.rb +16 -0
- data/lib/active_record/tasks/sqlserver_database_tasks.rb +131 -0
- data/lib/activerecord-jdbcsqlserver-adapter.rb +24 -0
- data/lib/activerecord-sqlserver-adapter.rb +1 -0
- data/lib/arel/visitors/sqlserver.rb +205 -0
- data/lib/arel_sqlserver.rb +3 -0
- data/test/appveyor/dbsetup.ps1 +27 -0
- data/test/appveyor/dbsetup.sql +11 -0
- data/test/bin/wait-for.sh +79 -0
- data/test/cases/adapter_test_sqlserver.rb +430 -0
- data/test/cases/coerced_tests.rb +845 -0
- data/test/cases/column_test_sqlserver.rb +812 -0
- data/test/cases/connection_test_sqlserver.rb +71 -0
- data/test/cases/execute_procedure_test_sqlserver.rb +45 -0
- data/test/cases/fetch_test_sqlserver.rb +57 -0
- data/test/cases/fully_qualified_identifier_test_sqlserver.rb +76 -0
- data/test/cases/helper_sqlserver.rb +44 -0
- data/test/cases/index_test_sqlserver.rb +47 -0
- data/test/cases/json_test_sqlserver.rb +32 -0
- data/test/cases/migration_test_sqlserver.rb +61 -0
- data/test/cases/order_test_sqlserver.rb +147 -0
- data/test/cases/pessimistic_locking_test_sqlserver.rb +94 -0
- data/test/cases/rake_test_sqlserver.rb +169 -0
- data/test/cases/schema_dumper_test_sqlserver.rb +234 -0
- data/test/cases/schema_test_sqlserver.rb +54 -0
- data/test/cases/scratchpad_test_sqlserver.rb +8 -0
- data/test/cases/showplan_test_sqlserver.rb +65 -0
- data/test/cases/specific_schema_test_sqlserver.rb +180 -0
- data/test/cases/transaction_test_sqlserver.rb +91 -0
- data/test/cases/utils_test_sqlserver.rb +129 -0
- data/test/cases/uuid_test_sqlserver.rb +49 -0
- data/test/config.yml +38 -0
- data/test/debug.rb +14 -0
- data/test/fixtures/1px.gif +0 -0
- data/test/migrations/transaction_table/1_table_will_never_be_created.rb +11 -0
- data/test/models/sqlserver/booking.rb +3 -0
- data/test/models/sqlserver/customers_view.rb +3 -0
- data/test/models/sqlserver/datatype.rb +3 -0
- data/test/models/sqlserver/datatype_migration.rb +8 -0
- data/test/models/sqlserver/dollar_table_name.rb +3 -0
- data/test/models/sqlserver/dot_table_name.rb +3 -0
- data/test/models/sqlserver/edge_schema.rb +13 -0
- data/test/models/sqlserver/fk_has_fk.rb +3 -0
- data/test/models/sqlserver/fk_has_pk.rb +3 -0
- data/test/models/sqlserver/natural_pk_data.rb +4 -0
- data/test/models/sqlserver/natural_pk_int_data.rb +3 -0
- data/test/models/sqlserver/no_pk_data.rb +3 -0
- data/test/models/sqlserver/object_default.rb +3 -0
- data/test/models/sqlserver/quoted_table.rb +7 -0
- data/test/models/sqlserver/quoted_view_1.rb +3 -0
- data/test/models/sqlserver/quoted_view_2.rb +3 -0
- data/test/models/sqlserver/sst_memory.rb +3 -0
- data/test/models/sqlserver/string_default.rb +3 -0
- data/test/models/sqlserver/string_defaults_big_view.rb +3 -0
- data/test/models/sqlserver/string_defaults_view.rb +3 -0
- data/test/models/sqlserver/tinyint_pk.rb +3 -0
- data/test/models/sqlserver/upper.rb +3 -0
- data/test/models/sqlserver/uppered.rb +3 -0
- data/test/models/sqlserver/uuid.rb +3 -0
- data/test/schema/datatypes/2012.sql +55 -0
- data/test/schema/enable-in-memory-oltp.sql +81 -0
- data/test/schema/sqlserver_specific_schema.rb +238 -0
- data/test/support/coerceable_test_sqlserver.rb +49 -0
- data/test/support/connection_reflection.rb +34 -0
- data/test/support/load_schema_sqlserver.rb +29 -0
- data/test/support/minitest_sqlserver.rb +1 -0
- data/test/support/paths_sqlserver.rb +50 -0
- data/test/support/rake_helpers.rb +41 -0
- data/test/support/sql_counter_sqlserver.rb +28 -0
- data/test/support/test_in_memory_oltp.rb +15 -0
- metadata +310 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
module ConnectionAdapters
|
|
3
|
+
module SQLServer
|
|
4
|
+
module DatabaseTasks
|
|
5
|
+
|
|
6
|
+
def create_database(database, options = {})
|
|
7
|
+
name = SQLServer::Utils.extract_identifiers(database)
|
|
8
|
+
db_options = create_database_options(options)
|
|
9
|
+
edition_options = create_database_edition_options(options)
|
|
10
|
+
do_execute "CREATE DATABASE #{name} #{db_options} #{edition_options}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def drop_database(database)
|
|
14
|
+
name = SQLServer::Utils.extract_identifiers(database)
|
|
15
|
+
do_execute "DROP DATABASE #{name}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def current_database
|
|
19
|
+
select_value 'SELECT DB_NAME()'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def charset
|
|
23
|
+
select_value "SELECT DATABASEPROPERTYEX(DB_NAME(), 'SqlCharSetName')"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def collation
|
|
27
|
+
@collation ||= select_value "SELECT DATABASEPROPERTYEX(DB_NAME(), 'Collation')"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def create_database_options(options={})
|
|
33
|
+
keys = [:collate]
|
|
34
|
+
copts = @connection_options
|
|
35
|
+
options = {
|
|
36
|
+
collate: copts[:collation]
|
|
37
|
+
}.merge(options.symbolize_keys).select { |_, v|
|
|
38
|
+
v.present?
|
|
39
|
+
}.slice(*keys).map { |k,v|
|
|
40
|
+
"#{k.to_s.upcase} #{v}"
|
|
41
|
+
}.join(' ')
|
|
42
|
+
options
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def create_database_edition_options(options={})
|
|
46
|
+
keys = [:maxsize, :edition, :service_objective]
|
|
47
|
+
copts = @connection_options
|
|
48
|
+
edition_options = {
|
|
49
|
+
maxsize: copts[:azure_maxsize],
|
|
50
|
+
edition: copts[:azure_edition],
|
|
51
|
+
service_objective: copts[:azure_service_objective]
|
|
52
|
+
}.merge(options.symbolize_keys).select { |_, v|
|
|
53
|
+
v.present?
|
|
54
|
+
}.slice(*keys).map { |k,v|
|
|
55
|
+
"#{k.to_s.upcase} = #{v}"
|
|
56
|
+
}.join(', ')
|
|
57
|
+
edition_options = "( #{edition_options} )" if edition_options.present?
|
|
58
|
+
edition_options
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
module ConnectionAdapters
|
|
3
|
+
module SQLServer
|
|
4
|
+
module JDBCOverrides
|
|
5
|
+
|
|
6
|
+
# @Override
|
|
7
|
+
# Needed to reapply this since the jdbc abstract versions don't do the check and
|
|
8
|
+
# end up overriding the sqlserver gem's version
|
|
9
|
+
def exec_insert(sql, name, binds, pk = nil, _sequence_name = nil)
|
|
10
|
+
if id_insert_table_name = exec_insert_requires_identity?(sql, pk, binds)
|
|
11
|
+
with_identity_insert_enabled(id_insert_table_name) { super }
|
|
12
|
+
else
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @Override
|
|
18
|
+
# Needed to reapply this since the jdbc abstract versions don't do the check and
|
|
19
|
+
# end up overriding the sqlserver gem's version
|
|
20
|
+
def execute(sql, name = nil)
|
|
21
|
+
if id_insert_table_name = query_requires_identity_insert?(sql)
|
|
22
|
+
with_identity_insert_enabled(id_insert_table_name) { super }
|
|
23
|
+
else
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# TODO Move to java for potential perf boost
|
|
29
|
+
def execute_procedure(proc_name, *variables, &block)
|
|
30
|
+
vars = if variables.any? && variables.first.is_a?(Hash)
|
|
31
|
+
variables.first.map { |k, v| "@#{k} = #{quote(v)}" }
|
|
32
|
+
else
|
|
33
|
+
variables.map { |v| quote(v) }
|
|
34
|
+
end.join(', ')
|
|
35
|
+
sql = "EXEC #{proc_name} #{vars}".strip
|
|
36
|
+
log(sql, 'Execute Procedure') do
|
|
37
|
+
result = @connection.execute(sql)
|
|
38
|
+
|
|
39
|
+
return [] unless result
|
|
40
|
+
|
|
41
|
+
if result.is_a?(Array)
|
|
42
|
+
result.map! do |res|
|
|
43
|
+
process_execute_procedure_result(res, &block)
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
result = process_execute_procedure_result(result, &block)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
result
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @Override
|
|
54
|
+
# MSSQL does not return query plans for prepared statements, so we have to unprepare them
|
|
55
|
+
# SQLServer gem handles this by overridding exec_explain but that doesn't correctly unprepare them for our needs
|
|
56
|
+
def explain(arel, binds = [])
|
|
57
|
+
arel = ActiveRecord::Base.send(:replace_bind_variables, arel, binds.map(&:value_for_database))
|
|
58
|
+
sql = to_sql(arel)
|
|
59
|
+
result = with_showplan_on { execute(sql, 'EXPLAIN') }
|
|
60
|
+
if result.is_a?(Array)
|
|
61
|
+
# We got back multiple result sets but the printer expects them to all be in one
|
|
62
|
+
main_result = result[0]
|
|
63
|
+
result.each_with_index do |result_obj, i|
|
|
64
|
+
next if i == 0
|
|
65
|
+
main_result.rows.concat(result_obj.rows)
|
|
66
|
+
end
|
|
67
|
+
result = main_result
|
|
68
|
+
end
|
|
69
|
+
printer = showplan_printer.new(result)
|
|
70
|
+
printer.pp
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @see ActiveRecord::ConnectionAdapters::JdbcAdapter#jdbc_connection_class
|
|
74
|
+
def jdbc_connection_class(_spec)
|
|
75
|
+
::ActiveRecord::ConnectionAdapters::MSSQLJdbcConnection
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Override
|
|
79
|
+
# Since we aren't passing dates/times around as strings we need to
|
|
80
|
+
# process them here, just making sure they are a string
|
|
81
|
+
def quoted_date(value)
|
|
82
|
+
super.to_s
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @Override
|
|
86
|
+
def reset!
|
|
87
|
+
clear_cache!
|
|
88
|
+
reset_transaction
|
|
89
|
+
@connection.rollback # Have to deal with rollbacks differently than the SQLServer gem
|
|
90
|
+
@connection.configure_connection
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @Overwrite
|
|
94
|
+
# Had some special logic and skipped using gem's internal query methods
|
|
95
|
+
def select_rows(sql, name = nil, binds = [])
|
|
96
|
+
|
|
97
|
+
# In some cases the limit is converted to a `TOP(1)` but the bind parameter is still in the array
|
|
98
|
+
if !binds.empty? && sql.include?('TOP(1)')
|
|
99
|
+
binds = binds.delete_if {|b| b.name == 'LIMIT' }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
exec_query(sql, name, binds).rows
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Have to reset this because the default arjdbc functionality is to return false unless a level is passed in
|
|
106
|
+
def supports_transaction_isolation?
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
protected
|
|
111
|
+
|
|
112
|
+
# Called to set any connection specific settings that aren't defined ahead of time
|
|
113
|
+
def configure_connection
|
|
114
|
+
# For sql server 2008+ we want it to send an actual time otherwise comparisons with time columns don't work
|
|
115
|
+
@connection.connection.setSendTimeAsDatetime(false)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @Overwrite
|
|
119
|
+
# Makes a connection before configuring it
|
|
120
|
+
# @connection actually gets defined and then the connect method in the sqlserver gem overrides it
|
|
121
|
+
# This can probably be fixed with a patch to the main gem
|
|
122
|
+
def connect
|
|
123
|
+
@spid = @connection.execute('SELECT @@SPID').first.values.first
|
|
124
|
+
@version_year = version_year # Not sure if this is necessary but kept it this way because the gem has it this way
|
|
125
|
+
configure_connection
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @Overwrite
|
|
129
|
+
# This ends up as a no-op without the override
|
|
130
|
+
def do_execute(sql, name = 'SQL')
|
|
131
|
+
execute(sql, name)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @Overwrite
|
|
135
|
+
# Overriding this in case it gets used in places that we don't override by default
|
|
136
|
+
def raw_connection_do(sql)
|
|
137
|
+
@connection.execute(sql)
|
|
138
|
+
ensure
|
|
139
|
+
@update_sql = false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# @Overwrite
|
|
143
|
+
def sp_executesql(sql, name, binds, _options = {})
|
|
144
|
+
exec_query(sql, name, binds)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @Overwrite
|
|
148
|
+
# Prevents turning an insert statement into a query with results
|
|
149
|
+
# Slightly adjusted since we know there should always be a table name in the sql
|
|
150
|
+
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
|
|
151
|
+
pk = primary_key(get_table_name(sql)) if pk.nil?
|
|
152
|
+
[sql, binds, pk, sequence_name]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @Override
|
|
156
|
+
def translate_exception(exception, message)
|
|
157
|
+
return ActiveRecord::ValueTooLong.new(message) if exception.message.include?('java.sql.DataTruncation')
|
|
158
|
+
super
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @Overwrite
|
|
162
|
+
# Made it so we don't use the internal calls from the gem
|
|
163
|
+
def version_year
|
|
164
|
+
return @version_year if defined?(@version_year)
|
|
165
|
+
@version_year = begin
|
|
166
|
+
vstring = select_value('SELECT @@version').to_s
|
|
167
|
+
return 2016 if vstring =~ /vNext/
|
|
168
|
+
/SQL Server (\d+)/.match(vstring).to_a.last.to_s.to_i
|
|
169
|
+
rescue Exception => e
|
|
170
|
+
2016
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private
|
|
175
|
+
|
|
176
|
+
def _quote(value)
|
|
177
|
+
return value.quoted if value.is_a?(SQLServer::CoreExt::Time) || value.is_a?(SQLServer::CoreExt::DateTime)
|
|
178
|
+
super
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def process_execute_procedure_result(result)
|
|
182
|
+
result.map do |row|
|
|
183
|
+
obj = row.with_indifferent_access
|
|
184
|
+
yield(obj) if block_given?
|
|
185
|
+
obj
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
module ConnectionAdapters
|
|
3
|
+
module SQLServer
|
|
4
|
+
module Quoting
|
|
5
|
+
|
|
6
|
+
QUOTED_TRUE = '1'.freeze
|
|
7
|
+
QUOTED_FALSE = '0'.freeze
|
|
8
|
+
QUOTED_STRING_PREFIX = 'N'.freeze
|
|
9
|
+
|
|
10
|
+
def fetch_type_metadata(sql_type, sqlserver_options = {})
|
|
11
|
+
cast_type = lookup_cast_type(sql_type)
|
|
12
|
+
SQLServer::SqlTypeMetadata.new(
|
|
13
|
+
sql_type: sql_type,
|
|
14
|
+
type: cast_type.type,
|
|
15
|
+
limit: cast_type.limit,
|
|
16
|
+
precision: cast_type.precision,
|
|
17
|
+
scale: cast_type.scale,
|
|
18
|
+
sqlserver_options: sqlserver_options
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def quote_string(s)
|
|
23
|
+
SQLServer::Utils.quote_string(s)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def quote_string_single(s)
|
|
27
|
+
SQLServer::Utils.quote_string_single(s)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def quote_string_single_national(s)
|
|
31
|
+
SQLServer::Utils.quote_string_single_national(s)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def quote_column_name(name)
|
|
35
|
+
SQLServer::Utils.extract_identifiers(name).quoted
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def quote_default_expression(value, column)
|
|
39
|
+
cast_type = lookup_cast_type(column.sql_type)
|
|
40
|
+
if cast_type.type == :uuid && value =~ /\(\)/
|
|
41
|
+
value
|
|
42
|
+
else
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def quoted_true
|
|
48
|
+
QUOTED_TRUE
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def unquoted_true
|
|
52
|
+
1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def quoted_false
|
|
56
|
+
QUOTED_FALSE
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def unquoted_false
|
|
60
|
+
0
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def quoted_date(value)
|
|
64
|
+
if value.acts_like?(:date)
|
|
65
|
+
Type::Date.new.serialize(value)
|
|
66
|
+
else value.acts_like?(:time)
|
|
67
|
+
Type::DateTime.new.serialize(value)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def _quote(value)
|
|
75
|
+
case value
|
|
76
|
+
when Type::Binary::Data
|
|
77
|
+
"0x#{value.hex}"
|
|
78
|
+
when ActiveRecord::Type::SQLServer::Data
|
|
79
|
+
value.quoted
|
|
80
|
+
when String, ActiveSupport::Multibyte::Chars
|
|
81
|
+
"#{QUOTED_STRING_PREFIX}#{super}"
|
|
82
|
+
else
|
|
83
|
+
super
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def _type_cast(value)
|
|
88
|
+
case value
|
|
89
|
+
when ActiveRecord::Type::SQLServer::Data
|
|
90
|
+
value.to_s
|
|
91
|
+
else
|
|
92
|
+
super
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
module ConnectionAdapters
|
|
3
|
+
module SQLServer
|
|
4
|
+
class SchemaCreation < AbstractAdapter::SchemaCreation
|
|
5
|
+
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def visit_TableDefinition(o)
|
|
9
|
+
if o.as
|
|
10
|
+
table_name = quote_table_name(o.temporary ? "##{o.name}" : o.name)
|
|
11
|
+
projections, source = @conn.to_sql(o.as).match(%r{SELECT\s+(.*)?\s+FROM\s+(.*)?}).captures
|
|
12
|
+
select_into = "SELECT #{projections} INTO #{table_name} FROM #{source}"
|
|
13
|
+
else
|
|
14
|
+
o.instance_variable_set :@as, nil
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def action_sql(action, dependency)
|
|
20
|
+
case dependency
|
|
21
|
+
when :restrict
|
|
22
|
+
raise ArgumentError, <<-MSG.strip_heredoc
|
|
23
|
+
'#{dependency}' is not supported for :on_update or :on_delete.
|
|
24
|
+
Supported values are: :nullify, :cascade
|
|
25
|
+
MSG
|
|
26
|
+
else
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
module ConnectionAdapters
|
|
3
|
+
module SQLServer
|
|
4
|
+
module SchemaDumper
|
|
5
|
+
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def schema_collation(column)
|
|
9
|
+
return unless column.collation
|
|
10
|
+
column.collation if column.collation != collation
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
module ActiveRecord
|
|
2
|
+
module ConnectionAdapters
|
|
3
|
+
module SQLServer
|
|
4
|
+
module SchemaStatements
|
|
5
|
+
|
|
6
|
+
def native_database_types
|
|
7
|
+
@native_database_types ||= initialize_native_database_types.freeze
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def tables(name = nil)
|
|
11
|
+
ActiveSupport::Deprecation.warn 'Passing arguments to #tables is deprecated without replacement.' if name
|
|
12
|
+
tables_sql('BASE TABLE')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def table_exists?(table_name)
|
|
16
|
+
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
|
17
|
+
#table_exists? currently checks both tables and views.
|
|
18
|
+
This behavior is deprecated and will be changed with Rails 5.1 to only check tables.
|
|
19
|
+
Use #data_source_exists? instead.
|
|
20
|
+
MSG
|
|
21
|
+
data_source_exists?(table_name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def data_source_exists?(table_name)
|
|
25
|
+
return false if table_name.blank?
|
|
26
|
+
unquoted_table_name = SQLServer::Utils.extract_identifiers(table_name).object
|
|
27
|
+
super(unquoted_table_name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def views
|
|
31
|
+
tables_sql('VIEW')
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def view_exists?(table_name)
|
|
35
|
+
identifier = SQLServer::Utils.extract_identifiers(table_name)
|
|
36
|
+
super(identifier.object)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_table(table_name, comment: nil, **options)
|
|
40
|
+
res = super
|
|
41
|
+
clear_cache!
|
|
42
|
+
res
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def drop_table(table_name, options = {})
|
|
46
|
+
if options[:if_exists] && @version_year != 2016
|
|
47
|
+
execute "IF EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = #{quote(table_name)}) DROP TABLE #{quote_table_name(table_name)}"
|
|
48
|
+
else
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def indexes(table_name, name = nil)
|
|
54
|
+
data = (select("EXEC sp_helpindex #{quote(table_name)}", name) || []) rescue [] # JDBC returns nil instead of an array or erring out for no results
|
|
55
|
+
data.reduce([]) do |indexes, index|
|
|
56
|
+
index = index.with_indifferent_access
|
|
57
|
+
if index[:index_description] =~ /primary key/
|
|
58
|
+
indexes
|
|
59
|
+
else
|
|
60
|
+
name = index[:index_name]
|
|
61
|
+
unique = index[:index_description] =~ /unique/
|
|
62
|
+
where = select_value("SELECT [filter_definition] FROM sys.indexes WHERE name = #{quote(name)}")
|
|
63
|
+
columns = index[:index_keys].split(',').map do |column|
|
|
64
|
+
column.strip!
|
|
65
|
+
column.gsub! '(-)', '' if column.ends_with?('(-)')
|
|
66
|
+
column
|
|
67
|
+
end
|
|
68
|
+
indexes << IndexDefinition.new(table_name, name, unique, columns, nil, nil, where)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def columns(table_name)
|
|
74
|
+
return [] if table_name.blank?
|
|
75
|
+
column_definitions(table_name).map do |ci|
|
|
76
|
+
sqlserver_options = ci.slice :ordinal_position, :is_primary, :is_identity
|
|
77
|
+
sql_type_metadata = fetch_type_metadata ci[:type], sqlserver_options
|
|
78
|
+
new_column(
|
|
79
|
+
ci[:name],
|
|
80
|
+
ci[:default_value],
|
|
81
|
+
sql_type_metadata,
|
|
82
|
+
ci[:null],
|
|
83
|
+
ci[:table_name],
|
|
84
|
+
ci[:default_function],
|
|
85
|
+
ci[:collation],
|
|
86
|
+
nil,
|
|
87
|
+
sqlserver_options
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil, comment = nil, sqlserver_options = {})
|
|
93
|
+
SQLServerColumn.new(
|
|
94
|
+
name,
|
|
95
|
+
default,
|
|
96
|
+
sql_type_metadata,
|
|
97
|
+
null, table_name,
|
|
98
|
+
default_function,
|
|
99
|
+
collation,
|
|
100
|
+
comment,
|
|
101
|
+
sqlserver_options
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def primary_keys(table_name)
|
|
106
|
+
primaries = schema_cache.columns(table_name).select(&:is_primary?).map(&:name)
|
|
107
|
+
primaries.present? ? primaries : identity_columns(table_name).map(&:name)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def rename_table(table_name, new_name)
|
|
111
|
+
do_execute "EXEC sp_rename '#{table_name}', '#{new_name}'"
|
|
112
|
+
rename_table_indexes(table_name, new_name)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def remove_column(table_name, column_name, type = nil, options = {})
|
|
116
|
+
raise ArgumentError.new('You must specify at least one column name. Example: remove_column(:people, :first_name)') if column_name.is_a? Array
|
|
117
|
+
remove_check_constraints(table_name, column_name)
|
|
118
|
+
remove_default_constraint(table_name, column_name)
|
|
119
|
+
remove_indexes(table_name, column_name)
|
|
120
|
+
do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def change_column(table_name, column_name, type, options = {})
|
|
124
|
+
sql_commands = []
|
|
125
|
+
indexes = []
|
|
126
|
+
column_object = schema_cache.columns(table_name).find { |c| c.name.to_s == column_name.to_s }
|
|
127
|
+
if options_include_default?(options) || (column_object && column_object.type != type.to_sym)
|
|
128
|
+
remove_default_constraint(table_name, column_name)
|
|
129
|
+
indexes = indexes(table_name).select { |index| index.columns.include?(column_name.to_s) }
|
|
130
|
+
remove_indexes(table_name, column_name)
|
|
131
|
+
end
|
|
132
|
+
sql_commands << "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(options[:default], column_object)} WHERE #{quote_column_name(column_name)} IS NULL" if !options[:null].nil? && options[:null] == false && !options[:default].nil?
|
|
133
|
+
sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
|
134
|
+
sql_commands[-1] << ' NOT NULL' if !options[:null].nil? && options[:null] == false
|
|
135
|
+
if options_include_default?(options)
|
|
136
|
+
sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote_default_expression(options[:default], column_object)} FOR #{quote_column_name(column_name)}"
|
|
137
|
+
end
|
|
138
|
+
# Add any removed indexes back
|
|
139
|
+
indexes.each do |index|
|
|
140
|
+
sql_commands << "CREATE INDEX #{quote_table_name(index.name)} ON #{quote_table_name(table_name)} (#{index.columns.map { |c| quote_column_name(c) }.join(', ')})"
|
|
141
|
+
end
|
|
142
|
+
sql_commands.each { |c| do_execute(c) }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def change_column_default(table_name, column_name, default_or_changes)
|
|
146
|
+
clear_cache!
|
|
147
|
+
column = column_for(table_name, column_name)
|
|
148
|
+
return unless column
|
|
149
|
+
remove_default_constraint(table_name, column_name)
|
|
150
|
+
default = extract_new_default_value(default_or_changes)
|
|
151
|
+
do_execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote_default_expression(default, column)} FOR #{quote_column_name(column_name)}"
|
|
152
|
+
clear_cache!
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def rename_column(table_name, column_name, new_column_name)
|
|
156
|
+
clear_cache!
|
|
157
|
+
identifier = SQLServer::Utils.extract_identifiers("#{table_name}.#{column_name}")
|
|
158
|
+
execute_procedure :sp_rename, identifier.quoted, new_column_name, 'COLUMN'
|
|
159
|
+
rename_column_indexes(table_name, column_name, new_column_name)
|
|
160
|
+
clear_cache!
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def rename_index(table_name, old_name, new_name)
|
|
164
|
+
raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" if new_name.length > allowed_index_name_length
|
|
165
|
+
identifier = SQLServer::Utils.extract_identifiers("#{table_name}.#{old_name}")
|
|
166
|
+
execute_procedure :sp_rename, identifier.quoted, new_name, 'INDEX'
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def remove_index!(table_name, index_name)
|
|
170
|
+
do_execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def foreign_keys(table_name)
|
|
174
|
+
identifier = SQLServer::Utils.extract_identifiers(table_name)
|
|
175
|
+
fk_info = execute_procedure :sp_fkeys, nil, identifier.schema, nil, identifier.object, identifier.schema
|
|
176
|
+
fk_info.map do |row|
|
|
177
|
+
from_table = identifier.object
|
|
178
|
+
to_table = row['PKTABLE_NAME']
|
|
179
|
+
options = {
|
|
180
|
+
name: row['FK_NAME'],
|
|
181
|
+
column: row['FKCOLUMN_NAME'],
|
|
182
|
+
primary_key: row['PKCOLUMN_NAME'],
|
|
183
|
+
on_update: extract_foreign_key_action('update', row['FK_NAME']),
|
|
184
|
+
on_delete: extract_foreign_key_action('delete', row['FK_NAME'])
|
|
185
|
+
}
|
|
186
|
+
ForeignKeyDefinition.new from_table, to_table, options
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def extract_foreign_key_action(action, fk_name)
|
|
191
|
+
case select_value("SELECT #{action}_referential_action_desc FROM sys.foreign_keys WHERE name = '#{fk_name}'")
|
|
192
|
+
when 'CASCADE' then :cascade
|
|
193
|
+
when 'SET_NULL' then :nullify
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
|
198
|
+
type_limitable = %w(string integer float char nchar varchar nvarchar).include?(type.to_s)
|
|
199
|
+
limit = nil unless type_limitable
|
|
200
|
+
case type.to_s
|
|
201
|
+
when 'integer'
|
|
202
|
+
case limit
|
|
203
|
+
when 1..2 then 'smallint'
|
|
204
|
+
when 3..4, nil then 'integer'
|
|
205
|
+
when 5..8 then 'bigint'
|
|
206
|
+
else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
|
|
207
|
+
end
|
|
208
|
+
when 'datetime2'
|
|
209
|
+
column_type_sql = super
|
|
210
|
+
if precision
|
|
211
|
+
if (0..7) === precision
|
|
212
|
+
column_type_sql << "(#{precision})"
|
|
213
|
+
else
|
|
214
|
+
raise(ActiveRecordError, "The dattime2 type has precision of #{precision}. The allowed range of precision is from 0 to 7")
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
column_type_sql
|
|
218
|
+
else
|
|
219
|
+
super
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def columns_for_distinct(columns, orders)
|
|
224
|
+
order_columns = orders.reject(&:blank?).map{ |s|
|
|
225
|
+
s = s.to_sql unless s.is_a?(String)
|
|
226
|
+
s.gsub(/\s+(?:ASC|DESC)\b/i, '')
|
|
227
|
+
.gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, '')
|
|
228
|
+
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
|
|
229
|
+
[super, *order_columns].join(', ')
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def update_table_definition(table_name, base)
|
|
233
|
+
SQLServer::Table.new(table_name, base)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def change_column_null(table_name, column_name, allow_null, default = nil)
|
|
237
|
+
table_id = SQLServer::Utils.extract_identifiers(table_name)
|
|
238
|
+
column_id = SQLServer::Utils.extract_identifiers(column_name)
|
|
239
|
+
column = detect_column_for! table_name, column_name
|
|
240
|
+
if !allow_null.nil? && allow_null == false && !default.nil?
|
|
241
|
+
do_execute("UPDATE #{table_id} SET #{column_id}=#{quote(default)} WHERE #{column_id} IS NULL")
|
|
242
|
+
end
|
|
243
|
+
sql = "ALTER TABLE #{table_id} ALTER COLUMN #{column_id} #{type_to_sql column.type, column.limit, column.precision, column.scale}"
|
|
244
|
+
sql << ' NOT NULL' if !allow_null.nil? && allow_null == false
|
|
245
|
+
do_execute sql
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
protected
|
|
250
|
+
|
|
251
|
+
# === SQLServer Specific ======================================== #
|
|
252
|
+
|
|
253
|
+
def initialize_native_database_types
|
|
254
|
+
{
|
|
255
|
+
primary_key: 'int NOT NULL IDENTITY(1,1) PRIMARY KEY',
|
|
256
|
+
primary_key_nonclustered: 'int NOT NULL IDENTITY(1,1) PRIMARY KEY NONCLUSTERED',
|
|
257
|
+
integer: { name: 'int', limit: 4 },
|
|
258
|
+
bigint: { name: 'bigint' },
|
|
259
|
+
boolean: { name: 'bit' },
|
|
260
|
+
decimal: { name: 'decimal' },
|
|
261
|
+
money: { name: 'money' },
|
|
262
|
+
smallmoney: { name: 'smallmoney' },
|
|
263
|
+
float: { name: 'float' },
|
|
264
|
+
real: { name: 'real' },
|
|
265
|
+
date: { name: 'date' },
|
|
266
|
+
datetime: { name: 'datetime' },
|
|
267
|
+
datetime2: { name: 'datetime2' },
|
|
268
|
+
datetimeoffset: { name: 'datetimeoffset' },
|
|
269
|
+
smalldatetime: { name: 'smalldatetime' },
|
|
270
|
+
timestamp: { name: 'datetime' },
|
|
271
|
+
time: { name: 'time' },
|
|
272
|
+
char: { name: 'char' },
|
|
273
|
+
varchar: { name: 'varchar', limit: 8000 },
|
|
274
|
+
varchar_max: { name: 'varchar(max)' },
|
|
275
|
+
text_basic: { name: 'text' },
|
|
276
|
+
nchar: { name: 'nchar' },
|
|
277
|
+
string: { name: 'nvarchar', limit: 4000 },
|
|
278
|
+
text: { name: 'nvarchar(max)' },
|
|
279
|
+
ntext: { name: 'ntext' },
|
|
280
|
+
binary_basic: { name: 'binary' },
|
|
281
|
+
varbinary: { name: 'varbinary', limit: 8000 },
|
|
282
|
+
binary: { name: 'varbinary(max)' },
|
|
283
|
+
uuid: { name: 'uniqueidentifier' },
|
|
284
|
+
ss_timestamp: { name: 'timestamp' },
|
|
285
|
+
json: { name: 'nvarchar(max)' }
|
|
286
|
+
}
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def tables_sql(type)
|
|
290
|
+
tn = lowercase_schema_reflection_sql 'TABLE_NAME'
|
|
291
|
+
sql = "SELECT #{tn} FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = '#{type}' ORDER BY TABLE_NAME"
|
|
292
|
+
select_values sql, 'SCHEMA'
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
COLUMN_DEFINITION_BIND_STRING_0 = defined?(JRUBY_VERSION) ? '?' : '@0'
|
|
296
|
+
COLUMN_DEFINITION_BIND_STRING_1 = defined?(JRUBY_VERSION) ? '?' : '@1'
|
|
297
|
+
|
|
298
|
+
def column_definitions(table_name)
|
|
299
|
+
identifier = if database_prefix_remote_server?
|
|
300
|
+
SQLServer::Utils.extract_identifiers("#{database_prefix}#{table_name}")
|
|
301
|
+
else
|
|
302
|
+
SQLServer::Utils.extract_identifiers(table_name)
|
|
303
|
+
end
|
|
304
|
+
database = identifier.fully_qualified_database_quoted
|
|
305
|
+
view_exists = view_exists?(table_name)
|
|
306
|
+
view_tblnm = view_table_name(table_name) if view_exists
|
|
307
|
+
sql = %{
|
|
308
|
+
SELECT DISTINCT
|
|
309
|
+
#{lowercase_schema_reflection_sql('columns.TABLE_NAME')} AS table_name,
|
|
310
|
+
#{lowercase_schema_reflection_sql('columns.COLUMN_NAME')} AS name,
|
|
311
|
+
columns.DATA_TYPE AS type,
|
|
312
|
+
columns.COLUMN_DEFAULT AS default_value,
|
|
313
|
+
columns.NUMERIC_SCALE AS numeric_scale,
|
|
314
|
+
columns.NUMERIC_PRECISION AS numeric_precision,
|
|
315
|
+
columns.DATETIME_PRECISION AS datetime_precision,
|
|
316
|
+
columns.COLLATION_NAME AS [collation],
|
|
317
|
+
columns.ordinal_position,
|
|
318
|
+
CASE
|
|
319
|
+
WHEN columns.DATA_TYPE IN ('nchar','nvarchar','char','varchar') THEN columns.CHARACTER_MAXIMUM_LENGTH
|
|
320
|
+
ELSE COL_LENGTH('#{database}.'+columns.TABLE_SCHEMA+'.'+columns.TABLE_NAME, columns.COLUMN_NAME)
|
|
321
|
+
END AS [length],
|
|
322
|
+
CASE
|
|
323
|
+
WHEN columns.IS_NULLABLE = 'YES' THEN 1
|
|
324
|
+
ELSE NULL
|
|
325
|
+
END AS [is_nullable],
|
|
326
|
+
CASE
|
|
327
|
+
WHEN KCU.COLUMN_NAME IS NOT NULL AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY' THEN 1
|
|
328
|
+
ELSE NULL
|
|
329
|
+
END AS [is_primary],
|
|
330
|
+
c.is_identity AS [is_identity]
|
|
331
|
+
FROM #{database}.INFORMATION_SCHEMA.COLUMNS columns
|
|
332
|
+
LEFT OUTER JOIN #{database}.INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC
|
|
333
|
+
ON TC.TABLE_NAME = columns.TABLE_NAME
|
|
334
|
+
AND TC.TABLE_SCHEMA = columns.TABLE_SCHEMA
|
|
335
|
+
AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY'
|
|
336
|
+
LEFT OUTER JOIN #{database}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
|
|
337
|
+
ON KCU.COLUMN_NAME = columns.COLUMN_NAME
|
|
338
|
+
AND KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
|
|
339
|
+
AND KCU.CONSTRAINT_CATALOG = TC.CONSTRAINT_CATALOG
|
|
340
|
+
AND KCU.CONSTRAINT_SCHEMA = TC.CONSTRAINT_SCHEMA
|
|
341
|
+
INNER JOIN #{database}.sys.schemas AS s
|
|
342
|
+
ON s.name = columns.TABLE_SCHEMA
|
|
343
|
+
AND s.schema_id = s.schema_id
|
|
344
|
+
INNER JOIN #{database}.sys.objects AS o
|
|
345
|
+
ON s.schema_id = o.schema_id
|
|
346
|
+
AND o.is_ms_shipped = 0
|
|
347
|
+
AND o.type IN ('U', 'V')
|
|
348
|
+
AND o.name = columns.TABLE_NAME
|
|
349
|
+
INNER JOIN #{database}.sys.columns AS c
|
|
350
|
+
ON o.object_id = c.object_id
|
|
351
|
+
AND c.name = columns.COLUMN_NAME
|
|
352
|
+
WHERE columns.TABLE_NAME = #{prepared_statements ? COLUMN_DEFINITION_BIND_STRING_0 : quote(identifier.object)}
|
|
353
|
+
AND columns.TABLE_SCHEMA = #{identifier.schema.blank? ? 'schema_name()' : (prepared_statements ? COLUMN_DEFINITION_BIND_STRING_1 : quote(identifier.schema))}
|
|
354
|
+
ORDER BY columns.ordinal_position
|
|
355
|
+
}.gsub(/[ \t\r\n]+/, ' ').strip
|
|
356
|
+
|
|
357
|
+
binds = []
|
|
358
|
+
if prepared_statements
|
|
359
|
+
nv128 = SQLServer::Type::UnicodeVarchar.new limit: 128
|
|
360
|
+
binds << Relation::QueryAttribute.new('TABLE_NAME', identifier.object, nv128)
|
|
361
|
+
binds << Relation::QueryAttribute.new('TABLE_SCHEMA', identifier.schema, nv128) unless identifier.schema.blank?
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
results = sp_executesql(sql, 'SCHEMA', binds)
|
|
365
|
+
results.map do |ci|
|
|
366
|
+
ci = ci.symbolize_keys
|
|
367
|
+
ci[:_type] = ci[:type]
|
|
368
|
+
ci[:table_name] = view_tblnm || table_name
|
|
369
|
+
ci[:type] = case ci[:type]
|
|
370
|
+
when /^bit|image|text|ntext|datetime$/
|
|
371
|
+
ci[:type]
|
|
372
|
+
when /^datetime2|datetimeoffset$/i
|
|
373
|
+
"#{ci[:type]}(#{ci[:datetime_precision]})"
|
|
374
|
+
when /^time$/i
|
|
375
|
+
"#{ci[:type]}(#{ci[:datetime_precision]})"
|
|
376
|
+
when /^numeric|decimal$/i
|
|
377
|
+
"#{ci[:type]}(#{ci[:numeric_precision]},#{ci[:numeric_scale]})"
|
|
378
|
+
when /^float|real$/i
|
|
379
|
+
"#{ci[:type]}"
|
|
380
|
+
when /^char|nchar|varchar|nvarchar|binary|varbinary|bigint|int|smallint$/
|
|
381
|
+
ci[:length].to_i == -1 ? "#{ci[:type]}(max)" : "#{ci[:type]}(#{ci[:length]})"
|
|
382
|
+
else
|
|
383
|
+
ci[:type]
|
|
384
|
+
end
|
|
385
|
+
ci[:default_value],
|
|
386
|
+
ci[:default_function] = begin
|
|
387
|
+
default = ci[:default_value]
|
|
388
|
+
if default.nil? && view_exists
|
|
389
|
+
default = select_value "
|
|
390
|
+
SELECT c.COLUMN_DEFAULT
|
|
391
|
+
FROM #{database}.INFORMATION_SCHEMA.COLUMNS c
|
|
392
|
+
WHERE c.TABLE_NAME = '#{view_tblnm}'
|
|
393
|
+
AND c.COLUMN_NAME = '#{views_real_column_name(table_name, ci[:name])}'".squish, 'SCHEMA'
|
|
394
|
+
end
|
|
395
|
+
case default
|
|
396
|
+
when nil
|
|
397
|
+
[nil, nil]
|
|
398
|
+
when /\A\((\w+\(\))\)\Z/
|
|
399
|
+
default_function = Regexp.last_match[1]
|
|
400
|
+
[nil, default_function]
|
|
401
|
+
when /\A\(N'(.*)'\)\Z/m
|
|
402
|
+
string_literal = SQLServer::Utils.unquote_string(Regexp.last_match[1])
|
|
403
|
+
[string_literal, nil]
|
|
404
|
+
when /CREATE DEFAULT/mi
|
|
405
|
+
[nil, nil]
|
|
406
|
+
else
|
|
407
|
+
type = case ci[:type]
|
|
408
|
+
when /smallint|int|bigint/ then ci[:_type]
|
|
409
|
+
else ci[:type]
|
|
410
|
+
end
|
|
411
|
+
value = default.match(/\A\((.*)\)\Z/m)[1]
|
|
412
|
+
value = select_value "SELECT CAST(#{value} AS #{type}) AS value", 'SCHEMA'
|
|
413
|
+
[value, nil]
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
ci[:null] = ci[:is_nullable].to_i == 1
|
|
417
|
+
ci.delete(:is_nullable)
|
|
418
|
+
ci[:is_primary] = ci[:is_primary].to_i == 1
|
|
419
|
+
ci[:is_identity] = ci[:is_identity].to_i == 1 unless [TrueClass, FalseClass].include?(ci[:is_identity].class)
|
|
420
|
+
ci
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def remove_check_constraints(table_name, column_name)
|
|
425
|
+
constraints = select_values "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE where TABLE_NAME = '#{quote_string(table_name)}' and COLUMN_NAME = '#{quote_string(column_name)}'", 'SCHEMA'
|
|
426
|
+
constraints.each do |constraint|
|
|
427
|
+
do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def remove_default_constraint(table_name, column_name)
|
|
432
|
+
# If their are foreign keys in this table, we could still get back a 2D array, so flatten just in case.
|
|
433
|
+
execute_procedure(:sp_helpconstraint, table_name, 'nomsg').flatten.select do |row|
|
|
434
|
+
row['constraint_type'] == "DEFAULT on column #{column_name}"
|
|
435
|
+
end.each do |row|
|
|
436
|
+
do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{row['constraint_name']}"
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def remove_indexes(table_name, column_name)
|
|
441
|
+
indexes(table_name).select { |index| index.columns.include?(column_name.to_s) }.each do |index|
|
|
442
|
+
remove_index(table_name, name: index.name)
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# === SQLServer Specific (Misc Helpers) ========================= #
|
|
447
|
+
|
|
448
|
+
def get_table_name(sql)
|
|
449
|
+
tn = if sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)(\s+INTO)?\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
|
|
450
|
+
Regexp.last_match[3] || Regexp.last_match[4]
|
|
451
|
+
elsif sql =~ /FROM\s+([^\(\s]+)\s*/i
|
|
452
|
+
Regexp.last_match[1]
|
|
453
|
+
else
|
|
454
|
+
nil
|
|
455
|
+
end
|
|
456
|
+
SQLServer::Utils.extract_identifiers(tn).object
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def default_constraint_name(table_name, column_name)
|
|
460
|
+
"DF_#{table_name}_#{column_name}"
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def detect_column_for!(table_name, column_name)
|
|
464
|
+
unless column = schema_cache.columns(table_name).find { |c| c.name == column_name.to_s }
|
|
465
|
+
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
|
|
466
|
+
end
|
|
467
|
+
column
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def lowercase_schema_reflection_sql(node)
|
|
471
|
+
lowercase_schema_reflection ? "LOWER(#{node})" : node
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# === SQLServer Specific (View Reflection) ====================== #
|
|
475
|
+
|
|
476
|
+
def view_table_name(table_name)
|
|
477
|
+
view_info = view_information(table_name)
|
|
478
|
+
view_info ? get_table_name(view_info['VIEW_DEFINITION']) : table_name
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def view_information(table_name)
|
|
482
|
+
@view_information ||= {}
|
|
483
|
+
@view_information[table_name] ||= begin
|
|
484
|
+
identifier = SQLServer::Utils.extract_identifiers(table_name)
|
|
485
|
+
view_info = select_one "SELECT * FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = '#{identifier.object}'", 'SCHEMA'
|
|
486
|
+
if view_info
|
|
487
|
+
view_info = view_info.with_indifferent_access
|
|
488
|
+
if view_info[:VIEW_DEFINITION].blank? || view_info[:VIEW_DEFINITION].length == 4000
|
|
489
|
+
view_info[:VIEW_DEFINITION] = begin
|
|
490
|
+
select_values("EXEC sp_helptext #{identifier.object_quoted}", 'SCHEMA').join
|
|
491
|
+
rescue
|
|
492
|
+
warn "No view definition found, possible permissions problem.\nPlease run GRANT VIEW DEFINITION TO your_user;"
|
|
493
|
+
nil
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
view_info
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def views_real_column_name(table_name, column_name)
|
|
502
|
+
view_definition = view_information(table_name)[:VIEW_DEFINITION]
|
|
503
|
+
return column_name unless view_definition
|
|
504
|
+
match_data = view_definition.match(/([\w-]*)\s+as\s+#{column_name}/im)
|
|
505
|
+
match_data ? match_data[1] : column_name
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
private
|
|
509
|
+
|
|
510
|
+
def create_table_definition(*args)
|
|
511
|
+
SQLServer::TableDefinition.new(*args)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
end
|