Skip to content

Commit

Permalink
Support 2008 Datatypes Using TDSVER=7.3. Fixes #433
Browse files Browse the repository at this point in the history
* All datetime casting uses the `Time::DATE_FORMATS[:_sqlserver_*]` formats set after connection.
* Removed `SQLServer::Utils.with_sqlserver_db_date_formats` helper and `quoted_date` hacks.
* Removed `Quoter` value type which allowed column => type special case quoting. cc @sgrif
* Every time datatype has perfect micro/nano second handling. Includes schema dumping support.
  • Loading branch information
metaskills committed Jan 3, 2016
1 parent c33ebc2 commit 5342848
Show file tree
Hide file tree
Showing 22 changed files with 436 additions and 225 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
@@ -1,11 +1,26 @@

## v4.2.7

#### Added

* Support 2008 Datatypes Using TDSVER=7.3. Fixes #433

#### Changed

* Test now use latest v0.9.5 of TinyTDS.
* Make linked servers stronger. Fixes #427. Thanks @jippeholwerda
* Use proper module for the `sqlserver_connection` method. Fixes #431. Thanks @jippeholwerda
* All datetime casting using the `Time::DATE_FORMATS[:_sqlserver_*]` formats set after connection.

#### Removed

* The `SQLServer::Utils.with_sqlserver_db_date_formats` helper and `quoted_date` hacks.
* The `Quoter` value type which allowed column => type special case quoting.

#### Fixed

* Every time datatype has perfect micro/nano second handling.
* All supported datatypes dump defaults properly to schema.rb


## v4.2.6
Expand Down
66 changes: 40 additions & 26 deletions README.md
Expand Up @@ -25,36 +25,50 @@ Account.execute_procedure :update_totals, named: 'params'

#### Native Data Type Support

We support every data type supported by FreeTDS and then a few more. All simplified Rails types in migrations will coorespond to a matching SQL Server national data type. Here is a basic chart. Always check the `initialize_native_database_types` method for an updated list.
We support every data type supported by FreeTDS and then a few more. All simplified Rails types in migrations will coorespond to a matching SQL Server national (unicode) data type. Here is a basic chart. Always check the `initialize_native_database_types` method for an updated list.

```ruby
integer: { name: 'int', limit: 4 }
bigint: { name: 'bigint' }
boolean: { name: 'bit' }
decimal: { name: 'decimal' }
money: { name: 'money' }
smallmoney: { name: 'smallmoney' }
float: { name: 'float' }
real: { name: 'real' }
date: { name: 'date' }
datetime: { name: 'datetime' }
timestamp: { name: 'datetime' }
time: { name: 'time' }
char: { name: 'char' }
varchar: { name: 'varchar', limit: 8000 }
varchar_max: { name: 'varchar(max)' }
text_basic: { name: 'text' }
nchar: { name: 'nchar' }
string: { name: 'nvarchar', limit: 4000 }
text: { name: 'nvarchar(max)' }
ntext: { name: 'ntext' }
binary_basic: { name: 'binary' }
varbinary: { name: 'varbinary', limit: 8000 }
binary: { name: 'varbinary(max)' }
uuid: { name: 'uniqueidentifier' }
ss_timestamp: { name: 'timestamp' }
integer: { name: 'int', limit: 4 }
bigint: { name: 'bigint' }
boolean: { name: 'bit' }
decimal: { name: 'decimal' }
money: { name: 'money' }
smallmoney: { name: 'smallmoney' }
float: { name: 'float' }
real: { name: 'real' }
date: { name: 'date' }
datetime: { name: 'datetime' }
datetime2: { name: 'datetime2', precision: 7 }
datetimeoffset: { name: 'datetimeoffset', precision: 7 }
smalldatetime: { name: 'smalldatetime' }
timestamp: { name: 'datetime' }
time: { name: 'time' }
char: { name: 'char' }
varchar: { name: 'varchar', limit: 8000 }
varchar_max: { name: 'varchar(max)' }
text_basic: { name: 'text' }
nchar: { name: 'nchar' }
string: { name: 'nvarchar', limit: 4000 }
text: { name: 'nvarchar(max)' }
ntext: { name: 'ntext' }
binary_basic: { name: 'binary' }
varbinary: { name: 'varbinary', limit: 8000 }
binary: { name: 'varbinary(max)' }
uuid: { name: 'uniqueidentifier' }
ss_timestamp: { name: 'timestamp' }
```

The following types require TDS version 7.3 with TinyTDS. This requires the latest FreeTDS v0.95 or higher.

* date
* datetime2
* datetimeoffset
* time

Set `tds_version` in your database.yml or the `TDSVER` environment variable to `7.3` to ensure you are using the proper protocol version till 7.3 becomes the default.

**Zone Conversion** - The `[datetimeoffset]` type is the only ActiveRecord time based datatype that does not cast the zone to ActiveRecord's default - typically UTC. As intended, this datatype is meant to maintain the zone you pass to it and/or retreived from the database.


#### Force Schema To Lowercase

Expand Down
4 changes: 3 additions & 1 deletion appveyor.yml
Expand Up @@ -2,7 +2,7 @@ init:
- SET PATH=C:\Ruby%ruby_version%\bin;%PATH%
- SET PATH=C:\MinGW\msys\1.0\bin;%PATH%
- SET RAKEOPT=-rdevkit
- SET TINYTDS_VERSION=0.9.5.beta.4
- SET TINYTDS_VERSION=0.9.5.beta.7
clone_depth: 5
skip_tags: true
matrix:
Expand All @@ -20,11 +20,13 @@ test_script:
- timeout /t 4 /nobreak > NUL
- sqlcmd -S ".\SQL2014" -U sa -P Password12! -i %APPVEYOR_BUILD_FOLDER%\test\appveyor\dbsetup.sql
- bundle exec rake test ACTIVERECORD_UNITTEST_DATASERVER="localhost\SQL2014"
- bundle exec rake test ACTIVERECORD_UNITTEST_DATASERVER="localhost\SQL2014" ACTIVERECORD_UNITTEST_TDSVERSION="7.3"
- ps: Stop-Service 'MSSQL$SQL2014'
- ps: Start-Service 'MSSQL$SQL2012SP1'
- timeout /t 4 /nobreak > NUL
- sqlcmd -S ".\SQL2012SP1" -U sa -P Password12! -i %APPVEYOR_BUILD_FOLDER%\test\appveyor\dbsetup.sql
- bundle exec rake test ACTIVERECORD_UNITTEST_DATASERVER="localhost\SQL2012SP1"
- bundle exec rake test ACTIVERECORD_UNITTEST_DATASERVER="localhost\SQL2012SP1" ACTIVERECORD_UNITTEST_TDSVERSION="7.3"
environment:
matrix:
- ruby_version: "200-x64"
Expand Down
20 changes: 4 additions & 16 deletions lib/active_record/connection_adapters/sqlserver/quoting.rb
Expand Up @@ -40,15 +40,10 @@ def unquoted_false
end

def quoted_date(value)
SQLServer::Utils.with_sqlserver_db_date_formats do
if value.acts_like?(:time) && value.respond_to?(:usec)
precision = (BigDecimal(value.usec.to_s) / 1_000_000).round(3).to_s.split('.').last
"#{super}.#{precision}"
elsif value.acts_like?(:date)
value.to_s(:_sqlserver_dateformat)
else
super
end
if value.acts_like?(:date)
Type::Date.new.type_cast_for_database(value)
else value.acts_like?(:time)
Type::DateTime.new.type_cast_for_database(value)
end
end

Expand All @@ -59,8 +54,6 @@ def _quote(value)
case value
when Type::Binary::Data
"0x#{value.hex}"
when SQLServer::Type::Quoter
value.quote_ss_value
when String, ActiveSupport::Multibyte::Chars
if value.is_utf8?
"#{QUOTED_STRING_PREFIX}#{super}"
Expand All @@ -72,11 +65,6 @@ def _quote(value)
end
end

def quoted_value_acts_like_time_filter(value)
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value
end

end
end
end
Expand Down
Expand Up @@ -201,6 +201,9 @@ def initialize_native_database_types
real: { name: 'real' },
date: { name: 'date' },
datetime: { name: 'datetime' },
datetime2: { name: 'datetime2' },
datetimeoffset: { name: 'datetimeoffset' },
smalldatetime: { name: 'smalldatetime' },
timestamp: { name: 'datetime' },
time: { name: 'time' },
char: { name: 'char' },
Expand Down Expand Up @@ -286,6 +289,8 @@ def column_definitions(table_name)
ci[:type] = case ci[:type]
when /^bit|image|text|ntext|datetime$/
ci[:type]
when /^datetime2|datetimeoffset$/i
"#{ci[:type]}(#{ci[:datetime_precision]})"
when /^time$/i
"#{ci[:type]}(#{ci[:datetime_precision]})"
when /^numeric|decimal$/i
Expand Down
Expand Up @@ -18,6 +18,14 @@ def money(name, options = {})
column(name, :money, options)
end

def datetime2(name, options = {})
column(name, :datetime2, options)
end

def datetimeoffset(name, options = {})
column(name, :datetimeoffset, options)
end

def smallmoney(name, options = {})
column(name, :smallmoney, options)
end
Expand Down
4 changes: 3 additions & 1 deletion lib/active_record/connection_adapters/sqlserver/type.rb
@@ -1,5 +1,4 @@
require 'active_record/type'
require 'active_record/connection_adapters/sqlserver/type/quoter.rb'
# Exact Numerics
require 'active_record/connection_adapters/sqlserver/type/integer.rb'
require 'active_record/connection_adapters/sqlserver/type/big_integer.rb'
Expand All @@ -13,8 +12,11 @@
require 'active_record/connection_adapters/sqlserver/type/float.rb'
require 'active_record/connection_adapters/sqlserver/type/real.rb'
# Date and Time
require 'active_record/connection_adapters/sqlserver/type/time_value_fractional.rb'
require 'active_record/connection_adapters/sqlserver/type/date.rb'
require 'active_record/connection_adapters/sqlserver/type/datetime.rb'
require 'active_record/connection_adapters/sqlserver/type/datetime2.rb'
require 'active_record/connection_adapters/sqlserver/type/datetimeoffset.rb'
require 'active_record/connection_adapters/sqlserver/type/smalldatetime.rb'
require 'active_record/connection_adapters/sqlserver/type/time.rb'
# Character Strings
Expand Down
9 changes: 9 additions & 0 deletions lib/active_record/connection_adapters/sqlserver/type/date.rb
Expand Up @@ -4,6 +4,15 @@ module SQLServer
module Type
class Date < ActiveRecord::Type::Date

def type_cast_for_database(value)
return unless value.present?
return value if value.acts_like?(:string)
value.to_s(:_sqlserver_dateformat)
end

def type_cast_for_schema(value)
type_cast_for_database(value).inspect
end

end
end
Expand Down
30 changes: 18 additions & 12 deletions lib/active_record/connection_adapters/sqlserver/type/datetime.rb
Expand Up @@ -4,28 +4,34 @@ module SQLServer
module Type
class DateTime < ActiveRecord::Type::DateTime

include TimeValueFractional

def type_cast_for_database(value)
return super unless value.acts_like?(:time)
value = zone_conversion(value)
datetime = value.to_s(:_sqlserver_datetime)
"#{datetime}".tap do |v|
fraction = quote_fractional(value)
v << ".#{fraction}" unless fraction.to_i.zero?
end
end

def type_cast_for_schema(value)
value.acts_like?(:string) ? "'#{value}'" : super
type_cast_for_database(value).inspect
end


private

def cast_value(value)
value = value.respond_to?(:usec) ? value : super
value = value.acts_like?(:time) ? value : super
return unless value
value.change usec: cast_usec(value)
end

def cast_usec(value)
return 0 if !value.respond_to?(:usec) || value.usec.zero?
seconds = value.usec.to_f / 1_000_000.0
ss_seconds = ((seconds * (1 / second_precision)).round / (1 / second_precision)).round(3)
(ss_seconds * 1_000_000).to_i
cast_fractional(value)
end

def second_precision
0.00333
def zone_conversion(value)
method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
value.respond_to?(method) ? value.send(method) : value
end

end
Expand Down
17 changes: 17 additions & 0 deletions lib/active_record/connection_adapters/sqlserver/type/datetime2.rb
@@ -0,0 +1,17 @@
module ActiveRecord
module ConnectionAdapters
module SQLServer
module Type
class DateTime2 < DateTime

include TimeValueFractional2

def type
:datetime2
end

end
end
end
end
end
@@ -0,0 +1,31 @@
module ActiveRecord
module ConnectionAdapters
module SQLServer
module Type
class DateTimeOffset < DateTime2

def type
:datetimeoffset
end

def type_cast_for_database(value)
return super unless value.acts_like?(:time)
value.to_s :_sqlserver_datetimeoffset
end

def type_cast_for_schema(value)
type_cast_for_database(value).inspect
end


private

def zone_conversion(value)
value
end

end
end
end
end
end
32 changes: 0 additions & 32 deletions lib/active_record/connection_adapters/sqlserver/type/quoter.rb

This file was deleted.

Expand Up @@ -4,15 +4,15 @@ module SQLServer
module Type
class SmallDateTime < DateTime

def type
:smalldatetime
end

private

def cast_usec(value)
0
end
private

def cast_usec_for_database(value)
'.000'
def cast_fractional(value)
value.change usec: 0
end

end
Expand Down

0 comments on commit 5342848

Please sign in to comment.