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.
Files changed (148) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.travis.yml +27 -0
  4. data/CHANGELOG.md +124 -0
  5. data/CODE_OF_CONDUCT.md +31 -0
  6. data/Dockerfile +20 -0
  7. data/Gemfile +77 -0
  8. data/Guardfile +29 -0
  9. data/MIT-LICENSE +20 -0
  10. data/RAILS5-TODO.md +5 -0
  11. data/README.md +93 -0
  12. data/RUNNING_UNIT_TESTS.md +96 -0
  13. data/Rakefile +46 -0
  14. data/VERSION +1 -0
  15. data/activerecord-jdbcsqlserver-adapter.gemspec +21 -0
  16. data/appveyor.yml +39 -0
  17. data/docker-compose.ci.yml +11 -0
  18. data/lib/active_record/connection_adapters/sqlserver/core_ext/active_record.rb +27 -0
  19. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +25 -0
  20. data/lib/active_record/connection_adapters/sqlserver/core_ext/date_time.rb +58 -0
  21. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +47 -0
  22. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +4 -0
  23. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +49 -0
  24. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +362 -0
  25. data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +67 -0
  26. data/lib/active_record/connection_adapters/sqlserver/errors.rb +7 -0
  27. data/lib/active_record/connection_adapters/sqlserver/jdbc_overrides.rb +192 -0
  28. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +99 -0
  29. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +34 -0
  30. data/lib/active_record/connection_adapters/sqlserver/schema_dumper.rb +16 -0
  31. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +517 -0
  32. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +66 -0
  33. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_table.rb +66 -0
  34. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_xml.rb +22 -0
  35. data/lib/active_record/connection_adapters/sqlserver/sql_type_metadata.rb +20 -0
  36. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +112 -0
  37. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +64 -0
  38. data/lib/active_record/connection_adapters/sqlserver/type.rb +49 -0
  39. data/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb +19 -0
  40. data/lib/active_record/connection_adapters/sqlserver/type/binary.rb +21 -0
  41. data/lib/active_record/connection_adapters/sqlserver/type/boolean.rb +15 -0
  42. data/lib/active_record/connection_adapters/sqlserver/type/char.rb +32 -0
  43. data/lib/active_record/connection_adapters/sqlserver/type/data.rb +30 -0
  44. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +61 -0
  45. data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +71 -0
  46. data/lib/active_record/connection_adapters/sqlserver/type/datetime2.rb +17 -0
  47. data/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb +23 -0
  48. data/lib/active_record/connection_adapters/sqlserver/type/decimal.rb +21 -0
  49. data/lib/active_record/connection_adapters/sqlserver/type/float.rb +19 -0
  50. data/lib/active_record/connection_adapters/sqlserver/type/integer.rb +15 -0
  51. data/lib/active_record/connection_adapters/sqlserver/type/json.rb +11 -0
  52. data/lib/active_record/connection_adapters/sqlserver/type/money.rb +25 -0
  53. data/lib/active_record/connection_adapters/sqlserver/type/real.rb +19 -0
  54. data/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb +15 -0
  55. data/lib/active_record/connection_adapters/sqlserver/type/small_money.rb +25 -0
  56. data/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb +29 -0
  57. data/lib/active_record/connection_adapters/sqlserver/type/string.rb +12 -0
  58. data/lib/active_record/connection_adapters/sqlserver/type/text.rb +19 -0
  59. data/lib/active_record/connection_adapters/sqlserver/type/time.rb +68 -0
  60. data/lib/active_record/connection_adapters/sqlserver/type/time_value_fractional.rb +93 -0
  61. data/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb +19 -0
  62. data/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb +25 -0
  63. data/lib/active_record/connection_adapters/sqlserver/type/unicode_char.rb +21 -0
  64. data/lib/active_record/connection_adapters/sqlserver/type/unicode_string.rb +12 -0
  65. data/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb +19 -0
  66. data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar.rb +26 -0
  67. data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb +24 -0
  68. data/lib/active_record/connection_adapters/sqlserver/type/uuid.rb +36 -0
  69. data/lib/active_record/connection_adapters/sqlserver/type/varbinary.rb +26 -0
  70. data/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb +24 -0
  71. data/lib/active_record/connection_adapters/sqlserver/type/varchar.rb +26 -0
  72. data/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb +24 -0
  73. data/lib/active_record/connection_adapters/sqlserver/utils.rb +146 -0
  74. data/lib/active_record/connection_adapters/sqlserver/version.rb +11 -0
  75. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +445 -0
  76. data/lib/active_record/connection_adapters/sqlserver_column.rb +28 -0
  77. data/lib/active_record/jdbc_sqlserver_connection_methods.rb +31 -0
  78. data/lib/active_record/sqlserver_base.rb +16 -0
  79. data/lib/active_record/tasks/sqlserver_database_tasks.rb +131 -0
  80. data/lib/activerecord-jdbcsqlserver-adapter.rb +24 -0
  81. data/lib/activerecord-sqlserver-adapter.rb +1 -0
  82. data/lib/arel/visitors/sqlserver.rb +205 -0
  83. data/lib/arel_sqlserver.rb +3 -0
  84. data/test/appveyor/dbsetup.ps1 +27 -0
  85. data/test/appveyor/dbsetup.sql +11 -0
  86. data/test/bin/wait-for.sh +79 -0
  87. data/test/cases/adapter_test_sqlserver.rb +430 -0
  88. data/test/cases/coerced_tests.rb +845 -0
  89. data/test/cases/column_test_sqlserver.rb +812 -0
  90. data/test/cases/connection_test_sqlserver.rb +71 -0
  91. data/test/cases/execute_procedure_test_sqlserver.rb +45 -0
  92. data/test/cases/fetch_test_sqlserver.rb +57 -0
  93. data/test/cases/fully_qualified_identifier_test_sqlserver.rb +76 -0
  94. data/test/cases/helper_sqlserver.rb +44 -0
  95. data/test/cases/index_test_sqlserver.rb +47 -0
  96. data/test/cases/json_test_sqlserver.rb +32 -0
  97. data/test/cases/migration_test_sqlserver.rb +61 -0
  98. data/test/cases/order_test_sqlserver.rb +147 -0
  99. data/test/cases/pessimistic_locking_test_sqlserver.rb +94 -0
  100. data/test/cases/rake_test_sqlserver.rb +169 -0
  101. data/test/cases/schema_dumper_test_sqlserver.rb +234 -0
  102. data/test/cases/schema_test_sqlserver.rb +54 -0
  103. data/test/cases/scratchpad_test_sqlserver.rb +8 -0
  104. data/test/cases/showplan_test_sqlserver.rb +65 -0
  105. data/test/cases/specific_schema_test_sqlserver.rb +180 -0
  106. data/test/cases/transaction_test_sqlserver.rb +91 -0
  107. data/test/cases/utils_test_sqlserver.rb +129 -0
  108. data/test/cases/uuid_test_sqlserver.rb +49 -0
  109. data/test/config.yml +38 -0
  110. data/test/debug.rb +14 -0
  111. data/test/fixtures/1px.gif +0 -0
  112. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +11 -0
  113. data/test/models/sqlserver/booking.rb +3 -0
  114. data/test/models/sqlserver/customers_view.rb +3 -0
  115. data/test/models/sqlserver/datatype.rb +3 -0
  116. data/test/models/sqlserver/datatype_migration.rb +8 -0
  117. data/test/models/sqlserver/dollar_table_name.rb +3 -0
  118. data/test/models/sqlserver/dot_table_name.rb +3 -0
  119. data/test/models/sqlserver/edge_schema.rb +13 -0
  120. data/test/models/sqlserver/fk_has_fk.rb +3 -0
  121. data/test/models/sqlserver/fk_has_pk.rb +3 -0
  122. data/test/models/sqlserver/natural_pk_data.rb +4 -0
  123. data/test/models/sqlserver/natural_pk_int_data.rb +3 -0
  124. data/test/models/sqlserver/no_pk_data.rb +3 -0
  125. data/test/models/sqlserver/object_default.rb +3 -0
  126. data/test/models/sqlserver/quoted_table.rb +7 -0
  127. data/test/models/sqlserver/quoted_view_1.rb +3 -0
  128. data/test/models/sqlserver/quoted_view_2.rb +3 -0
  129. data/test/models/sqlserver/sst_memory.rb +3 -0
  130. data/test/models/sqlserver/string_default.rb +3 -0
  131. data/test/models/sqlserver/string_defaults_big_view.rb +3 -0
  132. data/test/models/sqlserver/string_defaults_view.rb +3 -0
  133. data/test/models/sqlserver/tinyint_pk.rb +3 -0
  134. data/test/models/sqlserver/upper.rb +3 -0
  135. data/test/models/sqlserver/uppered.rb +3 -0
  136. data/test/models/sqlserver/uuid.rb +3 -0
  137. data/test/schema/datatypes/2012.sql +55 -0
  138. data/test/schema/enable-in-memory-oltp.sql +81 -0
  139. data/test/schema/sqlserver_specific_schema.rb +238 -0
  140. data/test/support/coerceable_test_sqlserver.rb +49 -0
  141. data/test/support/connection_reflection.rb +34 -0
  142. data/test/support/load_schema_sqlserver.rb +29 -0
  143. data/test/support/minitest_sqlserver.rb +1 -0
  144. data/test/support/paths_sqlserver.rb +50 -0
  145. data/test/support/rake_helpers.rb +41 -0
  146. data/test/support/sql_counter_sqlserver.rb +28 -0
  147. data/test/support/test_in_memory_oltp.rb +15 -0
  148. 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,7 @@
1
+ module ActiveRecord
2
+
3
+ class DeadlockVictim < WrappedDatabaseException
4
+ end
5
+
6
+
7
+ end
@@ -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