From: Rafael Kitover Date: Wed, 14 Apr 2010 23:41:26 +0000 (+0000) Subject: datetime millisecond precision for MSSQL X-Git-Tag: v0.08122~115 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=dbsrgits%2FDBIx-Class.git;a=commitdiff_plain;h=fb95dc4d2ea295f6f2c0ba5170792017e3a8781b datetime millisecond precision for MSSQL --- diff --git a/Changes b/Changes index 470c644..79934bc 100644 --- a/Changes +++ b/Changes @@ -1,5 +1,8 @@ Revision history for DBIx::Class + - InflateColumn::DateTime support for MSSQL via DBD::Sybase + - millisecond precision support for MSSQL datetimes for + InflateColumn::DateTime - support connecting using $ENV{DBI_DSN} and $ENV{DBI_DRIVER} - current_source_alias method on ResultSet objects to determine the alias to use in programatically assembled diff --git a/lib/DBIx/Class/InflateColumn/DateTime.pm b/lib/DBIx/Class/InflateColumn/DateTime.pm index ad3da46..db899cb 100644 --- a/lib/DBIx/Class/InflateColumn/DateTime.pm +++ b/lib/DBIx/Class/InflateColumn/DateTime.pm @@ -132,7 +132,7 @@ sub register_column { $info->{_ic_dt_method} ||= "timestamp_without_timezone"; } elsif ($type eq "smalldatetime") { $type = "datetime"; - $info->{_ic_dt_method} ||= "datetime"; + $info->{_ic_dt_method} ||= "smalldatetime"; } } diff --git a/lib/DBIx/Class/Storage/DBI/MSSQL.pm b/lib/DBIx/Class/Storage/DBI/MSSQL.pm index d9c8ee0..ef09d49 100644 --- a/lib/DBIx/Class/Storage/DBI/MSSQL.pm +++ b/lib/DBIx/Class/Storage/DBI/MSSQL.pm @@ -190,13 +190,9 @@ sub _svp_rollback { $self->_get_dbh->do("ROLLBACK TRANSACTION $name"); } -sub build_datetime_parser { - my $self = shift; - my $type = "DateTime::Format::Strptime"; - eval "use ${type}"; - $self->throw_exception("Couldn't load ${type}: $@") if $@; - return $type->new( pattern => '%Y-%m-%d %H:%M:%S' ); # %F %T -} +sub datetime_parser_type { + 'DBIx::Class::Storage::DBI::MSSQL::DateTime::Format' +} sub sqlt_type { 'SQLServer' } @@ -235,6 +231,54 @@ sub _ping { return $@ ? 0 : 1; } +package # hide from PAUSE + DBIx::Class::Storage::DBI::MSSQL::DateTime::Format; + +my $datetime_format = '%Y-%m-%d %H:%M:%S.%3N'; # %F %T +my $smalldatetime_format = '%Y-%m-%d %H:%M:%S'; + +my ($datetime_parser, $smalldatetime_parser); + +sub parse_datetime { + shift; + require DateTime::Format::Strptime; + $datetime_parser ||= DateTime::Format::Strptime->new( + pattern => $datetime_format, + on_error => 'croak', + ); + return $datetime_parser->parse_datetime(shift); +} + +sub format_datetime { + shift; + require DateTime::Format::Strptime; + $datetime_parser ||= DateTime::Format::Strptime->new( + pattern => $datetime_format, + on_error => 'croak', + ); + return $datetime_parser->format_datetime(shift); +} + +sub parse_smalldatetime { + shift; + require DateTime::Format::Strptime; + $smalldatetime_parser ||= DateTime::Format::Strptime->new( + pattern => $smalldatetime_format, + on_error => 'croak', + ); + return $smalldatetime_parser->parse_datetime(shift); +} + +sub format_smalldatetime { + shift; + require DateTime::Format::Strptime; + $smalldatetime_parser ||= DateTime::Format::Strptime->new( + pattern => $smalldatetime_format, + on_error => 'croak', + ); + return $smalldatetime_parser->format_datetime(shift); +} + 1; =head1 NAME diff --git a/lib/DBIx/Class/Storage/DBI/Sybase/Microsoft_SQL_Server.pm b/lib/DBIx/Class/Storage/DBI/Sybase/Microsoft_SQL_Server.pm index 94582da..ff9223a 100644 --- a/lib/DBIx/Class/Storage/DBI/Sybase/Microsoft_SQL_Server.pm +++ b/lib/DBIx/Class/Storage/DBI/Sybase/Microsoft_SQL_Server.pm @@ -8,6 +8,7 @@ use base qw/ DBIx::Class::Storage::DBI::MSSQL /; use mro 'c3'; +use Carp::Clan qw/^DBIx::Class/; sub _rebless { my $self = shift; @@ -70,6 +71,73 @@ sub _get_server_version { } } +=head2 connect_call_datetime_setup + +Used as: + + on_connect_call => 'datetime_setup' + +In L to set: + + $dbh->syb_date_fmt('ISO_strict'); # output fmt: 2004-08-21T14:36:48.080Z + +On connection for use with L + +This works for both C and C columns, although +C columns only have minute precision. + +=cut + +{ + my $old_dbd_warned = 0; + + sub connect_call_datetime_setup { + my $self = shift; + my $dbh = $self->_get_dbh; + + if ($dbh->can('syb_date_fmt')) { + # amazingly, this works with FreeTDS + $dbh->syb_date_fmt('ISO_strict'); + } elsif (not $old_dbd_warned) { + carp "Your DBD::Sybase is too old to support ". + "DBIx::Class::InflateColumn::DateTime, please upgrade!"; + $old_dbd_warned = 1; + } + } +} + +sub datetime_parser_type { + 'DBIx::Class::Storage::DBI::Sybase::Microsoft_SQL_Server::DateTime::Format' +} + +package # hide from PAUSE + DBIx::Class::Storage::DBI::Sybase::Microsoft_SQL_Server::DateTime::Format; + +my $datetime_parse_format = '%Y-%m-%dT%H:%M:%S.%3NZ'; +my $datetime_format_format = '%Y-%m-%d %H:%M:%S.%3N'; # %F %T + +my ($datetime_parser, $datetime_formatter); + +sub parse_datetime { + shift; + require DateTime::Format::Strptime; + $datetime_parser ||= DateTime::Format::Strptime->new( + pattern => $datetime_parse_format, + on_error => 'croak', + ); + return $datetime_parser->parse_datetime(shift); +} + +sub format_datetime { + shift; + require DateTime::Format::Strptime; + $datetime_formatter ||= DateTime::Format::Strptime->new( + pattern => $datetime_format_format, + on_error => 'croak', + ); + return $datetime_formatter->format_datetime(shift); +} + 1; =head1 NAME diff --git a/t/inflate/datetime_mssql.t b/t/inflate/datetime_mssql.t index bc85fdc..c1ce5dd 100644 --- a/t/inflate/datetime_mssql.t +++ b/t/inflate/datetime_mssql.t @@ -3,59 +3,79 @@ use warnings; use Test::More; use Test::Exception; +use Scope::Guard (); use lib qw(t/lib); use DBICTest; -my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_MSSQL_ODBC_${_}" } qw/DSN USER PASS/}; +# use this if you keep a copy of DBD::Sybase linked to FreeTDS somewhere else +BEGIN { + if (my $lib_dirs = $ENV{DBICTEST_MSSQL_PERL5LIB}) { + unshift @INC, $_ for split /:/, $lib_dirs; + } +} -if (not ($dsn && $user)) { +my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_MSSQL_ODBC_${_}" } qw/DSN USER PASS/}; +my ($dsn2, $user2, $pass2) = @ENV{map { "DBICTEST_MSSQL_${_}" } qw/DSN USER PASS/}; + +if (not ($dsn || $dsn2)) { plan skip_all => - 'Set $ENV{DBICTEST_MSSQL_ODBC_DSN}, _USER and _PASS to run this test' . + 'Set $ENV{DBICTEST_MSSQL_ODBC_DSN} and/or $ENV{DBICTEST_MSSQL_DSN} _USER ' + .'and _PASS to run this test' . "\nWarning: This test drops and creates a table called 'track'"; } else { eval "use DateTime; use DateTime::Format::Strptime;"; if ($@) { plan skip_all => 'needs DateTime and DateTime::Format::Strptime for testing'; } - else { - plan tests => 4 * 2; # (tests * dt_types) - } } -my $schema = DBICTest::Schema->clone; +my @connect_info = ( + [ $dsn, $user, $pass ], + [ $dsn2, $user2, $pass2 ], +); + +my $schema; + +for my $connect_info (@connect_info) { + my ($dsn, $user, $pass) = @$connect_info; + + next unless $dsn; -$schema->connection($dsn, $user, $pass); -$schema->storage->ensure_connected; + $schema = DBICTest::Schema->connect($dsn, $user, $pass, { + on_connect_call => 'datetime_setup' + }); + + my $guard = Scope::Guard->new(\&cleanup); # coltype, column, datehash -my @dt_types = ( - ['DATETIME', - 'last_updated_at', - { - year => 2004, - month => 8, - day => 21, - hour => 14, - minute => 36, - second => 48, - nanosecond => 500000000, - }], - ['SMALLDATETIME', # minute precision - 'small_dt', - { - year => 2004, - month => 8, - day => 21, - hour => 14, - minute => 36, - }], -); + my @dt_types = ( + ['DATETIME', + 'last_updated_at', + { + year => 2004, + month => 8, + day => 21, + hour => 14, + minute => 36, + second => 48, + nanosecond => 500000000, + }], + ['SMALLDATETIME', # minute precision + 'small_dt', + { + year => 2004, + month => 8, + day => 21, + hour => 14, + minute => 36, + }], + ); -for my $dt_type (@dt_types) { - my ($type, $col, $sample_dt) = @$dt_type; + for my $dt_type (@dt_types) { + my ($type, $col, $sample_dt) = @$dt_type; - eval { $schema->storage->dbh->do("DROP TABLE track") }; - $schema->storage->dbh->do(<<"SQL"); + eval { $schema->storage->dbh->do("DROP TABLE track") }; + $schema->storage->dbh->do(<<"SQL"); CREATE TABLE track ( trackid INT IDENTITY PRIMARY KEY, cd INT, @@ -63,23 +83,30 @@ CREATE TABLE track ( $col $type, ) SQL - ok(my $dt = DateTime->new($sample_dt)); - - my $row; - ok( $row = $schema->resultset('Track')->create({ - $col => $dt, - cd => 1, - })); - ok( $row = $schema->resultset('Track') - ->search({ trackid => $row->trackid }, { select => [$col] }) - ->first - ); - is( $row->$col, $dt, 'DateTime roundtrip' ); + ok(my $dt = DateTime->new($sample_dt)); + + my $row; + ok( $row = $schema->resultset('Track')->create({ + $col => $dt, + cd => 1, + })); + ok( $row = $schema->resultset('Track') + ->search({ trackid => $row->trackid }, { select => [$col] }) + ->first + ); + is( $row->$col, $dt, "$type roundtrip" ); + + is( $row->$col->nanosecond, $sample_dt->{nanosecond}, + 'DateTime fractional portion roundtrip' ) + if exists $sample_dt->{nanosecond}; + } } +done_testing; + # clean up our mess -END { - if (my $dbh = eval { $schema->storage->_dbh }) { +sub cleanup { + if (my $dbh = eval { $schema->storage->dbh }) { $dbh->do('DROP TABLE track'); } } diff --git a/t/inflate/datetime_sybase.t b/t/inflate/datetime_sybase.t index 2b1fbed..f1ff6fc 100644 --- a/t/inflate/datetime_sybase.t +++ b/t/inflate/datetime_sybase.t @@ -70,7 +70,11 @@ SQL ->search({ trackid => $row->trackid }, { select => [$col] }) ->first ); - is( $row->$col, $dt, 'DateTime roundtrip' ); + is( $row->$col, $dt, "$type roundtrip" ); + + is( $row->$col->nanosecond, $dt->nanosecond, + 'fractional DateTime portion roundtrip' ) + if $dt->nanosecond > 0; } # test a computed datetime column