activerecord-sqlserver-adapter 4.1.8 → 4.2.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +15 -0
  3. data/CHANGELOG.md +60 -0
  4. data/Gemfile +45 -0
  5. data/Guardfile +29 -0
  6. data/MIT-LICENSE +5 -5
  7. data/README.md +193 -0
  8. data/RUNNING_UNIT_TESTS.md +95 -0
  9. data/Rakefile +48 -0
  10. data/activerecord-sqlserver-adapter.gemspec +28 -0
  11. data/lib/active_record/connection_adapters/sqlserver/core_ext/active_record.rb +5 -15
  12. data/lib/active_record/connection_adapters/sqlserver/core_ext/attribute_methods.rb +25 -0
  13. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +6 -4
  14. data/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb +9 -3
  15. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +3 -1
  16. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +130 -151
  17. data/lib/active_record/connection_adapters/sqlserver/errors.rb +0 -25
  18. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +39 -78
  19. data/lib/active_record/connection_adapters/sqlserver/schema_cache.rb +71 -47
  20. data/lib/active_record/connection_adapters/sqlserver/schema_creation.rb +14 -30
  21. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +112 -108
  22. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +4 -2
  23. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_table.rb +1 -1
  24. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_xml.rb +1 -1
  25. data/lib/active_record/connection_adapters/sqlserver/table_definition.rb +52 -7
  26. data/lib/active_record/connection_adapters/sqlserver/transaction.rb +52 -0
  27. data/lib/active_record/connection_adapters/sqlserver/type.rb +46 -0
  28. data/lib/active_record/connection_adapters/sqlserver/type/big_integer.rb +15 -0
  29. data/lib/active_record/connection_adapters/sqlserver/type/binary.rb +15 -0
  30. data/lib/active_record/connection_adapters/sqlserver/type/boolean.rb +13 -0
  31. data/lib/active_record/connection_adapters/sqlserver/type/castable.rb +15 -0
  32. data/lib/active_record/connection_adapters/sqlserver/type/char.rb +15 -0
  33. data/lib/active_record/connection_adapters/sqlserver/type/core_ext/value.rb +39 -0
  34. data/lib/active_record/connection_adapters/sqlserver/type/date.rb +14 -0
  35. data/lib/active_record/connection_adapters/sqlserver/type/datetime.rb +37 -0
  36. data/lib/active_record/connection_adapters/sqlserver/type/decimal.rb +13 -0
  37. data/lib/active_record/connection_adapters/sqlserver/type/float.rb +17 -0
  38. data/lib/active_record/connection_adapters/sqlserver/type/integer.rb +13 -0
  39. data/lib/active_record/connection_adapters/sqlserver/type/money.rb +21 -0
  40. data/lib/active_record/connection_adapters/sqlserver/type/quoter.rb +32 -0
  41. data/lib/active_record/connection_adapters/sqlserver/type/real.rb +17 -0
  42. data/lib/active_record/connection_adapters/sqlserver/type/small_integer.rb +13 -0
  43. data/lib/active_record/connection_adapters/sqlserver/type/small_money.rb +21 -0
  44. data/lib/active_record/connection_adapters/sqlserver/type/smalldatetime.rb +24 -0
  45. data/lib/active_record/connection_adapters/sqlserver/type/string.rb +12 -0
  46. data/lib/active_record/connection_adapters/sqlserver/type/text.rb +15 -0
  47. data/lib/active_record/connection_adapters/sqlserver/type/time.rb +59 -0
  48. data/lib/active_record/connection_adapters/sqlserver/type/timestamp.rb +15 -0
  49. data/lib/active_record/connection_adapters/sqlserver/type/tiny_integer.rb +22 -0
  50. data/lib/active_record/connection_adapters/sqlserver/type/unicode_char.rb +15 -0
  51. data/lib/active_record/connection_adapters/sqlserver/type/unicode_string.rb +12 -0
  52. data/lib/active_record/connection_adapters/sqlserver/type/unicode_text.rb +15 -0
  53. data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar.rb +20 -0
  54. data/lib/active_record/connection_adapters/sqlserver/type/unicode_varchar_max.rb +20 -0
  55. data/lib/active_record/connection_adapters/sqlserver/type/uuid.rb +23 -0
  56. data/lib/active_record/connection_adapters/sqlserver/type/varbinary.rb +20 -0
  57. data/lib/active_record/connection_adapters/sqlserver/type/varbinary_max.rb +20 -0
  58. data/lib/active_record/connection_adapters/sqlserver/type/varchar.rb +20 -0
  59. data/lib/active_record/connection_adapters/sqlserver/type/varchar_max.rb +20 -0
  60. data/lib/active_record/connection_adapters/sqlserver/utils.rb +118 -12
  61. data/lib/active_record/connection_adapters/sqlserver/version.rb +11 -0
  62. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +133 -198
  63. data/lib/active_record/connection_adapters/sqlserver_column.rb +15 -86
  64. data/lib/active_record/sqlserver_base.rb +2 -0
  65. data/lib/arel/visitors/sqlserver.rb +120 -393
  66. data/lib/{arel/arel_sqlserver.rb → arel_sqlserver.rb} +1 -3
  67. data/test/cases/adapter_test_sqlserver.rb +420 -0
  68. data/test/cases/coerced_tests.rb +642 -0
  69. data/test/cases/column_test_sqlserver.rb +703 -0
  70. data/test/cases/connection_test_sqlserver.rb +216 -0
  71. data/test/cases/database_statements_test_sqlserver.rb +57 -0
  72. data/test/cases/execute_procedure_test_sqlserver.rb +38 -0
  73. data/test/cases/helper_sqlserver.rb +36 -0
  74. data/test/cases/migration_test_sqlserver.rb +66 -0
  75. data/test/cases/order_test_sqlserver.rb +147 -0
  76. data/test/cases/pessimistic_locking_test_sqlserver.rb +90 -0
  77. data/test/cases/schema_dumper_test_sqlserver.rb +175 -0
  78. data/test/cases/schema_test_sqlserver.rb +54 -0
  79. data/test/cases/scratchpad_test_sqlserver.rb +9 -0
  80. data/test/cases/showplan_test_sqlserver.rb +65 -0
  81. data/test/cases/specific_schema_test_sqlserver.rb +118 -0
  82. data/test/cases/transaction_test_sqlserver.rb +61 -0
  83. data/test/cases/utils_test_sqlserver.rb +91 -0
  84. data/test/cases/uuid_test_sqlserver.rb +41 -0
  85. data/test/config.yml +35 -0
  86. data/test/fixtures/1px.gif +0 -0
  87. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +11 -0
  88. data/test/models/sqlserver/customers_view.rb +3 -0
  89. data/test/models/sqlserver/datatype.rb +3 -0
  90. data/test/models/sqlserver/datatype_migration.rb +3 -0
  91. data/test/models/sqlserver/dollar_table_name.rb +3 -0
  92. data/test/models/sqlserver/edge_schema.rb +13 -0
  93. data/test/models/sqlserver/fk_has_fk.rb +3 -0
  94. data/test/models/sqlserver/fk_has_pk.rb +3 -0
  95. data/test/models/sqlserver/natural_pk_data.rb +4 -0
  96. data/test/models/sqlserver/natural_pk_int_data.rb +3 -0
  97. data/test/models/sqlserver/no_pk_data.rb +3 -0
  98. data/test/models/sqlserver/quoted_table.rb +7 -0
  99. data/test/models/sqlserver/quoted_view_1.rb +3 -0
  100. data/test/models/sqlserver/quoted_view_2.rb +3 -0
  101. data/test/models/sqlserver/string_default.rb +3 -0
  102. data/test/models/sqlserver/string_defaults_big_view.rb +3 -0
  103. data/test/models/sqlserver/string_defaults_view.rb +3 -0
  104. data/test/models/sqlserver/tinyint_pk.rb +3 -0
  105. data/test/models/sqlserver/upper.rb +3 -0
  106. data/test/models/sqlserver/uppered.rb +3 -0
  107. data/test/models/sqlserver/uuid.rb +3 -0
  108. data/test/schema/datatypes/2012.sql +64 -0
  109. data/test/schema/sqlserver_specific_schema.rb +181 -0
  110. data/test/support/coerceable_test_sqlserver.rb +45 -0
  111. data/test/support/load_schema_sqlserver.rb +29 -0
  112. data/test/support/minitest_sqlserver.rb +1 -0
  113. data/test/support/paths_sqlserver.rb +48 -0
  114. data/test/support/rake_helpers.rb +41 -0
  115. data/test/support/sql_counter_sqlserver.rb +32 -0
  116. metadata +271 -21
  117. data/CHANGELOG +0 -39
  118. data/VERSION +0 -1
  119. data/lib/active_record/connection_adapters/sqlserver/core_ext/relation.rb +0 -17
  120. data/lib/active_record/sqlserver_test_case.rb +0 -17
  121. data/lib/arel/nodes_sqlserver.rb +0 -14
  122. data/lib/arel/select_manager_sqlserver.rb +0 -62
@@ -1,32 +1,7 @@
1
1
  module ActiveRecord
2
- class LostConnection < WrappedDatabaseException
3
- end
4
2
 
5
3
  class DeadlockVictim < WrappedDatabaseException
6
4
  end
7
5
 
8
- module ConnectionAdapters
9
- module Sqlserver
10
- module Errors
11
- LOST_CONNECTION_EXCEPTIONS = {
12
- dblib: ['TinyTds::Error'],
13
- odbc: ['ODBC::Error']
14
- }.freeze
15
-
16
- LOST_CONNECTION_MESSAGES = {
17
- dblib: [/closed connection/, /dead or not enabled/, /server failed/i],
18
- odbc: [/link failure/, /server failed/, /connection was already closed/, /invalid handle/i]
19
- }.freeze
20
6
 
21
- def lost_connection_exceptions
22
- exceptions = LOST_CONNECTION_EXCEPTIONS[@connection_options[:mode]]
23
- @lost_connection_exceptions ||= exceptions ? exceptions.map { |e| e.constantize rescue nil }.compact : []
24
- end
25
-
26
- def lost_connection_messages
27
- LOST_CONNECTION_MESSAGES[@connection_options[:mode]]
28
- end
29
- end
30
- end
31
- end
32
7
  end
@@ -1,73 +1,25 @@
1
1
  module ActiveRecord
2
2
  module ConnectionAdapters
3
- module Sqlserver
3
+ module SQLServer
4
4
  module Quoting
5
- QUOTED_TRUE, QUOTED_FALSE = '1', '0'
6
- QUOTED_STRING_PREFIX = 'N'
7
-
8
- def quote(value, column = nil)
9
- case value
10
- when String, ActiveSupport::Multibyte::Chars
11
- if column && column.type == :integer && value.blank?
12
- value.to_i.to_s
13
- elsif column && column.type == :binary
14
- column.class.string_to_binary(value)
15
- elsif column && [:uuid, :uniqueidentifier].include?(column.type)
16
- "'#{quote_string(value)}'"
17
- elsif value.is_utf8? || (column && column.type == :string)
18
- "#{quoted_string_prefix}'#{quote_string(value)}'"
19
- else
20
- super
21
- end
22
- when Date, Time
23
- if column && column.sql_type == 'datetime'
24
- "'#{quoted_datetime(value)}'"
25
- elsif column && (column.sql_type == 'datetimeoffset' || column.sql_type == 'time')
26
- "'#{quoted_full_iso8601(value)}'"
27
- else
28
- super
29
- end
30
- when nil
31
- column.respond_to?(:sql_type) && column.sql_type == 'timestamp' ? 'DEFAULT' : super
32
- else
33
- super
34
- end
35
- end
36
5
 
37
- def quoted_string_prefix
38
- QUOTED_STRING_PREFIX
39
- end
6
+ QUOTED_TRUE = '1'
7
+ QUOTED_FALSE = '0'
8
+ QUOTED_STRING_PREFIX = 'N'
40
9
 
41
- def quote_string(string)
42
- string.to_s.gsub(/\'/, "''")
10
+ def quote_string(s)
11
+ SQLServer::Utils.quote_string(s)
43
12
  end
44
13
 
45
14
  def quote_column_name(name)
46
- schema_cache.quote_name(name)
15
+ SQLServer::Utils.extract_identifiers(name).quoted
47
16
  end
48
17
 
49
- def quote_table_name(name)
50
- quote_column_name(name)
51
- end
52
-
53
- def quote_database_name(name)
54
- schema_cache.quote_name(name, false)
55
- end
56
-
57
- # Does not quote function default values for UUID columns
58
18
  def quote_default_value(value, column)
59
19
  if column.type == :uuid && value =~ /\(\)/
60
20
  value
61
21
  else
62
- quote(value)
63
- end
64
- end
65
-
66
- def substitute_at(column, index)
67
- if column.respond_to?(:sql_type) && column.sql_type == 'timestamp'
68
- nil
69
- else
70
- Arel::Nodes::BindParam.new "@#{index}"
22
+ quote(value, column)
71
23
  end
72
24
  end
73
25
 
@@ -75,47 +27,56 @@ module ActiveRecord
75
27
  QUOTED_TRUE
76
28
  end
77
29
 
30
+ def unquoted_true
31
+ 1
32
+ end
33
+
78
34
  def quoted_false
79
35
  QUOTED_FALSE
80
36
  end
81
37
 
82
- def quoted_datetime(value)
83
- if value.acts_like?(:time)
84
- time_zone_qualified_value = quoted_value_acts_like_time_filter(value)
85
- if value.is_a?(Date)
86
- time_zone_qualified_value.iso8601(3).to(18)
38
+ def unquoted_false
39
+ 0
40
+ end
41
+
42
+ def quoted_date(value)
43
+ SQLServer::Utils.with_sqlserver_db_date_formats do
44
+ if value.acts_like?(:time) && value.respond_to?(:usec)
45
+ precision = (BigDecimal(value.usec.to_s) / 1_000_000).round(3).to_s.split('.').last
46
+ "#{super}.#{precision}"
47
+ elsif value.acts_like?(:date)
48
+ value.to_s(:_sqlserver_dateformat)
87
49
  else
88
- time_zone_qualified_value.iso8601(3).to(22)
50
+ super
89
51
  end
90
- else
91
- quoted_date(value)
92
52
  end
93
53
  end
94
54
 
95
- def quoted_full_iso8601(value)
96
- if value.acts_like?(:time)
97
- value.is_a?(Date) ? quoted_value_acts_like_time_filter(value).to_time.xmlschema.to(18) : quoted_value_acts_like_time_filter(value).iso8601(7).to(22)
98
- else
99
- quoted_date(value)
100
- end
101
- end
102
55
 
103
- def quoted_date(value)
104
- if value.acts_like?(:time) && value.respond_to?(:usec)
105
- "#{super}.#{sprintf('%03d', value.usec / 1000)}"
106
- elsif value.acts_like?(:date)
107
- value.to_s(:_sqlserver_dateformat)
56
+ private
57
+
58
+ def _quote(value)
59
+ case value
60
+ when Type::Binary::Data
61
+ "0x#{value.hex}"
62
+ when SQLServer::Type::Quoter
63
+ value.quote_ss_value
64
+ when String, ActiveSupport::Multibyte::Chars
65
+ if value.is_utf8?
66
+ "#{QUOTED_STRING_PREFIX}#{super}"
67
+ else
68
+ super
69
+ end
108
70
  else
109
71
  super
110
72
  end
111
73
  end
112
74
 
113
- protected
114
-
115
75
  def quoted_value_acts_like_time_filter(value)
116
76
  zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
117
77
  value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value
118
78
  end
79
+
119
80
  end
120
81
  end
121
82
  end
@@ -1,89 +1,113 @@
1
1
  module ActiveRecord
2
2
  module ConnectionAdapters
3
- module Sqlserver
3
+ module SQLServer
4
4
  class SchemaCache < ActiveRecord::ConnectionAdapters::SchemaCache
5
- attr_reader :view_information
6
5
 
7
6
  def initialize(conn)
8
7
  super
9
- @table_names = nil
10
- @view_names = nil
8
+ @views = {}
11
9
  @view_information = {}
12
- @quoted_names = {}
13
10
  end
14
11
 
15
12
  # Superclass Overrides
16
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
+
17
19
  def table_exists?(table_name)
18
- return false if table_name.blank?
19
- key = table_name_key(table_name)
20
- return @tables[key] if @tables.key? key
21
- @tables[key] = connection.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
+ }]
22
41
  end
23
42
 
24
43
  def clear!
25
44
  super
26
- @table_names = nil
27
- @view_names = nil
45
+ @views.clear
28
46
  @view_information.clear
29
- @quoted_names.clear
30
47
  end
31
48
 
32
- def clear_table_cache!(table_name)
33
- key = table_name_key(table_name)
34
- super(key)
35
- super(table_name)
36
- # SQL Server Specific
37
- if @table_names
38
- @table_names.delete key
39
- @table_names.delete table_name
40
- end
41
- if @view_names
42
- @view_names.delete key
43
- @view_names.delete table_name
44
- end
45
- @view_information.delete key
49
+ def size
50
+ super + [@views, @view_information].map{ |x| x.size }.inject(:+)
46
51
  end
47
52
 
48
- # SQL Server Specific
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
49
62
 
50
- def table_names
51
- @table_names ||= connection.tables
63
+ def marshal_dump
64
+ super + [@views, @view_information]
52
65
  end
53
66
 
54
- def view_names
55
- @view_names ||= connection.views
67
+ def marshal_load(array)
68
+ @views, @view_information = array[-2..-1]
69
+ super(array[0..-3])
56
70
  end
57
71
 
72
+ # SQL Server Specific
73
+
58
74
  def view_exists?(table_name)
59
- table_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)
60
79
  end
61
80
 
62
81
  def view_information(table_name)
63
- key = table_name_key(table_name)
64
- return @view_information[key] if @view_information.key? key
65
- @view_information[key] = connection.send(: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)
66
85
  end
67
86
 
68
- def quote_name(name, split_on_dots = true)
69
- return @quoted_names[name] if @quoted_names.key? name
70
87
 
71
- @quoted_names[name] = if split_on_dots
72
- name.to_s.split('.').map { |n| quote_name_part(n) }.join('.')
73
- else
74
- quote_name_part(name.to_s)
75
- end
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
76
96
  end
77
97
 
78
- private
98
+ def prepare_tables_and_views
99
+ prepare_views if @views.empty?
100
+ prepare_tables if @tables.empty?
101
+ end
79
102
 
80
- def quote_name_part(part)
81
- part =~ /^\[.*\]$/ ? part : "[#{part.to_s.gsub(']', ']]')}]"
103
+ def prepare_tables
104
+ connection.tables.each { |table| @tables[key(table)] = true }
82
105
  end
83
106
 
84
- def table_name_key(table_name)
85
- Utils.unqualify_table_name(table_name)
107
+ def prepare_views
108
+ connection.views.each { |view| @views[key(view)] = true }
86
109
  end
110
+
87
111
  end
88
112
  end
89
113
  end
@@ -1,7 +1,8 @@
1
1
  module ActiveRecord
2
2
  module ConnectionAdapters
3
- module Sqlserver
3
+ module SQLServer
4
4
  class SchemaCreation < AbstractAdapter::SchemaCreation
5
+
5
6
  private
6
7
 
7
8
  def visit_ColumnDefinition(o)
@@ -13,43 +14,26 @@ module ActiveRecord
13
14
  sql
14
15
  end
15
16
 
16
- def add_column_options!(sql, options)
17
- column = options.fetch(:column) { return super }
18
- if [:uniqueidentifier, :uuid].include?(column.type) && options[:default] =~ /\(\)/
19
- sql << " DEFAULT #{options.delete(:default)}"
20
- super
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}"
21
22
  else
23
+ o.instance_variable_set :@as, nil
22
24
  super
23
25
  end
24
26
  end
25
27
 
26
- def visit_TableDefinition(o)
27
- quoted_name = "#{quote_table_name((o.temporary ? '#' : '') + o.name.to_s)} "
28
-
29
- if o.as
30
- if o.as.is_a?(ActiveRecord::Relation)
31
- select = o.as.to_sql
32
- elsif o.as.is_a?(String)
33
- select = o.as
34
- else
35
- raise 'Only able to generate a table from a SELECT statement passed as a String or ActiveRecord::Relation'
36
- end
37
-
38
- create_sql = 'SELECT * INTO '
39
- create_sql << quoted_name
40
- create_sql << 'FROM ('
41
- create_sql << select
42
- create_sql << ') AS __sq'
43
-
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)}"
44
32
  else
45
- create_sql = "CREATE TABLE "
46
- create_sql << quoted_name
47
- create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) "
48
- create_sql << "#{o.options}"
33
+ super
49
34
  end
50
-
51
- create_sql
52
35
  end
36
+
53
37
  end
54
38
  end
55
39
  end
@@ -1,7 +1,8 @@
1
1
  module ActiveRecord
2
2
  module ConnectionAdapters
3
- module Sqlserver
3
+ module SQLServer
4
4
  module SchemaStatements
5
+
5
6
  def native_database_types
6
7
  @native_database_types ||= initialize_native_database_types.freeze
7
8
  end
@@ -12,7 +13,7 @@ module ActiveRecord
12
13
 
13
14
  def table_exists?(table_name)
14
15
  return false if table_name.blank?
15
- unquoted_table_name = Utils.unqualify_table_name(table_name)
16
+ unquoted_table_name = SQLServer::Utils.extract_identifiers(table_name).object
16
17
  super || tables.include?(unquoted_table_name) || views.include?(unquoted_table_name)
17
18
  end
18
19
 
@@ -31,13 +32,12 @@ module ActiveRecord
31
32
  else
32
33
  name = index[:index_name]
33
34
  unique = index[:index_description] =~ /unique/
34
- where = select_value("SELECT [filter_definition] FROM sys.indexes WHERE name = #{quote(name)}")
35
35
  columns = index[:index_keys].split(',').map do |column|
36
36
  column.strip!
37
37
  column.gsub! '(-)', '' if column.ends_with?('(-)')
38
38
  column
39
39
  end
40
- indexes << IndexDefinition.new(table_name, name, unique, columns, nil, nil, where)
40
+ indexes << IndexDefinition.new(table_name, name, unique, columns)
41
41
  end
42
42
  end
43
43
  end
@@ -45,18 +45,14 @@ module ActiveRecord
45
45
  def columns(table_name, _name = nil)
46
46
  return [] if table_name.blank?
47
47
  column_definitions(table_name).map do |ci|
48
- sqlserver_options = ci.except(:name, :default_value, :type, :null).merge(database_year: database_year)
49
- SQLServerColumn.new ci[:name], ci[:default_value], ci[:type], ci[:null], sqlserver_options
48
+ sqlserver_options = ci.slice :ordinal_position, :is_primary, :is_identity, :default_function, :table_name
49
+ cast_type = lookup_cast_type(ci[:type])
50
+ new_column ci[:name], ci[:default_value], cast_type, ci[:type], ci[:null], sqlserver_options
50
51
  end
51
52
  end
52
53
 
53
- # like postgres, sqlserver requires the ORDER BY columns in the select list for distinct queries, and
54
- # requires that the ORDER BY include the distinct column. Unfortunately, sqlserver does not support
55
- # DISTINCT ON () like Posgres, or FIRST_VALUE() like Oracle (at least before SQL Server 2012). Because
56
- # of these facts, we don't actually add any extra columns for distinct, but instead have to create
57
- # a subquery with ROW_NUMBER() and DENSE_RANK() in our monkey-patches to Arel.
58
- def columns_for_distinct(columns, _orders) #:nodoc:
59
- columns
54
+ def new_column(name, default, cast_type, sql_type = nil, null = true, sqlserver_options={})
55
+ SQLServerColumn.new name, default, cast_type, sql_type, null, sqlserver_options
60
56
  end
61
57
 
62
58
  def rename_table(table_name, new_name)
@@ -64,7 +60,7 @@ module ActiveRecord
64
60
  rename_table_indexes(table_name, new_name)
65
61
  end
66
62
 
67
- def remove_column(table_name, column_name, type = nil, options = {})
63
+ def remove_column(table_name, column_name, _type = nil)
68
64
  raise ArgumentError.new('You must specify at least one column name. Example: remove_column(:people, :first_name)') if column_name.is_a? Array
69
65
  remove_check_constraints(table_name, column_name)
70
66
  remove_default_constraint(table_name, column_name)
@@ -76,7 +72,6 @@ module ActiveRecord
76
72
  sql_commands = []
77
73
  indexes = []
78
74
  column_object = schema_cache.columns(table_name).find { |c| c.name.to_s == column_name.to_s }
79
-
80
75
  if options_include_default?(options) || (column_object && column_object.type != type.to_sym)
81
76
  remove_default_constraint(table_name, column_name)
82
77
  indexes = indexes(table_name).select { |index| index.columns.include?(column_name.to_s) }
@@ -88,7 +83,6 @@ module ActiveRecord
88
83
  if options_include_default?(options)
89
84
  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
85
  end
91
-
92
86
  # Add any removed indexes back
93
87
  indexes.each do |index|
94
88
  sql_commands << "CREATE INDEX #{quote_table_name(index.name)} ON #{quote_table_name(table_name)} (#{index.columns.map { |c| quote_column_name(c) }.join(', ')})"
@@ -106,13 +100,16 @@ module ActiveRecord
106
100
  def rename_column(table_name, column_name, new_column_name)
107
101
  schema_cache.clear_table_cache!(table_name)
108
102
  detect_column_for! table_name, column_name
109
- do_execute "EXEC sp_rename '#{table_name}.#{column_name}', '#{new_column_name}', 'COLUMN'"
103
+ identifier = SQLServer::Utils.extract_identifiers("#{table_name}.#{column_name}")
104
+ execute_procedure :sp_rename, identifier.quoted, new_column_name, 'COLUMN'
110
105
  rename_column_indexes(table_name, column_name, new_column_name)
111
106
  schema_cache.clear_table_cache!(table_name)
112
107
  end
113
108
 
114
109
  def rename_index(table_name, old_name, new_name)
115
- execute "EXEC sp_rename N'#{table_name}.#{old_name}', N'#{new_name}', N'INDEX'"
110
+ 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
111
+ identifier = SQLServer::Utils.extract_identifiers("#{table_name}.#{old_name}")
112
+ execute_procedure :sp_rename, identifier.quoted, new_name, 'INDEX'
116
113
  end
117
114
 
118
115
  def remove_index!(table_name, index_name)
@@ -135,6 +132,15 @@ module ActiveRecord
135
132
  end
136
133
  end
137
134
 
135
+ def columns_for_distinct(columns, orders)
136
+ order_columns = orders.reject(&:blank?).map{ |s|
137
+ s = s.to_sql unless s.is_a?(String)
138
+ s.gsub(/\s+(?:ASC|DESC)\b/i, '')
139
+ .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, '')
140
+ }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
141
+ [super, *order_columns].join(', ')
142
+ end
143
+
138
144
  def change_column_null(table_name, column_name, allow_null, default = nil)
139
145
  column = detect_column_for! table_name, column_name
140
146
  if !allow_null.nil? && allow_null == false && !default.nil?
@@ -151,6 +157,7 @@ module ActiveRecord
151
157
  tables('VIEW')
152
158
  end
153
159
 
160
+
154
161
  protected
155
162
 
156
163
  # === SQLServer Specific ======================================== #
@@ -158,34 +165,39 @@ module ActiveRecord
158
165
  def initialize_native_database_types
159
166
  {
160
167
  primary_key: 'int NOT NULL IDENTITY(1,1) PRIMARY KEY',
161
- string: { name: native_string_database_type, limit: 255 },
162
- text: { name: native_text_database_type },
163
168
  integer: { name: 'int', limit: 4 },
164
- float: { name: 'float', limit: 8 },
169
+ bigint: { name: 'bigint' },
170
+ boolean: { name: 'bit' },
165
171
  decimal: { name: 'decimal' },
172
+ money: { name: 'money' },
173
+ smallmoney: { name: 'smallmoney' },
174
+ float: { name: 'float' },
175
+ real: { name: 'real' },
176
+ date: { name: 'date' },
166
177
  datetime: { name: 'datetime' },
167
178
  timestamp: { name: 'datetime' },
168
- time: { name: native_time_database_type },
169
- date: { name: native_date_database_type },
170
- binary: { name: native_binary_database_type },
171
- boolean: { name: 'bit' },
172
- uuid: { name: 'uniqueidentifier' },
173
- # These are custom types that may move somewhere else for good schema_dumper.rb hacking to output them.
179
+ time: { name: 'time' },
174
180
  char: { name: 'char' },
181
+ varchar: { name: 'varchar', limit: 8000 },
175
182
  varchar_max: { name: 'varchar(max)' },
183
+ text_basic: { name: 'text' },
176
184
  nchar: { name: 'nchar' },
177
- nvarchar: { name: 'nvarchar', limit: 255 },
178
- nvarchar_max: { name: 'nvarchar(max)' },
185
+ string: { name: 'nvarchar', limit: 4000 },
186
+ text: { name: 'nvarchar(max)' },
179
187
  ntext: { name: 'ntext' },
188
+ binary_basic: { name: 'binary' },
189
+ varbinary: { name: 'varbinary', limit: 8000 },
190
+ binary: { name: 'varbinary(max)' },
191
+ uuid: { name: 'uniqueidentifier' },
180
192
  ss_timestamp: { name: 'timestamp' }
181
193
  }
182
194
  end
183
195
 
184
196
  def column_definitions(table_name)
185
- db_name = Utils.unqualify_db_name(table_name)
186
- db_name_with_period = "#{db_name}." if db_name
187
- table_schema = Utils.unqualify_table_schema(table_name)
188
- table_name = Utils.unqualify_table_name(table_name)
197
+ identifier = SQLServer::Utils.extract_identifiers(table_name)
198
+ database = "#{identifier.database_quoted}." if identifier.database_quoted
199
+ view_exists = schema_cache.view_exists?(table_name)
200
+ view_tblnm = table_name_or_views_table_name(table_name) if view_exists
189
201
  sql = %{
190
202
  SELECT DISTINCT
191
203
  #{lowercase_schema_reflection_sql('columns.TABLE_NAME')} AS table_name,
@@ -194,10 +206,11 @@ module ActiveRecord
194
206
  columns.COLUMN_DEFAULT AS default_value,
195
207
  columns.NUMERIC_SCALE AS numeric_scale,
196
208
  columns.NUMERIC_PRECISION AS numeric_precision,
209
+ columns.DATETIME_PRECISION AS datetime_precision,
197
210
  columns.ordinal_position,
198
211
  CASE
199
212
  WHEN columns.DATA_TYPE IN ('nchar','nvarchar') THEN columns.CHARACTER_MAXIMUM_LENGTH
200
- ELSE COL_LENGTH('#{db_name_with_period}'+columns.TABLE_SCHEMA+'.'+columns.TABLE_NAME, columns.COLUMN_NAME)
213
+ ELSE COL_LENGTH('#{database}'+columns.TABLE_SCHEMA+'.'+columns.TABLE_NAME, columns.COLUMN_NAME)
201
214
  END AS [length],
202
215
  CASE
203
216
  WHEN columns.IS_NULLABLE = 'YES' THEN 1
@@ -208,63 +221,80 @@ module ActiveRecord
208
221
  ELSE NULL
209
222
  END AS [is_primary],
210
223
  c.is_identity AS [is_identity]
211
- FROM #{db_name_with_period}INFORMATION_SCHEMA.COLUMNS columns
212
- LEFT OUTER JOIN #{db_name_with_period}INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC
224
+ FROM #{database}INFORMATION_SCHEMA.COLUMNS columns
225
+ LEFT OUTER JOIN #{database}INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TC
213
226
  ON TC.TABLE_NAME = columns.TABLE_NAME
214
227
  AND TC.CONSTRAINT_TYPE = N'PRIMARY KEY'
215
- LEFT OUTER JOIN #{db_name_with_period}INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
228
+ LEFT OUTER JOIN #{database}INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
216
229
  ON KCU.COLUMN_NAME = columns.COLUMN_NAME
217
230
  AND KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
218
231
  AND KCU.CONSTRAINT_CATALOG = TC.CONSTRAINT_CATALOG
219
232
  AND KCU.CONSTRAINT_SCHEMA = TC.CONSTRAINT_SCHEMA
220
- INNER JOIN #{db_name}.sys.schemas AS s
233
+ INNER JOIN #{identifier.database_quoted}.sys.schemas AS s
221
234
  ON s.name = columns.TABLE_SCHEMA
222
235
  AND s.schema_id = s.schema_id
223
- INNER JOIN #{db_name}.sys.objects AS o
236
+ INNER JOIN #{identifier.database_quoted}.sys.objects AS o
224
237
  ON s.schema_id = o.schema_id
225
238
  AND o.is_ms_shipped = 0
226
239
  AND o.type IN ('U', 'V')
227
240
  AND o.name = columns.TABLE_NAME
228
- INNER JOIN #{db_name}.sys.columns AS c
241
+ INNER JOIN #{identifier.database_quoted}.sys.columns AS c
229
242
  ON o.object_id = c.object_id
230
243
  AND c.name = columns.COLUMN_NAME
231
244
  WHERE columns.TABLE_NAME = @0
232
- AND columns.TABLE_SCHEMA = #{table_schema.blank? ? 'schema_name()' : '@1'}
245
+ AND columns.TABLE_SCHEMA = #{identifier.schema.blank? ? 'schema_name()' : '@1'}
233
246
  ORDER BY columns.ordinal_position
234
247
  }.gsub(/[ \t\r\n]+/, ' ')
235
- binds = [['table_name', table_name]]
236
- binds << ['table_schema', table_schema] unless table_schema.blank?
237
- results = do_exec_query(sql, 'SCHEMA', binds)
248
+ binds = [[info_schema_table_name_column, identifier.object]]
249
+ binds << [info_schema_table_schema_column, identifier.schema] unless identifier.schema.blank?
250
+ results = sp_executesql(sql, 'SCHEMA', binds)
238
251
  results.map do |ci|
239
252
  ci = ci.symbolize_keys
253
+ ci[:_type] = ci[:type]
254
+ ci[:table_name] = view_tblnm || table_name
240
255
  ci[:type] = case ci[:type]
241
256
  when /^bit|image|text|ntext|datetime$/
242
257
  ci[:type]
258
+ when /^time$/i
259
+ "#{ci[:type]}(#{ci[:datetime_precision]})"
243
260
  when /^numeric|decimal$/i
244
261
  "#{ci[:type]}(#{ci[:numeric_precision]},#{ci[:numeric_scale]})"
245
262
  when /^float|real$/i
246
- "#{ci[:type]}(#{ci[:numeric_precision]})"
247
- when /^char|nchar|varchar|nvarchar|varbinary|bigint|int|smallint$/
263
+ "#{ci[:type]}"
264
+ when /^char|nchar|varchar|nvarchar|binary|varbinary|bigint|int|smallint$/
248
265
  ci[:length].to_i == -1 ? "#{ci[:type]}(max)" : "#{ci[:type]}(#{ci[:length]})"
249
266
  else
250
267
  ci[:type]
251
268
  end
252
- if ci[:default_value].nil? && schema_cache.view_names.include?(table_name)
253
- real_table_name = table_name_or_views_table_name(table_name)
254
- real_column_name = views_real_column_name(table_name, ci[:name])
255
- col_default_sql = "SELECT c.COLUMN_DEFAULT FROM #{db_name_with_period}INFORMATION_SCHEMA.COLUMNS c WHERE c.TABLE_NAME = '#{real_table_name}' AND c.COLUMN_NAME = '#{real_column_name}'"
256
- ci[:default_value] = select_value col_default_sql, 'SCHEMA'
269
+ ci[:default_value],
270
+ ci[:default_function] = begin
271
+ default = ci[:default_value]
272
+ if default.nil? && view_exists
273
+ default = select_value "
274
+ SELECT c.COLUMN_DEFAULT
275
+ FROM #{database}INFORMATION_SCHEMA.COLUMNS c
276
+ WHERE c.TABLE_NAME = '#{view_tblnm}'
277
+ AND c.COLUMN_NAME = '#{views_real_column_name(table_name, ci[:name])}'".squish, 'SCHEMA'
278
+ end
279
+ case default
280
+ when nil
281
+ [nil, nil]
282
+ when /\A\((\w+\(\))\)\Z/
283
+ default_function = Regexp.last_match[1]
284
+ [nil, default_function]
285
+ when /\A\(N'(.*)'\)\Z/m
286
+ string_literal = SQLServer::Utils.unquote_string(Regexp.last_match[1])
287
+ [string_literal, nil]
288
+ else
289
+ type = case ci[:type]
290
+ when /smallint|int|bigint/ then ci[:_type]
291
+ else ci[:type]
292
+ end
293
+ value = default.match(/\A\((.*)\)\Z/m)[1]
294
+ value = select_value "SELECT CAST(#{value} AS #{type}) AS value", 'SCHEMA'
295
+ [value, nil]
296
+ end
257
297
  end
258
- ci[:default_value] = case ci[:default_value]
259
- when nil, '(null)', '(NULL)'
260
- nil
261
- when /\A\((\w+\(\))\)\Z/
262
- ci[:default_function] = Regexp.last_match[1]
263
- nil
264
- else
265
- match_data = ci[:default_value].match(/\A\(+N?'?(.*?)'?\)+\Z/m)
266
- match_data ? match_data[1].gsub("''", "'") : nil
267
- end
268
298
  ci[:null] = ci[:is_nullable].to_i == 1
269
299
  ci.delete(:is_nullable)
270
300
  ci[:is_primary] = ci[:is_primary].to_i == 1
@@ -273,6 +303,14 @@ module ActiveRecord
273
303
  end
274
304
  end
275
305
 
306
+ def info_schema_table_name_column
307
+ @info_schema_table_name_column ||= new_column 'table_name', nil, lookup_cast_type('nvarchar(128)'), 'nvarchar(128)', true
308
+ end
309
+
310
+ def info_schema_table_schema_column
311
+ @info_schema_table_schema_column ||= new_column 'table_schema', nil, lookup_cast_type('nvarchar(128)'), 'nvarchar(128)', true
312
+ end
313
+
276
314
  def remove_check_constraints(table_name, column_name)
277
315
  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'
278
316
  constraints.each do |constraint|
@@ -298,13 +336,14 @@ module ActiveRecord
298
336
  # === SQLServer Specific (Misc Helpers) ========================= #
299
337
 
300
338
  def get_table_name(sql)
301
- if sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)(\s+INTO)?\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
339
+ tn = if sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)(\s+INTO)?\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
302
340
  Regexp.last_match[3] || Regexp.last_match[4]
303
341
  elsif sql =~ /FROM\s+([^\(\s]+)\s*/i
304
342
  Regexp.last_match[1]
305
343
  else
306
344
  nil
307
345
  end
346
+ SQLServer::Utils.extract_identifiers(tn).object
308
347
  end
309
348
 
310
349
  def default_constraint_name(table_name, column_name)
@@ -330,25 +369,24 @@ module ActiveRecord
330
369
  end
331
370
 
332
371
  def view_information(table_name)
333
- table_name = Utils.unqualify_table_name(table_name)
334
- view_info = select_one "SELECT * FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = '#{table_name}'", 'SCHEMA'
372
+ identifier = SQLServer::Utils.extract_identifiers(table_name)
373
+ view_info = select_one "SELECT * FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = '#{identifier.object}'", 'SCHEMA'
335
374
  if view_info
336
375
  view_info = view_info.with_indifferent_access
337
376
  if view_info[:VIEW_DEFINITION].blank? || view_info[:VIEW_DEFINITION].length == 4000
338
377
  view_info[:VIEW_DEFINITION] = begin
339
- select_values("EXEC sp_helptext #{quote_table_name(table_name)}", 'SCHEMA').join
340
- rescue
341
- warn "No view definition found, possible permissions problem.\nPlease run GRANT VIEW DEFINITION TO your_user;"
342
- nil
343
- end
378
+ select_values("EXEC sp_helptext #{identifier.object_quoted}", 'SCHEMA').join
379
+ rescue
380
+ warn "No view definition found, possible permissions problem.\nPlease run GRANT VIEW DEFINITION TO your_user;"
381
+ nil
382
+ end
344
383
  end
345
384
  end
346
385
  view_info
347
386
  end
348
387
 
349
388
  def table_name_or_views_table_name(table_name)
350
- unquoted_table_name = Utils.unqualify_table_name(table_name)
351
- schema_cache.view_names.include?(unquoted_table_name) ? view_table_name(unquoted_table_name) : unquoted_table_name
389
+ schema_cache.view_exists?(table_name) ? view_table_name(table_name) : table_name
352
390
  end
353
391
 
354
392
  def views_real_column_name(table_name, column_name)
@@ -373,51 +411,17 @@ module ActiveRecord
373
411
  !(sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)/i).nil?
374
412
  end
375
413
 
376
- def strip_ident_from_update(sql)
377
- # We can't update Identiy columns in sqlserver. So, strip out the id from the update.
378
- # There has to be a better way to handle this, but this'll do for now.
379
- table_name = get_table_name(sql)
380
- id_column = identity_column(table_name)
381
-
382
- if id_column
383
- regex_col_name = Regexp.quote(quote_column_name(id_column.name))
384
- if sql =~ /, #{regex_col_name} = @?[0-9]*/
385
- sql = sql.gsub(/, #{regex_col_name} = @?[0-9]*/, '')
386
- elsif sql =~ /\s#{regex_col_name} = @?[0-9]*,/
387
- sql = sql.gsub(/\s#{regex_col_name} = @?[0-9]*,/, '')
388
- end
389
- end
390
- sql
391
- end
392
-
393
- def update_sql?(sql)
394
- !(sql =~ /^\s*(UPDATE|EXEC sp_executesql N'UPDATE)/i).nil?
395
- end
396
-
397
- def with_identity_insert_enabled(table_name)
398
- table_name = quote_table_name(table_name_or_views_table_name(table_name))
399
- set_identity_insert(table_name, true)
400
- yield
401
- ensure
402
- set_identity_insert(table_name, false)
403
- end
404
-
405
- def set_identity_insert(table_name, enable = true)
406
- sql = "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
407
- do_execute sql, 'SCHEMA'
408
- rescue Exception
409
- raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
410
- end
411
-
412
414
  def identity_column(table_name)
413
415
  schema_cache.columns(table_name).find(&:is_identity?)
414
416
  end
415
417
 
418
+
416
419
  private
417
420
 
418
421
  def create_table_definition(name, temporary, options, as = nil)
419
- TableDefinition.new native_database_types, name, temporary, options, as
422
+ SQLServer::TableDefinition.new native_database_types, name, temporary, options, as
420
423
  end
424
+
421
425
  end
422
426
  end
423
427
  end