Multiple code/test/doc improvements for MSSQL over DBD::ADO
Rafael Kitover [Sun, 6 Feb 2011 20:22:34 +0000 (15:22 -0500)]
Changes
lib/DBIx/Class/Optional/Dependencies.pm
lib/DBIx/Class/Storage/DBI/ADO.pm
lib/DBIx/Class/Storage/DBI/ADO/Microsoft_SQL_Server.pm
t/747mssql_ado.t
t/inflate/datetime_mssql.t

diff --git a/Changes b/Changes
index ddeda06..3656835 100644 (file)
--- a/Changes
+++ b/Changes
@@ -3,6 +3,7 @@ Revision history for DBIx::Class
     * New Features / Changes
         - Add quote_names connection option. When set to true automatically
           sets quote_char and name_sep appropriate for your RDBMS
+        - IC::DateTime support for MSSQL over DBD::ADO
 
     * Fixes
         - Disable mysql_auto_reconnect for MySQL - depending on the ENV
index 082ce79..fbb19e3 100644 (file)
@@ -54,6 +54,9 @@ my $rdbms_mssql_odbc = {
 my $rdbms_mssql_sybase = {
   'DBD::Sybase'                   => '0',
 };
+my $rdbms_mssql_ado = {
+  'DBD::ADO'                      => '0',
+};
 my $rdbms_mysql = {
   'DBD::mysql'                    => '0',
 };
@@ -242,7 +245,17 @@ my $reqs = {
     },
     pod => {
       title => 'MSSQL support via DBD::Sybase',
-      desc => 'Modules required to connect to MSSQL support via DBD::Sybase',
+      desc => 'Modules required to connect to MSSQL via DBD::Sybase',
+    },
+  },
+
+  rdbms_mssql_ado => {
+    req => {
+      %$rdbms_mssql_ado,
+    },
+    pod => {
+      title => 'MSSQL support via DBD::ADO (Windows only)',
+      desc => 'Modules required to connect to MSSQL via DBD::ADO. This particular DBD is available on Windows only',
     },
   },
 
@@ -308,6 +321,15 @@ my $reqs = {
     },
   },
 
+  test_rdbms_mssql_ado => {
+    req => {
+      $ENV{DBICTEST_MSSQL_ADO_DSN}
+        ? (
+          %$rdbms_mssql_ado,
+        ) : ()
+    },
+  },
+
   test_rdbms_mssql_sybase => {
     req => {
       $ENV{DBICTEST_MSSQL_DSN}
index aa9fb5d..91d731c 100644 (file)
@@ -1,29 +1,53 @@
-package # hide from PAUSE
-    DBIx::Class::Storage::DBI::ADO;
+package DBIx::Class::Storage::DBI::ADO;
 
 use base 'DBIx::Class::Storage::DBI';
+use mro 'c3';
 use Try::Tiny;
 use namespace::clean;
 
+=head1 NAME
+
+DBIx::Class::Storage::DBI::ADO - Support for L<DBD::ADO>
+
+=head1 DESCRIPTION
+
+This class provides a mechanism for discovering and loading a sub-class
+for a specific ADO backend, as well as some workarounds for L<DBD::ADO>. It
+should be transparent to the user.
+
+=cut
+
 sub _rebless {
   my $self = shift;
 
-# check for MSSQL
-# XXX This should be using an OpenSchema method of some sort, but I don't know
-# how.
-# Current version is stolen from Sybase.pm
-  try {
-    my $dbtype = @{$self->_get_dbh
-      ->selectrow_arrayref(qq{sp_server_info \@attribute_id=1})
-    }[2];
-
-    $dbtype =~ s/\W/_/gi;
-    my $subclass = "DBIx::Class::Storage::DBI::ADO::${dbtype}";
-    if ($self->load_optional_class($subclass) && !$self->isa($subclass)) {
-      bless $self, $subclass;
-      $self->_rebless;
-    }
+  my $dbtype = $self->_dbh_get_info(17);
+
+  if (not $dbtype) {
+    warn 'Unable to determine ADO driver, failling back to generic support';
+    return;
+  }
+
+  $dbtype =~ s/\W/_/gi;
+  my $subclass = "DBIx::Class::Storage::DBI::ADO::${dbtype}";
+  if ($self->load_optional_class($subclass) && !$self->isa($subclass)) {
+    bless $self, $subclass;
+    $self->_rebless;
+  }
+}
+
+# cleanup some warnings from DBD::ADO
+# RT#65563, not fixed as of DBD::ADO v2.98
+sub _dbh_get_info {
+  my $self = shift;
+
+  my $warn_handler = $SIG{__WARN__} || sub { warn @_ };
+
+  local $SIG{__WARN__} = sub {
+    $warn_handler->(@_)
+      unless $_[0] =~ m{^Missing argument in sprintf at \S+/ADO/GetInfo\.pm};
   };
+
+  $self->next::method(@_);
 }
 
 # Here I was just experimenting with ADO cursor types, left in as a comment in
@@ -41,3 +65,14 @@ sub _rebless {
 #}
 
 1;
+
+=head1 AUTHOR
+
+See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
+
+=head1 LICENSE
+
+You may distribute this code under the same terms as Perl itself.
+
+=cut
+# vim:sts=2 sw=2:
index 90d7639..7c053af 100644 (file)
@@ -9,6 +9,54 @@ use base qw/
 /;
 use mro 'c3';
 
+=head1 NAME
+
+DBIx::Class::Storage::DBI::ADO::Microsoft_SQL_Server - Support for Microsoft
+SQL Server via DBD::ADO
+
+=head1 SYNOPSIS
+
+This subclass supports MSSQL server connections via L<DBD::ADO>.
+
+=head1 DESCRIPTION
+
+The MSSQL specific functionality is provided by
+L<DBIx::Class::Storage::DBI::MSSQL>.
+
+=head1 EXAMPLE DSN
+
+  dbi:ADO:provider=sqlncli10;server=EEEBOX\SQLEXPRESS
+
+=head1 CAVEATS
+
+=head2 identities
+
+C<_identity_method> is set to C<@@identity>, as C<SCOPE_IDENTITY()> doesn't work
+with L<DBD::ADO>. See L<DBIx::Class::Storage::DBI::MSSQL/IMPLEMENTATION NOTES>
+for caveats regarding this.
+
+=head2 truncation bug
+
+There is a bug with MSSQL ADO providers where data gets truncated based on the
+size of the bind sizes in the first prepare call:
+
+L<https://rt.cpan.org/Ticket/Display.html?id=52048>
+
+The C<ado_size> workaround is used (see L<DBD::ADO/"ADO Providers">) with the
+approximate maximum size of the data_type of the bound column, or 8000 (maximum
+VARCHAR size) if the data_type is not available.
+
+This code is incomplete and may be buggy. Particularly, C<VARCHAR(MAX)> is not
+supported yet. The data_type list for other DBs is also incomplete. Please
+report problems (and send patches.)
+
+=head2 fractional seconds
+
+Fractional seconds with L<DBIx::Class::InflateColumn::DateTime> are not
+currently supported, datetimes are truncated at the second.
+
+=cut
+
 sub _rebless {
   my $self = shift;
   $self->_identity_method('@@identity');
@@ -94,51 +142,45 @@ sub _mssql_max_data_type_representation_size_in_bytes {
   }
 }
 
-1;
-
-=head1 NAME
-
-DBIx::Class::Storage::DBI::ADO::Microsoft_SQL_Server - Support for Microsoft
-SQL Server via DBD::ADO
-
-=head1 SYNOPSIS
-
-This subclass supports MSSQL server connections via L<DBD::ADO>.
-
-=head1 DESCRIPTION
-
-The MSSQL specific functionality is provided by
-L<DBIx::Class::Storage::DBI::MSSQL>.
-
-=head2 CAVEATS
-
-=head3 identities
-
-C<_identity_method> is set to C<@@identity>, as C<SCOPE_IDENTITY()> doesn't work
-with L<DBD::ADO>. See L<DBIx::Class::Storage::DBI::MSSQL/IMPLEMENTATION NOTES>
-for caveats regarding this.
+sub datetime_parser_type {
+  'DBIx::Class::Storage::DBI::ADO::Microsoft_SQL_Server::DateTime::Format'
+}
 
-=head3 truncation bug
+package # hide from PAUSE
+  DBIx::Class::Storage::DBI::ADO::Microsoft_SQL_Server::DateTime::Format;
 
-There is a bug with MSSQL ADO providers where data gets truncated based on the
-size of the bind sizes in the first prepare call:
+my $datetime_format = '%m/%d/%Y %I:%M:%S %p';
+my $datetime_parser;
 
-L<https://rt.cpan.org/Ticket/Display.html?id=52048>
+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);
+}
 
-The C<ado_size> workaround is used (see L<DBD::ADO/"ADO Providers">) with the
-approximate maximum size of the data_type of the bound column, or 8000 (maximum
-VARCHAR size) if the data_type is not available.
+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);
+}
 
-This code is incomplete and may be buggy. Particularly, C<VARCHAR(MAX)> is not
-supported yet. The data_type list for other DBs is also incomplete. Please
-report problems (and send patches.)
+1;
 
 =head1 AUTHOR
 
-See L<DBIx::Class/CONTRIBUTORS>.
+See L<DBIx::Class/AUTHOR> and L<DBIx::Class/CONTRIBUTORS>.
 
 =head1 LICENSE
 
 You may distribute this code under the same terms as Perl itself.
 
 =cut
+# vim:sts=2 sw=2:
index fd847bd..3c276ef 100644 (file)
@@ -6,6 +6,9 @@ use Test::Exception;
 use lib qw(t/lib);
 use DBICTest;
 
+# Example DSN (from frew):
+# dbi:ADO:PROVIDER=sqlncli10;SERVER=tcp:172.24.2.10;MARS Connection=True;Initial Catalog=CIS;UID=cis_web;PWD=...;DataTypeCompatibility=80;
+
 my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_MSSQL_ADO_${_}" } qw/DSN USER PASS/};
 
 plan skip_all => 'Set $ENV{DBICTEST_MSSQL_ADO_DSN}, _USER and _PASS to run this test'
@@ -16,9 +19,16 @@ $schema->storage->ensure_connected;
 
 isa_ok( $schema->storage, 'DBIx::Class::Storage::DBI::ADO::Microsoft_SQL_Server' );
 
+my $ver = $schema->storage->_server_info->{normalized_dbms_version};
+
+ok $ver, 'can introspect DBMS version';
+
+is $schema->storage->sql_limit_dialect, ($ver >= 9 ? 'RowNumberOver' : 'Top'),
+  'correct limit dialect detected';
+
 $schema->storage->dbh_do (sub {
     my ($storage, $dbh) = @_;
-    eval { $dbh->do("DROP TABLE artist") };
+    eval { local $^W = 0; $dbh->do("DROP TABLE artist") };
     $dbh->do(<<'SQL');
 CREATE TABLE artist (
    artistid INT IDENTITY NOT NULL,
@@ -39,8 +49,8 @@ is $found->artistid, $new->artistid, 'search works';
 
 # test large column list in select
 $found = $schema->resultset('Artist')->search({ name => 'foo' }, {
-  select => ['artistid', 'name', map "'foo' foo_$_", 0..50],
-  as     => ['artistid', 'name', map       "foo_$_", 0..50],
+  select => ['artistid', 'name', map \"'foo' foo_$_", 0..50],
+  as     => ['artistid', 'name', map        "foo_$_", 0..50],
 })->first;
 is $found->artistid, $new->artistid, 'select with big column list';
 is $found->get_column('foo_50'), 'foo', 'last item in big column list';
@@ -71,6 +81,10 @@ done_testing;
 
 # clean up our mess
 END {
+  my $warn_handler = $SIG{__WARN__} || sub { warn @_ };
+  local $SIG{__WARN__} = sub {
+    $warn_handler->(@_) unless $_[0] =~ /Not a Win32::OLE object/
+  };
   if (my $dbh = eval { $schema->storage->_dbh }) {
     eval { $dbh->do("DROP TABLE $_") }
       for qw/artist/;
index 3c425e7..cff0fba 100644 (file)
@@ -17,14 +17,16 @@ BEGIN {
   }
 }
 
-my ($dsn, $user, $pass)    = @ENV{map { "DBICTEST_MSSQL_ODBC_${_}" } qw/DSN USER PASS/};
+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/};
+my ($dsn3, $user3, $pass3) = @ENV{map { "DBICTEST_MSSQL_ADO_${_}" }  qw/DSN USER PASS/};
 
-if (not ($dsn || $dsn2)) {
+if (not ($dsn || $dsn2 || $dsn3)) {
   plan skip_all =>
-    '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 'small_dt'";
+    'Set $ENV{DBICTEST_MSSQL_ODBC_DSN} and/or $ENV{DBICTEST_MSSQL_DSN} and/or '
+    .'$ENV{DBICTEST_MSSQL_ADO_DSN} _USER and _PASS to run this test' .
+    "\nWarning: This test drops and creates tables called 'event_small_dt' and"
+    ." 'track'.";
 }
 
 plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missing_for ('test_dt')
@@ -33,6 +35,7 @@ plan skip_all => 'Test needs ' . DBIx::Class::Optional::Dependencies->req_missin
 my @connect_info = (
   [ $dsn,  $user,  $pass ],
   [ $dsn2, $user2, $pass2 ],
+  [ $dsn3, $user3, $pass3 ],
 );
 
 my $schema;
@@ -58,7 +61,8 @@ for my $connect_info (@connect_info) {
 
   my $guard = Scope::Guard->new(\&cleanup);
 
-  try { $schema->storage->dbh->do("DROP TABLE track") };
+  # $^W because DBD::ADO is a piece of crap
+  try { local $^W = 0; $schema->storage->dbh->do("DROP TABLE track") };
   $schema->storage->dbh->do(<<"SQL");
 CREATE TABLE track (
  trackid INT IDENTITY PRIMARY KEY,
@@ -67,7 +71,7 @@ CREATE TABLE track (
  last_updated_at DATETIME,
 )
 SQL
-  try { $schema->storage->dbh->do("DROP TABLE event_small_dt") };
+  try { local $^W = 0; $schema->storage->dbh->do("DROP TABLE event_small_dt") };
   $schema->storage->dbh->do(<<"SQL");
 CREATE TABLE event_small_dt (
  id INT IDENTITY PRIMARY KEY,
@@ -108,6 +112,8 @@ SQL
   for my $dt_type (@dt_types) {
     my ($type, $col, $source, $pk, $create_extra, $sample_dt) = @$dt_type;
 
+    delete $sample_dt->{nanosecond} if $dsn =~ /:ADO:/;
+
     ok(my $dt = DateTime->new($sample_dt));
 
     my $row;