add mysql bit_as_unsigned on_connect_call option, and tests for bit type
Rafael Kitover [Fri, 23 Jul 2010 22:27:37 +0000 (18:27 -0400)]
Changes
lib/DBIx/Class/Storage/DBI/mysql.pm
t/71mysql.t
t/lib/DBICTest/Schema/BitField.pm [new file with mode: 0644]

diff --git a/Changes b/Changes
index e5d5b23..c4251c7 100644 (file)
--- a/Changes
+++ b/Changes
@@ -16,6 +16,7 @@ Revision history for DBIx::Class
           failures
         - Fixed distinct with order_by to not double-specify the same
           column in the GROUP BY clause
+        - add bit_as_unsigned on_connect_call option for MySQL (RT#59659)
 
     * Misc
         - Refactored capability handling in Storage::DBI, allows for
index 7d80187..7663735 100644 (file)
@@ -8,9 +8,13 @@ use base qw/
   DBIx::Class::Storage::DBI
 /;
 use mro 'c3';
+use DBI ':sql_types';
+use namespace::clean;
 
 __PACKAGE__->sql_maker_class('DBIx::Class::SQLAHacks::MySQL');
 
+__PACKAGE__->mk_group_accessors(simple => qw/_bit_as/);
+
 sub with_deferred_fk_checks {
   my ($self, $sub) = @_;
 
@@ -101,6 +105,73 @@ sub _subq_update_delete {
   return shift->_per_row_update_delete (@_);
 }
 
+# handle bit fields properly
+
+=head2 connect_call_bit_as_unsigned
+
+Used as:
+
+  on_connect_call => 'bit_as_unsigned'
+
+in your L<connect_info|DBIx::Class::Storage::DBI/connect_info>.
+
+Beginning in MySQL 5.0.3 C<BIT> columns are stored as binary data, where before
+they were an alias for C<TINYINT>.
+
+See L<http://dev.mysql.com/doc/refman/5.0/en/numeric-types.html> for details.
+
+This option allows you to use C<BIT> columns as C<UNSIGNED> integers in all
+versions of MySQL.
+
+B<NOTE:> do not insert negative values when using this option, they will not be
+inserted correctly.
+
+=cut
+
+sub connect_call_bit_as_unsigned {
+  my $self = shift;
+
+  $self->_bit_as('UNSIGNED');
+}
+
+sub bind_attribute_by_data_type {
+  my $self        = shift;
+  my ($data_type) = @_;
+
+  my $res = $self->next::method(@_) || {};
+
+  if ($data_type && $self->_bit_as && lc($data_type) eq 'bit') {
+    $res->{TYPE} = SQL_INTEGER;
+  }
+
+  return $res;
+}
+
+sub _select_args {
+  my $self             = shift;
+  my ($ident, $select) = @_;
+
+  return $self->next::method(@_) unless $self->_bit_as;
+
+  my $col_info = $self->_resolve_column_info($ident);
+
+  for my $select_idx (0..$#$select) {
+    my $selected = $select->[$select_idx];
+
+    next if ref $selected;
+
+    my $data_type = $col_info->{$selected}{data_type};
+
+    if ($data_type && lc($data_type) eq 'bit') {
+      $selected = $self->sql_maker->_quote($selected);
+
+      $select->[$select_idx] = \("CAST($selected AS " . $self->_bit_as . ")");
+    }
+  }
+
+  return $self->next::method(@_);
+}
+
 1;
 
 =head1 NAME
index 8d5a323..29aca08 100644 (file)
@@ -7,6 +7,9 @@ use lib qw(t/lib);
 use DBICTest;
 use DBI::Const::GetInfoType;
 use DBIC::SqlMakerTest;
+use Try::Tiny;
+
+DBICTest::Schema->load_classes('BitField');
 
 my ($dsn, $user, $pass) = @ENV{map { "DBICTEST_MYSQL_${_}" } qw/DSN USER PASS/};
 
@@ -43,6 +46,18 @@ $dbh->do("DROP TABLE IF EXISTS books;");
 
 $dbh->do("CREATE TABLE books (id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, source VARCHAR(100) NOT NULL, owner integer NOT NULL, title varchar(100) NOT NULL,  price integer);");
 
+$dbh->do("DROP TABLE IF EXISTS bitfield_test;");
+
+my $have_binary_bit =
+  $schema->storage->_server_info->{normalized_dbms_version} > 5.000002;
+
+if ($have_binary_bit) {
+  $dbh->do("CREATE TABLE bitfield_test (id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, bitfield_1 BIT(1), bitfield_32 bit(32), bitfield_64 bit(64));");
+}
+else {
+  $dbh->do("CREATE TABLE bitfield_test (id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY, bitfield_1 BIT(1));");
+}
+
 #'dbi:mysql:host=localhost;database=dbic_test', 'dbic_test', '');
 
 # make sure sqlt_type overrides work (::Storage::DBI::mysql does this) 
@@ -325,6 +340,73 @@ ZEROINSEARCH: {
   );
 }
 
+# test bit fields as binary
+SKIP: {
+  skip 'no binary BIT(X) support in your MySQL', 4 unless $have_binary_bit;
+
+  my $rs = $schema->resultset('BitField');
+
+  my $b1_bit   = "\01";
+  my $b32_bits = "\0xFFFFFFFF";
+  my $b64_bits = "\0xFFFFFFFFFFFFFFFF";
+
+  ok((my $row1 = $rs->create({
+    bitfield_1 => $b1_bit, bitfield_32 => $b32_bits, bitfield_64 => $b64_bits
+  })), 'created a row with bit columns');
+
+  is $row1->bitfield_1,  $b1_bit,   'bit(1)  field set correctly';
+  is $row1->bitfield_32, $b32_bits, 'bit(32) field set correctly';
+  is $row1->bitfield_64, $b64_bits, 'bit(64) field set correctly';
+
+  $row1->delete;
+}
+
+# test bit fields with bit_as_unsigned
+{
+  my $bit_schema = DBICTest::Schema->connect ($dsn, $user, $pass, {
+    on_connect_call => 'bit_as_unsigned'
+  });
+
+  my $rs = $bit_schema->resultset('BitField');
+
+  require Math::BigInt;
+
+  my $i32_bits = Math::BigInt->new('0xFFFFFFFF');
+  my $i64_bits = Math::BigInt->new('0xFFFFFFFFFFFFFFFF');
+
+  ok((my $row1 = $rs->create({
+    bitfield_1 => 1,
+    ($have_binary_bit ?
+      (bitfield_32 => $i32_bits, bitfield_64 => $i64_bits) :
+      ()
+    ),
+  })), 'created a row with bit columns');
+
+  $row1->discard_changes;
+
+  is $row1->bitfield_1,  1, 'bit(1) field set to 1 correctly';
+
+  SKIP: {
+    skip 'no binary BIT(X) support in your MySQL', 2 unless $have_binary_bit;
+
+    is $row1->bitfield_32, $i32_bits, 'bit(32) field set correctly';
+    is $row1->bitfield_64, $i64_bits, 'bit(64) field set correctly';
+  };
+
+  ok((my $row2 = $rs->create({ bitfield_1 => 0 })),
+    'created a row with bit columns');
+
+  $row2->discard_changes;
+
+  is $row2->bitfield_1, 0, 'bit(1) field set to 0 correctly';
+
+  is $rs->search({ bitfield_1 => 0 })->count, 1,
+  'correct count of rows with bit field set to 0';
+
+  is $rs->search({ bitfield_1 => 1 })->count, 1,
+  'correct count of rows with bit field set to 1';
+}
+
 ## If find() is the first query after connect()
 ## DBI::Storage::sql_maker() will be called before
 ## _determine_driver() and so the ::SQLHacks class for MySQL
@@ -335,3 +417,12 @@ $schema2->resultset("Artist")->find(4);
 isa_ok($schema2->storage->sql_maker, 'DBIx::Class::SQLAHacks::MySQL');
 
 done_testing;
+
+END {
+  if (my $dbh = try { $schema->storage->dbh }) {
+    foreach my $table (qw/artist cd producer cd_to_producer owners books
+                          bitfield_test/) {
+      $dbh->do("DROP TABLE $table");
+    }
+  }
+}
diff --git a/t/lib/DBICTest/Schema/BitField.pm b/t/lib/DBICTest/Schema/BitField.pm
new file mode 100644 (file)
index 0000000..38c7aba
--- /dev/null
@@ -0,0 +1,30 @@
+package # hide from PAUSE 
+    DBICTest::Schema::BitField;
+
+use base qw/DBICTest::BaseResult/;
+
+__PACKAGE__->table('bitfield_test');
+__PACKAGE__->add_columns(
+  'id' => {
+    data_type => 'integer',
+    is_auto_increment => 1,
+  },
+  'bitfield_1' => {
+    data_type => 'bit',
+    size => 1,
+    is_nullable => 1,
+  },
+  'bitfield_32' => {
+    data_type => 'bit',
+    size => 32,
+    is_nullable => 1,
+  },
+  'bitfield_64' => {
+    data_type => 'bit',
+    size => 64,
+    is_nullable => 1,
+  },
+);
+__PACKAGE__->set_primary_key('id');
+
+1;