activerecord-sqlserver-adapter 4.1.8 → 4.2.0.pre

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