activerecord-sqlserver-adapter_new 4.2.15

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 (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