activerecord-sqlserver-adapter_new 4.2.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/CHANGELOG.md +212 -0
  4. data/CODE_OF_CONDUCT.md +31 -0
  5. data/Gemfile +61 -0
  6. data/Guardfile +29 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.md +201 -0
  9. data/RUNNING_UNIT_TESTS.md +121 -0
  10. data/Rakefile +48 -0
  11. data/VERSION +1 -0
  12. data/activerecord-sqlserver-adapter_new.gemspec +20 -0
  13. data/appveyor.yml +39 -0
  14. data/lib/active_record/connection_adapters/sqlserver/core_ext/active_record.rb +27 -0
  15. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +25 -0
  16. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +40 -0
  17. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +4 -0
  18. data/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb +34 -0
  19. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +49 -0
  20. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +386 -0
  21. data/lib/active_record/connection_adapters/sqlserver/database_tasks.rb +68 -0
  22. data/lib/active_record/connection_adapters/sqlserver/errors.rb +7 -0
  23. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +69 -0
  24. data/lib/active_record/connection_adapters/sqlserver/schema_cache.rb +114 -0
  25. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +52 -0
  26. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +473 -0
  27. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +66 -0
  28. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_table.rb +66 -0
  29. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_xml.rb +22 -0
  30. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +76 -0
  31. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +57 -0
  32. data/lib/active_record/connection_adapters/sqlserver/type.rb +46 -0
  33. data/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb +15 -0
  34. data/lib/active_record/connection_adapters/sqlserver/type/binary.rb +15 -0
  35. data/lib/active_record/connection_adapters/sqlserver/type/boolean.rb +12 -0
  36. data/lib/active_record/connection_adapters/sqlserver/type/char.rb +38 -0
  37. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +21 -0
  38. data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +41 -0
  39. data/lib/active_record/connection_adapters/sqlserver/type/datetime2.rb +17 -0
  40. data/lib/active_record/connection_adapters/sqlserver/type/datetimeoffset.rb +31 -0
  41. data/lib/active_record/connection_adapters/sqlserver/type/decimal.rb +12 -0
  42. data/lib/active_record/connection_adapters/sqlserver/type/float.rb +15 -0
  43. data/lib/active_record/connection_adapters/sqlserver/type/integer.rb +12 -0
  44. data/lib/active_record/connection_adapters/sqlserver/type/money.rb +21 -0
  45. data/lib/active_record/connection_adapters/sqlserver/type/real.rb +15 -0
  46. data/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb +13 -0
  47. data/lib/active_record/connection_adapters/sqlserver/type/small_money.rb +21 -0
  48. data/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb +22 -0
  49. data/lib/active_record/connection_adapters/sqlserver/type/string.rb +12 -0
  50. data/lib/active_record/connection_adapters/sqlserver/type/text.rb +15 -0
  51. data/lib/active_record/connection_adapters/sqlserver/type/time.rb +40 -0
  52. data/lib/active_record/connection_adapters/sqlserver/type/time_value_fractional.rb +76 -0
  53. data/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb +15 -0
  54. data/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb +22 -0
  55. data/lib/active_record/connection_adapters/sqlserver/type/unicode_char.rb +15 -0
  56. data/lib/active_record/connection_adapters/sqlserver/type/unicode_string.rb +12 -0
  57. data/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb +15 -0
  58. data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar.rb +20 -0
  59. data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb +20 -0
  60. data/lib/active_record/connection_adapters/sqlserver/type/uuid.rb +23 -0
  61. data/lib/active_record/connection_adapters/sqlserver/type/varbinary.rb +20 -0
  62. data/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb +20 -0
  63. data/lib/active_record/connection_adapters/sqlserver/type/varchar.rb +20 -0
  64. data/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb +20 -0
  65. data/lib/active_record/connection_adapters/sqlserver/utils.rb +136 -0
  66. data/lib/active_record/connection_adapters/sqlserver/version.rb +11 -0
  67. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +405 -0
  68. data/lib/active_record/connection_adapters/sqlserver_column.rb +53 -0
  69. data/lib/active_record/sqlserver_base.rb +20 -0
  70. data/lib/active_record/tasks/sqlserver_database_tasks.rb +131 -0
  71. data/lib/activerecord-sqlserver-adapter.rb +1 -0
  72. data/lib/arel/visitors/sqlserver.rb +214 -0
  73. data/lib/arel_sqlserver.rb +3 -0
  74. data/test/appveyor/dbsetup.ps1 +27 -0
  75. data/test/appveyor/dbsetup.sql +11 -0
  76. data/test/cases/adapter_test_sqlserver.rb +444 -0
  77. data/test/cases/coerced_tests.rb +713 -0
  78. data/test/cases/column_test_sqlserver.rb +780 -0
  79. data/test/cases/connection_test_sqlserver.rb +142 -0
  80. data/test/cases/execute_procedure_test_sqlserver.rb +44 -0
  81. data/test/cases/fetch_test_sqlserver.rb +57 -0
  82. data/test/cases/fully_qualified_identifier_test_sqlserver.rb +76 -0
  83. data/test/cases/helper_sqlserver.rb +54 -0
  84. data/test/cases/migration_test_sqlserver.rb +61 -0
  85. data/test/cases/order_test_sqlserver.rb +147 -0
  86. data/test/cases/pessimistic_locking_test_sqlserver.rb +90 -0
  87. data/test/cases/rake_test_sqlserver.rb +163 -0
  88. data/test/cases/schema_dumper_test_sqlserver.rb +198 -0
  89. data/test/cases/schema_test_sqlserver.rb +54 -0
  90. data/test/cases/scratchpad_test_sqlserver.rb +9 -0
  91. data/test/cases/showplan_test_sqlserver.rb +65 -0
  92. data/test/cases/specific_schema_test_sqlserver.rb +167 -0
  93. data/test/cases/transaction_test_sqlserver.rb +66 -0
  94. data/test/cases/utils_test_sqlserver.rb +129 -0
  95. data/test/cases/uuid_test_sqlserver.rb +48 -0
  96. data/test/config.yml +41 -0
  97. data/test/debug.rb +14 -0
  98. data/test/fixtures/1px.gif +0 -0
  99. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +11 -0
  100. data/test/models/sqlserver/booking.rb +3 -0
  101. data/test/models/sqlserver/customers_view.rb +3 -0
  102. data/test/models/sqlserver/datatype.rb +3 -0
  103. data/test/models/sqlserver/datatype_migration.rb +3 -0
  104. data/test/models/sqlserver/dollar_table_name.rb +3 -0
  105. data/test/models/sqlserver/dot_table_name.rb +3 -0
  106. data/test/models/sqlserver/edge_schema.rb +13 -0
  107. data/test/models/sqlserver/fk_has_fk.rb +3 -0
  108. data/test/models/sqlserver/fk_has_pk.rb +3 -0
  109. data/test/models/sqlserver/natural_pk_data.rb +4 -0
  110. data/test/models/sqlserver/natural_pk_int_data.rb +3 -0
  111. data/test/models/sqlserver/no_pk_data.rb +3 -0
  112. data/test/models/sqlserver/object_default.rb +3 -0
  113. data/test/models/sqlserver/quoted_table.rb +7 -0
  114. data/test/models/sqlserver/quoted_view_1.rb +3 -0
  115. data/test/models/sqlserver/quoted_view_2.rb +3 -0
  116. data/test/models/sqlserver/string_default.rb +3 -0
  117. data/test/models/sqlserver/string_defaults_big_view.rb +3 -0
  118. data/test/models/sqlserver/string_defaults_view.rb +3 -0
  119. data/test/models/sqlserver/tinyint_pk.rb +3 -0
  120. data/test/models/sqlserver/upper.rb +3 -0
  121. data/test/models/sqlserver/uppered.rb +3 -0
  122. data/test/models/sqlserver/uuid.rb +3 -0
  123. data/test/schema/datatypes/2012.sql +55 -0
  124. data/test/schema/sqlserver_specific_schema.rb +207 -0
  125. data/test/support/coerceable_test_sqlserver.rb +45 -0
  126. data/test/support/connection_reflection.rb +37 -0
  127. data/test/support/load_schema_sqlserver.rb +29 -0
  128. data/test/support/minitest_sqlserver.rb +1 -0
  129. data/test/support/paths_sqlserver.rb +50 -0
  130. data/test/support/rake_helpers.rb +41 -0
  131. data/test/support/sql_counter_sqlserver.rb +32 -0
  132. metadata +253 -0
@@ -0,0 +1,68 @@
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
+ select_value "SELECT DATABASEPROPERTYEX(DB_NAME(), 'Collation')"
28
+ end
29
+
30
+
31
+ private
32
+
33
+ def create_database_options(options={})
34
+ keys = [:collate]
35
+ copts = @connection_options
36
+ options = {
37
+ collate: copts[:collation]
38
+ }.merge(options.symbolize_keys).select { |_, v|
39
+ v.present?
40
+ }.slice(*keys).map { |k,v|
41
+ "#{k.to_s.upcase} #{v}"
42
+ }.join(' ')
43
+ options
44
+ end
45
+
46
+ def create_database_edition_options(options={})
47
+ keys = [:maxsize, :edition, :service_objective]
48
+ copts = @connection_options
49
+ edition_options = {
50
+ maxsize: copts[:azure_maxsize],
51
+ edition: copts[:azure_edition],
52
+ service_objective: copts[:azure_service_objective]
53
+ }.merge(options.symbolize_keys).select { |_, v|
54
+ v.present?
55
+ }.slice(*keys).map { |k,v|
56
+ "#{k.to_s.upcase} = #{v}"
57
+ }.join(', ')
58
+ edition_options = "( #{edition_options} )" if edition_options.present?
59
+ edition_options
60
+ end
61
+
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+
68
+
@@ -0,0 +1,7 @@
1
+ module ActiveRecord
2
+
3
+ class DeadlockVictim < WrappedDatabaseException
4
+ end
5
+
6
+
7
+ end
@@ -0,0 +1,69 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module SQLServer
4
+ module Quoting
5
+
6
+ QUOTED_TRUE = '1'
7
+ QUOTED_FALSE = '0'
8
+ QUOTED_STRING_PREFIX = 'N'
9
+
10
+ def quote_string(s)
11
+ SQLServer::Utils.quote_string(s)
12
+ end
13
+
14
+ def quote_column_name(name)
15
+ SQLServer::Utils.extract_identifiers(name).quoted
16
+ end
17
+
18
+ def quote_default_value(value, column)
19
+ if column.type == :uuid && value =~ /\(\)/
20
+ value
21
+ else
22
+ quote(value, column)
23
+ end
24
+ end
25
+
26
+ def quoted_true
27
+ QUOTED_TRUE
28
+ end
29
+
30
+ def unquoted_true
31
+ 1
32
+ end
33
+
34
+ def quoted_false
35
+ QUOTED_FALSE
36
+ end
37
+
38
+ def unquoted_false
39
+ 0
40
+ end
41
+
42
+ def quoted_date(value)
43
+ if value.acts_like?(:date)
44
+ Type::Date.new.type_cast_for_database(value)
45
+ else value.acts_like?(:time)
46
+ Type::DateTime.new.type_cast_for_database(value)
47
+ end
48
+ end
49
+
50
+
51
+ private
52
+
53
+ def _quote(value)
54
+ case value
55
+ when Type::Binary::Data
56
+ "0x#{value.hex}"
57
+ when ActiveRecord::Type::SQLServer::Char::Data
58
+ value.quoted
59
+ when String, ActiveSupport::Multibyte::Chars
60
+ "#{QUOTED_STRING_PREFIX}#{super}"
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,114 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module SQLServer
4
+ class SchemaCache < ActiveRecord::ConnectionAdapters::SchemaCache
5
+
6
+ def initialize(conn)
7
+ super
8
+ @views = {}
9
+ @view_information = {}
10
+ end
11
+
12
+ # Superclass Overrides
13
+
14
+ def primary_keys(table_name)
15
+ name = key(table_name)
16
+ @primary_keys[name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil
17
+ end
18
+
19
+ def table_exists?(table_name)
20
+ name = key(table_name)
21
+ prepare_tables_and_views
22
+ return @tables[name] if @tables.key? name
23
+ table_exists = @tables[name] = connection.table_exists?(table_name)
24
+ table_exists || view_exists?(table_name)
25
+ end
26
+
27
+ def tables(name)
28
+ super(key(name))
29
+ end
30
+
31
+ def columns(table_name)
32
+ name = key(table_name)
33
+ @columns[name] ||= connection.columns(table_name)
34
+ end
35
+
36
+ def columns_hash(table_name)
37
+ name = key(table_name)
38
+ @columns_hash[name] ||= Hash[columns(table_name).map { |col|
39
+ [col.name, col]
40
+ }]
41
+ end
42
+
43
+ def clear!
44
+ super
45
+ @views.clear
46
+ @view_information.clear
47
+ end
48
+
49
+ def size
50
+ super + [@views, @view_information].map{ |x| x.size }.inject(:+)
51
+ end
52
+
53
+ def clear_table_cache!(table_name)
54
+ name = key(table_name)
55
+ @columns.delete name
56
+ @columns_hash.delete name
57
+ @primary_keys.delete name
58
+ @tables.delete name
59
+ @views.delete name
60
+ @view_information.delete name
61
+ end
62
+
63
+ def marshal_dump
64
+ super + [@views, @view_information]
65
+ end
66
+
67
+ def marshal_load(array)
68
+ @views, @view_information = array[-2..-1]
69
+ super(array[0..-3])
70
+ end
71
+
72
+ # SQL Server Specific
73
+
74
+ def view_exists?(table_name)
75
+ name = key(table_name)
76
+ prepare_tables_and_views
77
+ return @views[name] if @views.key? name
78
+ @views[name] = connection.views.include?(table_name)
79
+ end
80
+
81
+ def view_information(table_name)
82
+ name = key(table_name)
83
+ return @view_information[name] if @view_information.key? name
84
+ @view_information[name] = connection.send(:view_information, table_name)
85
+ end
86
+
87
+
88
+ private
89
+
90
+ def identifier(table_name)
91
+ SQLServer::Utils.extract_identifiers(table_name)
92
+ end
93
+
94
+ def key(table_name)
95
+ identifier(table_name).quoted
96
+ end
97
+
98
+ def prepare_tables_and_views
99
+ prepare_views if @views.empty?
100
+ prepare_tables if @tables.empty?
101
+ end
102
+
103
+ def prepare_tables
104
+ connection.tables.each { |table| @tables[key(table)] = true }
105
+ end
106
+
107
+ def prepare_views
108
+ connection.views.each { |view| @views[key(view)] = true }
109
+ end
110
+
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,52 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module SQLServer
4
+ class SchemaCreation < AbstractAdapter::SchemaCreation
5
+
6
+ private
7
+
8
+ def visit_ColumnDefinition(o)
9
+ sql = super
10
+ if o.primary_key? && o.type == :uuid
11
+ sql << ' PRIMARY KEY '
12
+ add_column_options!(sql, column_options(o))
13
+ end
14
+ sql
15
+ end
16
+
17
+ def visit_TableDefinition(o)
18
+ if o.as
19
+ table_name = quote_table_name(o.temporary ? "##{o.name}" : o.name)
20
+ projections, source = @conn.to_sql(o.as).match(%r{SELECT\s+(.*)?\s+FROM\s+(.*)?}).captures
21
+ select_into = "SELECT #{projections} INTO #{table_name} FROM #{source}"
22
+ else
23
+ o.instance_variable_set :@as, nil
24
+ super
25
+ end
26
+ end
27
+
28
+ def add_column_options!(sql, options)
29
+ column = options.fetch(:column) { return super }
30
+ if (column.type == :uuid || column.type == :uniqueidentifier) && options[:default] =~ /\(\)/
31
+ sql << " DEFAULT #{options.delete(:default)}"
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ def action_sql(action, dependency)
38
+ case dependency
39
+ when :restrict
40
+ raise ArgumentError, <<-MSG.strip_heredoc
41
+ '#{dependency}' is not supported for :on_update or :on_delete.
42
+ Supported values are: :nullify, :cascade
43
+ MSG
44
+ else
45
+ super
46
+ end
47
+ end
48
+
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,473 @@
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 data_sources
11
+ tables + views
12
+ end
13
+
14
+ def tables(table_type = 'BASE TABLE')
15
+ select_values "SELECT #{lowercase_schema_reflection_sql('TABLE_NAME')} FROM INFORMATION_SCHEMA.TABLES #{"WHERE TABLE_TYPE = '#{table_type}'" if table_type} ORDER BY TABLE_NAME", 'SCHEMA'
16
+ end
17
+
18
+ def table_exists?(table_name)
19
+ return false if table_name.blank?
20
+ unquoted_table_name = SQLServer::Utils.extract_identifiers(table_name).object
21
+ super || tables.include?(unquoted_table_name) || views.include?(unquoted_table_name)
22
+ end
23
+
24
+ def create_table(table_name, options = {})
25
+ res = super
26
+ schema_cache.clear_table_cache!(table_name)
27
+ res
28
+ end
29
+
30
+ def indexes(table_name, name = nil)
31
+ data = select("EXEC sp_helpindex #{quote(table_name)}", name) rescue []
32
+ data.reduce([]) do |indexes, index|
33
+ index = index.with_indifferent_access
34
+ if index[:index_description] =~ /primary key/
35
+ indexes
36
+ else
37
+ name = index[:index_name]
38
+ unique = index[:index_description] =~ /unique/
39
+ where = select_value("SELECT [filter_definition] FROM sys.indexes WHERE name = #{quote(name)}")
40
+ columns = index[:index_keys].split(',').map do |column|
41
+ column.strip!
42
+ column.gsub! '(-)', '' if column.ends_with?('(-)')
43
+ column
44
+ end
45
+ indexes << IndexDefinition.new(table_name, name, unique, columns, nil, nil, where)
46
+ end
47
+ end
48
+ end
49
+
50
+ def columns(table_name, _name = nil)
51
+ return [] if table_name.blank?
52
+ column_definitions(table_name).map do |ci|
53
+ sqlserver_options = ci.slice :ordinal_position, :is_primary, :is_identity, :default_function, :table_name, :collation
54
+ cast_type = lookup_cast_type(ci[:type])
55
+ new_column ci[:name], ci[:default_value], cast_type, ci[:type], ci[:null], sqlserver_options
56
+ end
57
+ end
58
+
59
+ def new_column(name, default, cast_type, sql_type = nil, null = true, sqlserver_options={})
60
+ SQLServerColumn.new name, default, cast_type, sql_type, null, sqlserver_options
61
+ end
62
+
63
+ def rename_table(table_name, new_name)
64
+ do_execute "EXEC sp_rename '#{table_name}', '#{new_name}'"
65
+ rename_table_indexes(table_name, new_name)
66
+ end
67
+
68
+ def remove_column(table_name, column_name, type = nil, options = {})
69
+ raise ArgumentError.new('You must specify at least one column name. Example: remove_column(:people, :first_name)') if column_name.is_a? Array
70
+ remove_check_constraints(table_name, column_name)
71
+ remove_default_constraint(table_name, column_name)
72
+ remove_indexes(table_name, column_name)
73
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
74
+ end
75
+
76
+ def change_column(table_name, column_name, type, options = {})
77
+ sql_commands = []
78
+ indexes = []
79
+ column_object = schema_cache.columns(table_name).find { |c| c.name.to_s == column_name.to_s }
80
+ if options_include_default?(options) || (column_object && column_object.type != type.to_sym)
81
+ remove_default_constraint(table_name, column_name)
82
+ indexes = indexes(table_name).select { |index| index.columns.include?(column_name.to_s) }
83
+ remove_indexes(table_name, column_name)
84
+ end
85
+ sql_commands << "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(options[:default], column_object)} WHERE #{quote_column_name(column_name)} IS NULL" if !options[:null].nil? && options[:null] == false && !options[:default].nil?
86
+ 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])}"
87
+ sql_commands[-1] << ' NOT NULL' if !options[:null].nil? && options[:null] == false
88
+ if options_include_default?(options)
89
+ sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote_default_value(options[:default], column_object)} FOR #{quote_column_name(column_name)}"
90
+ end
91
+ # Add any removed indexes back
92
+ indexes.each do |index|
93
+ sql_commands << "CREATE INDEX #{quote_table_name(index.name)} ON #{quote_table_name(table_name)} (#{index.columns.map { |c| quote_column_name(c) }.join(', ')})"
94
+ end
95
+ sql_commands.each { |c| do_execute(c) }
96
+ end
97
+
98
+ def change_column_default(table_name, column_name, default)
99
+ schema_cache.clear_table_cache!(table_name)
100
+ remove_default_constraint(table_name, column_name)
101
+ column_object = schema_cache.columns(table_name).find { |c| c.name.to_s == column_name.to_s }
102
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_constraint_name(table_name, column_name)} DEFAULT #{quote_default_value(default, column_object)} FOR #{quote_column_name(column_name)}"
103
+ schema_cache.clear_table_cache!(table_name)
104
+ end
105
+
106
+ def rename_column(table_name, column_name, new_column_name)
107
+ schema_cache.clear_table_cache!(table_name)
108
+ detect_column_for! table_name, column_name
109
+ identifier = SQLServer::Utils.extract_identifiers("#{table_name}.#{column_name}")
110
+ execute_procedure :sp_rename, identifier.quoted, new_column_name, 'COLUMN'
111
+ rename_column_indexes(table_name, column_name, new_column_name)
112
+ schema_cache.clear_table_cache!(table_name)
113
+ end
114
+
115
+ def rename_index(table_name, old_name, new_name)
116
+ 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
117
+ identifier = SQLServer::Utils.extract_identifiers("#{table_name}.#{old_name}")
118
+ execute_procedure :sp_rename, identifier.quoted, new_name, 'INDEX'
119
+ end
120
+
121
+ def remove_index!(table_name, index_name)
122
+ do_execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
123
+ end
124
+
125
+ def foreign_keys(table_name)
126
+ identifier = SQLServer::Utils.extract_identifiers(table_name)
127
+ fk_info = execute_procedure :sp_fkeys, nil, identifier.schema, nil, identifier.object, identifier.schema
128
+ fk_info.map do |row|
129
+ from_table = identifier.object
130
+ to_table = row['PKTABLE_NAME']
131
+ options = {
132
+ name: row['FK_NAME'],
133
+ column: row['FKCOLUMN_NAME'],
134
+ primary_key: row['PKCOLUMN_NAME'],
135
+ on_update: extract_foreign_key_action('update', row['FK_NAME']),
136
+ on_delete: extract_foreign_key_action('delete', row['FK_NAME'])
137
+ }
138
+ ForeignKeyDefinition.new from_table, to_table, options
139
+ end
140
+ end
141
+
142
+ def extract_foreign_key_action(action, fk_name)
143
+ case select_value("SELECT #{action}_referential_action_desc FROM sys.foreign_keys WHERE name = '#{fk_name}'")
144
+ when 'CASCADE' then :cascade
145
+ when 'SET_NULL' then :nullify
146
+ end
147
+ end
148
+
149
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
150
+ type_limitable = %w(string integer float char nchar varchar nvarchar).include?(type.to_s)
151
+ limit = nil unless type_limitable
152
+ case type.to_s
153
+ when 'integer'
154
+ case limit
155
+ when 1..2 then 'smallint'
156
+ when 3..4, nil then 'integer'
157
+ when 5..8 then 'bigint'
158
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
159
+ end
160
+ else
161
+ super
162
+ end
163
+ end
164
+
165
+ def columns_for_distinct(columns, orders)
166
+ order_columns = orders.reject(&:blank?).map{ |s|
167
+ s = s.to_sql unless s.is_a?(String)
168
+ s.gsub(/\s+(?:ASC|DESC)\b/i, '')
169
+ .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, '')
170
+ }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
171
+ [super, *order_columns].join(', ')
172
+ end
173
+
174
+ def change_column_null(table_name, column_name, allow_null, default = nil)
175
+ table_id = SQLServer::Utils.extract_identifiers(table_name)
176
+ column_id = SQLServer::Utils.extract_identifiers(column_name)
177
+ column = detect_column_for! table_name, column_name
178
+ if !allow_null.nil? && allow_null == false && !default.nil?
179
+ do_execute("UPDATE #{table_id} SET #{column_id}=#{quote(default)} WHERE #{column_id} IS NULL")
180
+ end
181
+ sql = "ALTER TABLE #{table_id} ALTER COLUMN #{column_id} #{type_to_sql column.type, column.limit, column.precision, column.scale}"
182
+ sql << ' NOT NULL' if !allow_null.nil? && allow_null == false
183
+ do_execute sql
184
+ end
185
+
186
+ # === SQLServer Specific ======================================== #
187
+
188
+ def views
189
+ tables('VIEW')
190
+ end
191
+
192
+
193
+ protected
194
+
195
+ # === SQLServer Specific ======================================== #
196
+
197
+ def initialize_native_database_types
198
+ {
199
+ primary_key: 'int NOT NULL IDENTITY(1,1) PRIMARY KEY',
200
+ integer: { name: 'int', limit: 4 },
201
+ bigint: { name: 'bigint' },
202
+ boolean: { name: 'bit' },
203
+ decimal: { name: 'decimal' },
204
+ money: { name: 'money' },
205
+ smallmoney: { name: 'smallmoney' },
206
+ float: { name: 'float' },
207
+ real: { name: 'real' },
208
+ date: { name: 'date' },
209
+ datetime: { name: 'datetime' },
210
+ datetime2: { name: 'datetime2' },
211
+ datetimeoffset: { name: 'datetimeoffset' },
212
+ smalldatetime: { name: 'smalldatetime' },
213
+ timestamp: { name: 'datetime' },
214
+ time: { name: 'time' },
215
+ char: { name: 'char' },
216
+ varchar: { name: 'varchar', limit: 8000 },
217
+ varchar_max: { name: 'varchar(max)' },
218
+ text_basic: { name: 'text' },
219
+ nchar: { name: 'nchar' },
220
+ string: { name: 'nvarchar', limit: 4000 },
221
+ text: { name: 'nvarchar(max)' },
222
+ ntext: { name: 'ntext' },
223
+ binary_basic: { name: 'binary' },
224
+ varbinary: { name: 'varbinary', limit: 8000 },
225
+ binary: { name: 'varbinary(max)' },
226
+ uuid: { name: 'uniqueidentifier' },
227
+ ss_timestamp: { name: 'timestamp' }
228
+ }
229
+ end
230
+
231
+ def column_definitions(table_name)
232
+ identifier = if database_prefix_remote_server?
233
+ SQLServer::Utils.extract_identifiers("#{database_prefix}#{table_name}")
234
+ else
235
+ SQLServer::Utils.extract_identifiers(table_name)
236
+ end
237
+ database = identifier.fully_qualified_database_quoted
238
+ view_exists = schema_cache.view_exists?(table_name)
239
+ view_tblnm = table_name_or_views_table_name(table_name) if view_exists
240
+ sql = %{
241
+ SELECT DISTINCT
242
+ #{lowercase_schema_reflection_sql('columns.TABLE_NAME')} AS table_name,
243
+ #{lowercase_schema_reflection_sql('columns.COLUMN_NAME')} AS name,
244
+ columns.DATA_TYPE AS type,
245
+ columns.COLUMN_DEFAULT AS default_value,
246
+ columns.NUMERIC_SCALE AS numeric_scale,
247
+ columns.NUMERIC_PRECISION AS numeric_precision,
248
+ columns.DATETIME_PRECISION AS datetime_precision,
249
+ columns.COLLATION_NAME AS collation,
250
+ columns.ordinal_position,
251
+ CASE
252
+ WHEN columns.DATA_TYPE IN ('nchar','nvarchar','char','varchar') THEN columns.CHARACTER_MAXIMUM_LENGTH
253
+ ELSE COL_LENGTH('#{database}.'+columns.TABLE_SCHEMA+'.'+columns.TABLE_NAME, columns.COLUMN_NAME)
254
+ END AS [length],
255
+ CASE
256
+ WHEN columns.IS_NULLABLE = 'YES' THEN 1
257
+ ELSE NULL
258
+ END AS [is_nullable],
259
+ CASE
260
+ WHEN KCU.COLUMN_NAME IS NOT NULL AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY' THEN 1
261
+ ELSE NULL
262
+ END AS [is_primary],
263
+ c.is_identity AS [is_identity]
264
+ FROM #{database}.INFORMATION_SCHEMA.COLUMNS columns
265
+ LEFT OUTER JOIN #{database}.INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC
266
+ ON TC.TABLE_NAME = columns.TABLE_NAME
267
+ AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY'
268
+ LEFT OUTER JOIN #{database}.INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
269
+ ON KCU.COLUMN_NAME = columns.COLUMN_NAME
270
+ AND KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
271
+ AND KCU.CONSTRAINT_CATALOG = TC.CONSTRAINT_CATALOG
272
+ AND KCU.CONSTRAINT_SCHEMA = TC.CONSTRAINT_SCHEMA
273
+ INNER JOIN #{database}.sys.schemas AS s
274
+ ON s.name = columns.TABLE_SCHEMA
275
+ AND s.schema_id = s.schema_id
276
+ INNER JOIN #{database}.sys.objects AS o
277
+ ON s.schema_id = o.schema_id
278
+ AND o.is_ms_shipped = 0
279
+ AND o.type IN ('U', 'V')
280
+ AND o.name = columns.TABLE_NAME
281
+ INNER JOIN #{database}.sys.columns AS c
282
+ ON o.object_id = c.object_id
283
+ AND c.name = columns.COLUMN_NAME
284
+ WHERE columns.TABLE_NAME = @0
285
+ AND columns.TABLE_SCHEMA = #{identifier.schema.blank? ? 'schema_name()' : '@1'}
286
+ ORDER BY columns.ordinal_position
287
+ }.gsub(/[ \t\r\n]+/, ' ')
288
+ binds = [[info_schema_table_name_column, identifier.object]]
289
+ binds << [info_schema_table_schema_column, identifier.schema] unless identifier.schema.blank?
290
+ results = sp_executesql(sql, 'SCHEMA', binds)
291
+ results.map do |ci|
292
+ ci = ci.symbolize_keys
293
+ ci[:_type] = ci[:type]
294
+ ci[:table_name] = view_tblnm || table_name
295
+ ci[:type] = case ci[:type]
296
+ when /^bit|image|text|ntext|datetime$/
297
+ ci[:type]
298
+ when /^datetime2|datetimeoffset$/i
299
+ "#{ci[:type]}(#{ci[:datetime_precision]})"
300
+ when /^time$/i
301
+ "#{ci[:type]}(#{ci[:datetime_precision]})"
302
+ when /^numeric|decimal$/i
303
+ "#{ci[:type]}(#{ci[:numeric_precision]},#{ci[:numeric_scale]})"
304
+ when /^float|real$/i
305
+ "#{ci[:type]}"
306
+ when /^char|nchar|varchar|nvarchar|binary|varbinary|bigint|int|smallint$/
307
+ ci[:length].to_i == -1 ? "#{ci[:type]}(max)" : "#{ci[:type]}(#{ci[:length]})"
308
+ else
309
+ ci[:type]
310
+ end
311
+ ci[:default_value],
312
+ ci[:default_function] = begin
313
+ default = ci[:default_value]
314
+ if default.nil? && view_exists
315
+ default = select_value "
316
+ SELECT c.COLUMN_DEFAULT
317
+ FROM #{database}.INFORMATION_SCHEMA.COLUMNS c
318
+ WHERE c.TABLE_NAME = '#{view_tblnm}'
319
+ AND c.COLUMN_NAME = '#{views_real_column_name(table_name, ci[:name])}'".squish, 'SCHEMA'
320
+ end
321
+ case default
322
+ when nil
323
+ [nil, nil]
324
+ when /\A\((\w+\(\))\)\Z/
325
+ default_function = Regexp.last_match[1]
326
+ [nil, default_function]
327
+ when /\A\(N'(.*)'\)\Z/m
328
+ string_literal = SQLServer::Utils.unquote_string(Regexp.last_match[1])
329
+ [string_literal, nil]
330
+ when /CREATE DEFAULT/mi
331
+ [nil, nil]
332
+ else
333
+ type = case ci[:type]
334
+ when /smallint|int|bigint/ then ci[:_type]
335
+ else ci[:type]
336
+ end
337
+ value = default.match(/\A\((.*)\)\Z/m)[1]
338
+ value = select_value "SELECT CAST(#{value} AS #{type}) AS value", 'SCHEMA'
339
+ [value, nil]
340
+ end
341
+ end
342
+ ci[:null] = ci[:is_nullable].to_i == 1
343
+ ci.delete(:is_nullable)
344
+ ci[:is_primary] = ci[:is_primary].to_i == 1
345
+ ci[:is_identity] = ci[:is_identity].to_i == 1 unless [TrueClass, FalseClass].include?(ci[:is_identity].class)
346
+ ci
347
+ end
348
+ end
349
+
350
+ def info_schema_table_name_column
351
+ @info_schema_table_name_column ||= new_column 'table_name', nil, lookup_cast_type('nvarchar(128)'), 'nvarchar(128)', true
352
+ end
353
+
354
+ def info_schema_table_schema_column
355
+ @info_schema_table_schema_column ||= new_column 'table_schema', nil, lookup_cast_type('nvarchar(128)'), 'nvarchar(128)', true
356
+ end
357
+
358
+ def remove_check_constraints(table_name, column_name)
359
+ 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'
360
+ constraints.each do |constraint|
361
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
362
+ end
363
+ end
364
+
365
+ def remove_default_constraint(table_name, column_name)
366
+ # If their are foreign keys in this table, we could still get back a 2D array, so flatten just in case.
367
+ execute_procedure(:sp_helpconstraint, table_name, 'nomsg').flatten.select do |row|
368
+ row['constraint_type'] == "DEFAULT on column #{column_name}"
369
+ end.each do |row|
370
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{row['constraint_name']}"
371
+ end
372
+ end
373
+
374
+ def remove_indexes(table_name, column_name)
375
+ indexes(table_name).select { |index| index.columns.include?(column_name.to_s) }.each do |index|
376
+ remove_index(table_name, name: index.name)
377
+ end
378
+ end
379
+
380
+ # === SQLServer Specific (Misc Helpers) ========================= #
381
+
382
+ def get_table_name(sql)
383
+ tn = if sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)(\s+INTO)?\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
384
+ Regexp.last_match[3] || Regexp.last_match[4]
385
+ elsif sql =~ /FROM\s+([^\(\s]+)\s*/i
386
+ Regexp.last_match[1]
387
+ else
388
+ nil
389
+ end
390
+ SQLServer::Utils.extract_identifiers(tn).object
391
+ end
392
+
393
+ def default_constraint_name(table_name, column_name)
394
+ "DF_#{table_name}_#{column_name}"
395
+ end
396
+
397
+ def detect_column_for!(table_name, column_name)
398
+ unless column = schema_cache.columns(table_name).find { |c| c.name == column_name.to_s }
399
+ raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
400
+ end
401
+ column
402
+ end
403
+
404
+ def lowercase_schema_reflection_sql(node)
405
+ lowercase_schema_reflection ? "LOWER(#{node})" : node
406
+ end
407
+
408
+ # === SQLServer Specific (View Reflection) ====================== #
409
+
410
+ def view_table_name(table_name)
411
+ view_info = schema_cache.view_information(table_name)
412
+ view_info ? get_table_name(view_info['VIEW_DEFINITION']) : table_name
413
+ end
414
+
415
+ def view_information(table_name)
416
+ identifier = SQLServer::Utils.extract_identifiers(table_name)
417
+ view_info = select_one "SELECT * FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = '#{identifier.object}'", 'SCHEMA'
418
+ if view_info
419
+ view_info = view_info.with_indifferent_access
420
+ if view_info[:VIEW_DEFINITION].blank? || view_info[:VIEW_DEFINITION].length == 4000
421
+ view_info[:VIEW_DEFINITION] = begin
422
+ select_values("EXEC sp_helptext #{identifier.object_quoted}", 'SCHEMA').join
423
+ rescue
424
+ warn "No view definition found, possible permissions problem.\nPlease run GRANT VIEW DEFINITION TO your_user;"
425
+ nil
426
+ end
427
+ end
428
+ end
429
+ view_info
430
+ end
431
+
432
+ def table_name_or_views_table_name(table_name)
433
+ schema_cache.view_exists?(table_name) ? view_table_name(table_name) : table_name
434
+ end
435
+
436
+ def views_real_column_name(table_name, column_name)
437
+ view_definition = schema_cache.view_information(table_name)[:VIEW_DEFINITION]
438
+ return column_name unless view_definition
439
+ match_data = view_definition.match(/([\w-]*)\s+as\s+#{column_name}/im)
440
+ match_data ? match_data[1] : column_name
441
+ end
442
+
443
+ # === SQLServer Specific (Identity Inserts) ===================== #
444
+
445
+ def query_requires_identity_insert?(sql)
446
+ if insert_sql?(sql)
447
+ table_name = get_table_name(sql)
448
+ id_column = identity_column(table_name)
449
+ id_column && sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)[^(]+\([^)]*\b(#{id_column.name})\b,?[^)]*\)/i ? quote_table_name(table_name) : false
450
+ else
451
+ false
452
+ end
453
+ end
454
+
455
+ def insert_sql?(sql)
456
+ !(sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)/i).nil?
457
+ end
458
+
459
+ def identity_column(table_name)
460
+ schema_cache.columns(table_name).find(&:is_identity?)
461
+ end
462
+
463
+
464
+ private
465
+
466
+ def create_table_definition(name, temporary, options, as = nil)
467
+ SQLServer::TableDefinition.new native_database_types, name, temporary, options, as
468
+ end
469
+
470
+ end
471
+ end
472
+ end
473
+ end