Skip to content

Commit 1aac9ba

Browse files
committed
Implement very clean national/unicode column quoting by creating a #quoted_utf8_value method hooked into #quote. Add many tests for making sure sql types are correct with correct limiting in columns for all different types. Bump to first release version 2.2.0.
1 parent 7b22ec6 commit 1aac9ba

File tree

9 files changed

+421
-149
lines changed

9 files changed

+421
-149
lines changed

2000-2005-adapter.gemspec

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Gem::Specification.new do |s|
22
s.name = "2000-2005-adapter"
3-
s.version = "1.0.1"
4-
s.date = "2008-11-19"
3+
s.version = "2.2.0"
4+
s.date = "2008-11-21"
55
s.summary = "SQL Server 2000 & 2005 Adapter For Rails."
66
s.email = "ken@metaskills.net"
77
s.homepage = "http://github.com/rails-sqlserver/2000-2005-adapter/"
@@ -37,11 +37,12 @@ Gem::Specification.new do |s|
3737
"test/cases/schema_dumper_test_sqlserver.rb",
3838
"test/cases/specific_schema_test_sqlserver.rb",
3939
"test/cases/sqlserver_helper.rb",
40+
"test/cases/table_name_test_sqlserver.rb"
41+
"test/cases/unicode_test_sqlserver.rb",
4042
"test/connections/native_sqlserver/connection.rb",
4143
"test/connections/native_sqlserver_odbc/connection.rb",
4244
"test/migrations/transaction_table/1_table_will_never_be_created.rb",
43-
"test/schema/sqlserver_specific_schema.rb",
44-
"test/schema/table_name_test_sqlserver.rb" ]
45+
"test/schema/sqlserver_specific_schema.rb" ]
4546
s.rdoc_options = ["--line-numbers", "--inline-source", "--main", "README.rdoc"]
4647
s.extra_rdoc_files = ["README.rdoc"]
4748
end

README.rdoc

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,43 @@ The SQL Server adapter for rails is back for ActiveRecord 2.2 and up! We are cur
1414
* Enabled #case_sensitive_equality_operator used by unique validations.
1515
* Unicode character support for nchar, nvarchar and ntext data types.
1616

17-
==== Native Text Data Type Accessor
17+
==== Date/Time Data Type Hinting
18+
19+
Both SQL Server 2000 and 2005 do not have native data types for just 'date' or 'time', it only has 'datetime'. To pass the ActiveRecord tests we implemented two simple class methods that can teach your models to coerce column information to be cast correctly. Simply past a list of symbols to either the <tt>coerce_sqlserver_date</tt> or <tt>coerce_sqlserver_time</tt> methods that correspond to 'datetime' columns that need to be cast correctly.
20+
21+
class Topic < ActiveRecord::Base
22+
coerce_sqlserver_date :last_read
23+
coerce_sqlserver_time :bonus_time
24+
end
25+
26+
This implementation has some limitations. To date we can only coerce date/time types for models that conform to the expected ActiveRecord class to table naming convention. So a table of 'foo_bar_widgets' will look for coerced types in the FooBarWidget class if it exists.
27+
28+
==== Native Data Type Support
29+
30+
Currently the following custom data types have been tested for schema definitions.
31+
32+
* char
33+
* nchar
34+
* nvarchar
35+
* ntext
36+
* varchar(max) for SQL Server 2005 only.
37+
* nvarchar(max) for SQL Server 2005 only.
38+
39+
For example:
40+
41+
create_table :sql_server_custom_types, :force => true do |t|
42+
t.column :ten_code, :char, :limit => 10
43+
t.column :ten_code_utf8, :nchar, :limit => 10
44+
t.column :title_utf8, :nvarchar
45+
t.column :body, :varchar_max # Creates varchar(max)
46+
t.column :body_utf8, :ntext
47+
t.column :body2_utf8, :nvarchar_max # Creates nvarchar(max)
48+
end
49+
50+
Manually creating a varchar(max) on SQL Server 2005 is not necessary since this is the default type created when specifying a :text field. As time goes on we will be testing other SQL Server specific data types are handled correctly when created in a migration.
51+
52+
53+
==== Native Text/Binary Data Type Accessor
1854

1955
To pass the ActiveRecord tests we had to implement an class accessor for the native type created for :text columns. By default any :text column created by migrations will create these native types.
2056

@@ -25,16 +61,10 @@ During testing this type is set to 'varchar(8000)' for both versions. The reason
2561

2662
ActiveRecord::ConnectionAdapters::SQLServerAdapter.native_text_database_type = 'varchar(8000)'
2763

28-
==== Date/Time Data Type Hinting
29-
30-
Both SQL Server 2000 and 2005 do not have native data types for just 'date' or 'time', it only has 'datetime'. To pass the ActiveRecord tests we implemented two simple class methods that can teach your models to coerce column information to be cast correctly. Simply past a list of symbols to either the <tt>coerce_sqlserver_date</tt> or <tt>coerce_sqlserver_time</tt> methods that correspond to 'datetime' columns that need to be cast correctly.
64+
By default any :binary column created by migrations will create these native types
3165

32-
class Topic < ActiveRecord::Base
33-
coerce_sqlserver_date :last_read
34-
coerce_sqlserver_time :bonus_time
35-
end
36-
37-
This implementation has some limitations. To date we can only coerce date/time types for models that conform to the expected ActiveRecord class to table naming convention. So a table of 'foo_bar_widgets' will look for coerced types in the FooBarWidget class if it exists.
66+
* SQL Server 2000 is 'image'
67+
* SQL Server 2005 is 'varbinary(max)'
3868

3969

4070
== Versions

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 64 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def is_special?
6161
end
6262

6363
def is_utf8?
64-
sql_type =~ /nvarchar|ntext|nchar|nvarchar(max)/i
64+
sql_type =~ /nvarchar|ntext|nchar/i
6565
end
6666

6767
def table_name
@@ -77,10 +77,16 @@ def table_klass
7777

7878
def extract_limit(sql_type)
7979
case sql_type
80-
when /^smallint/i then 2
81-
when /^int/i then 4
82-
when /^bigint/i then 8
83-
else super
80+
when /^smallint/i
81+
2
82+
when /^int/i
83+
4
84+
when /^bigint/i
85+
8
86+
when /\(max\)/, /decimal/, /numeric/
87+
nil
88+
else
89+
super
8490
end
8591
end
8692

@@ -146,14 +152,14 @@ class SQLServerAdapter < AbstractAdapter
146152
ADAPTER_NAME = 'SQLServer'.freeze
147153
DATABASE_VERSION_REGEXP = /Microsoft SQL Server\s+(\d{4})/
148154
SUPPORTED_VERSIONS = [2000,2005].freeze
149-
LIMITABLE_TYPES = [:string,:integer,:float].freeze
155+
LIMITABLE_TYPES = ['string','integer','float','char','nchar','varchar','nvarchar'].freeze
150156

151-
cattr_accessor :native_text_database_type
157+
cattr_accessor :native_text_database_type, :native_binary_database_type
152158

153159
class << self
154160

155161
def type_limitable?(type)
156-
LIMITABLE_TYPES.include?(type.to_sym)
162+
LIMITABLE_TYPES.include?(type.to_s)
157163
end
158164

159165
end
@@ -205,11 +211,22 @@ def native_text_database_type
205211
self.class.native_text_database_type || (sqlserver_2005? ? 'varchar(max)' : 'text')
206212
end
207213

214+
def native_binary_database_type
215+
self.class.native_binary_database_type || (sqlserver_2005? ? 'varbinary(max)' : 'image')
216+
end
217+
208218
# QUOTING ==================================================#
209219

210220
def quote(value, column = nil)
211-
if value.kind_of?(String) && column && column.type == :binary
212-
column.class.string_to_binary(value)
221+
case value
222+
when String, ActiveSupport::Multibyte::Chars
223+
if column && column.type == :binary
224+
column.class.string_to_binary(value)
225+
elsif column && column.respond_to?(:is_utf8?) && column.is_utf8?
226+
quoted_utf8_value(value)
227+
else
228+
super
229+
end
213230
else
214231
super
215232
end
@@ -244,6 +261,10 @@ def quoted_date(value)
244261
end
245262
end
246263

264+
def quoted_utf8_value(value)
265+
"N'#{quote_string(value)}'"
266+
end
267+
247268
# REFERENTIAL INTEGRITY ====================================#
248269

249270
def disable_referential_integrity(&block)
@@ -394,20 +415,26 @@ def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
394415
# SCHEMA STATEMENTS ========================================#
395416

396417
def native_database_types
397-
binary = sqlserver_2005? ? "varbinary(max)" : "image"
398418
{
399-
:primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
400-
:string => { :name => "varchar", :limit => 255 },
401-
:text => { :name => native_text_database_type },
402-
:integer => { :name => "int", :limit => 4 },
403-
:float => { :name => "float", :limit => 8 },
404-
:decimal => { :name => "decimal" },
405-
:datetime => { :name => "datetime" },
406-
:timestamp => { :name => "datetime" },
407-
:time => { :name => "datetime" },
408-
:date => { :name => "datetime" },
409-
:binary => { :name => binary },
410-
:boolean => { :name => "bit"}
419+
:primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
420+
:string => { :name => "varchar", :limit => 255 },
421+
:text => { :name => native_text_database_type },
422+
:integer => { :name => "int", :limit => 4 },
423+
:float => { :name => "float", :limit => 8 },
424+
:decimal => { :name => "decimal" },
425+
:datetime => { :name => "datetime" },
426+
:timestamp => { :name => "datetime" },
427+
:time => { :name => "datetime" },
428+
:date => { :name => "datetime" },
429+
:binary => { :name => native_binary_database_type },
430+
:boolean => { :name => "bit"},
431+
# These are custom types that may move somewhere else for good schema_dumper.rb hacking to output them.
432+
:char => { :name => 'char' },
433+
:varchar_max => { :name => 'varchar(max)' },
434+
:nchar => { :name => "nchar" },
435+
:nvarchar => { :name => "nvarchar", :limit => 255 },
436+
:nvarchar_max => { :name => "nvarchar(max)" },
437+
:ntext => { :name => "ntext" }
411438
}
412439
end
413440

@@ -505,7 +532,8 @@ def remove_index(table_name, options = {})
505532

506533
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
507534
limit = nil unless self.class.type_limitable?(type)
508-
if type.to_s == 'integer'
535+
case type.to_s
536+
when 'integer'
509537
case limit
510538
when 1..2 then 'smallint'
511539
when 3..4, nil then 'integer'
@@ -606,12 +634,10 @@ def select(sql, name = nil, ignore_special_columns = false)
606634
end
607635

608636
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
609-
set_utf8_values!(sql)
610637
super || select_value("SELECT SCOPE_IDENTITY() AS Ident")
611638
end
612639

613640
def update_sql(sql, name = nil)
614-
set_utf8_values!(sql)
615641
execute(sql, name)
616642
select_value('SELECT @@ROWCOUNT AS AffectedRows')
617643
end
@@ -794,11 +820,9 @@ def remove_sqlserver_columns_cache_for(table_name)
794820
def column_definitions(table_name)
795821
db_name = unqualify_db_name(table_name)
796822
table_name = unqualify_table_name(table_name)
797-
# COL_LENGTH returns values that do not reflect how much data can be stored in certain data types.
798-
# COL_LENGTH returns -1 for varchar(max), nvarchar(max), and varbinary(max)
799-
# COL_LENGTH returns 16 for ntext, text, image types
800823
sql = %{
801824
SELECT
825+
columns.TABLE_NAME as table_name,
802826
columns.COLUMN_NAME as name,
803827
columns.DATA_TYPE as type,
804828
CASE
@@ -808,10 +832,7 @@ def column_definitions(table_name)
808832
columns.NUMERIC_SCALE as numeric_scale,
809833
columns.NUMERIC_PRECISION as numeric_precision,
810834
CASE
811-
WHEN columns.DATA_TYPE IN ('nvarchar') AND COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME) = -1 THEN 1073741823
812-
WHEN columns.DATA_TYPE IN ('varchar', 'varbinary') AND COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME) = -1 THEN 2147483647
813-
WHEN columns.DATA_TYPE IN ('ntext') AND COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME) = 16 THEN 1073741823
814-
WHEN columns.DATA_TYPE IN ('text', 'image') AND COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME) = 16 THEN 2147483647
835+
WHEN columns.DATA_TYPE IN ('nchar','nvarchar') THEN columns.CHARACTER_MAXIMUM_LENGTH
815836
ELSE COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME)
816837
END as length,
817838
CASE
@@ -829,12 +850,16 @@ def column_definitions(table_name)
829850
results = without_type_conversion { select(sql,nil,true) }
830851
results.collect do |ci|
831852
ci.symbolize_keys!
832-
ci[:type] = if ci[:type] =~ /numeric|decimal/i
853+
ci[:type] = case ci[:type]
854+
when /^bit|image|text|ntext|datetime$/
855+
ci[:type]
856+
when /^numeric|decimal$/i
833857
"#{ci[:type]}(#{ci[:numeric_precision]},#{ci[:numeric_scale]})"
858+
when /^char|nchar|varchar|nvarchar|varbinary|bigint|int|smallint$/
859+
ci[:length].to_i == -1 ? "#{ci[:type]}(max)" : "#{ci[:type]}(#{ci[:length]})"
834860
else
835-
"#{ci[:type]}(#{ci[:length]})"
861+
ci[:type]
836862
end
837-
ci[:table_name] = table_name
838863
ci[:default_value] = ci[:default_value].match(/\A\(+N?'?(.*?)'?\)+\Z/)[1] if ci[:default_value]
839864
ci[:null] = ci[:is_nullable].to_i == 1 ; ci.delete(:is_nullable)
840865
ci
@@ -848,8 +873,6 @@ def column_for(table_name, column_name)
848873
column
849874
end
850875

851-
852-
853876
def change_order_direction(order)
854877
order.split(",").collect {|fragment|
855878
case fragment
@@ -859,11 +882,11 @@ def change_order_direction(order)
859882
end
860883
}.join(",")
861884
end
862-
885+
863886
def special_columns(table_name)
864887
columns(table_name).select(&:is_special?).map(&:name)
865888
end
866-
889+
867890
def repair_special_columns(sql)
868891
special_cols = special_columns(get_table_name(sql))
869892
for col in special_cols.to_a
@@ -872,35 +895,7 @@ def repair_special_columns(sql)
872895
end
873896
sql
874897
end
875-
876-
def utf8_columns(table_name)
877-
columns(table_name).select(&:is_utf8?).map(&:name)
878-
end
879-
880-
def set_utf8_values!(sql)
881-
utf8_cols = utf8_columns(get_table_name(sql))
882-
if sql =~ /^\s*UPDATE/i
883-
utf8_cols.each do |col|
884-
sql.gsub!("[#{col.to_s}] = '", "[#{col.to_s}] = N'")
885-
end
886-
elsif sql =~ /^\s*INSERT(?!.*DEFAULT VALUES\s*$)/i
887-
# TODO This code should be simplified
888-
# Get columns and values, split them into arrays, and store the original_values for when we need to replace them
889-
columns_and_values = sql.scan(/\((.*?)\)/m).flatten
890-
columns = columns_and_values.first.split(',')
891-
values = columns_and_values[1].split(',')
892-
original_values = values.dup
893-
# Iterate columns that should be UTF8, and append an N to the value, if the value is not NULL
894-
utf8_cols.each do |col|
895-
columns.each_with_index do |column, idx|
896-
values[idx] = " N#{values[idx].gsub(/^ /, '')}" if column =~ /\[#{col}\]/ and values[idx] !~ /^NULL$/
897-
end
898-
end
899-
# Replace (in place) the SQL
900-
sql.gsub!(original_values.join(','), values.join(','))
901-
end
902-
end
903-
898+
904899
end #class SQLServerAdapter < AbstractAdapter
905900

906901
end #module ConnectionAdapters

test/cases/adapter_test_sqlserver.rb

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -215,33 +215,6 @@ def setup
215215

216216
end
217217

218-
context 'which have coerced types' do
219-
220-
setup do
221-
christmas_08 = "2008-12-25".to_time
222-
christmas_08_afternoon = "2008-12-25 12:00".to_time
223-
@chronic_date = SqlServerChronic.create!(:date => christmas_08).reload
224-
@chronic_time = SqlServerChronic.create!(:time => christmas_08_afternoon).reload
225-
end
226-
227-
should 'have an inheritable attribute ' do
228-
assert SqlServerChronic.coerced_sqlserver_date_columns.include?('date')
229-
end
230-
231-
should 'have column and objects cast to date' do
232-
date_column = SqlServerChronic.columns_hash['date']
233-
assert_equal :date, date_column.type, "This column: \n#{date_column.inspect}"
234-
assert_instance_of Date, @chronic_date.date
235-
end
236-
237-
should 'have column objects cast to time' do
238-
time_column = SqlServerChronic.columns_hash['time']
239-
assert_equal :time, time_column.type, "This column: \n#{time_column.inspect}"
240-
assert_instance_of Time, @chronic_time.time
241-
end
242-
243-
end
244-
245218
end
246219

247220
context 'For identity inserts' do

0 commit comments

Comments
 (0)