package DBIx::XHTML_Table;
use strict;
use vars qw($VERSION);
$VERSION = '1.36';
use DBI;
use Carp;
# GLOBALS
use vars qw(%ESCAPES $T $N);
($T,$N) = ("\t","\n");
%ESCAPES = (
'&' => '&',
'<' => '<',
'>' => '>',
'"' => '"',
);
#################### CONSTRUCTOR ###################################
# see POD for documentation
sub new {
my $class = shift;
my $self = {
null_value => ' ',
};
bless $self, $class;
# last arg might be GTCH (global table config hash)
$self->{'global'} = pop if ref $_[$#_] eq 'HASH';
# note: disconnected handles aren't caught :(
if (UNIVERSAL::isa($_[0],'DBI::db')) {
# use supplied db handle
$self->{'dbh'} = $_[0];
$self->{'keep_alive'} = 1;
}
elsif (ref($_[0]) eq 'ARRAY') {
# go ahead and accept a pre-built 2d array ref
$self->_do_black_magic(@_);
}
else {
# create my own db handle
eval { $self->{'dbh'} = DBI->connect(@_) };
carp $@ and return undef if $@;
}
return $self;
}
#################### OBJECT METHODS ################################
sub exec_query {
my ($self,$sql,$vars) = @_;
carp "can't call exec_query(): do database handle" unless $self->{'dbh'};
eval {
$self->{'sth'} = (UNIVERSAL::isa($sql,'DBI::st'))
? $sql
: $self->{'dbh'}->prepare($sql)
;
$self->{'sth'}->execute(@$vars);
};
carp $@ and return undef if $@;
# store the results
$self->{'fields_arry'} = [ map { lc } @{$self->{'sth'}->{'NAME'}} ];
$self->{'fields_hash'} = $self->_reset_fields_hash();
$self->{'rows'} = $self->{'sth'}->fetchall_arrayref();
carp "can't call exec_query(): no data was returned from query" unless @{$self->{'rows'}};
if (exists $self->{'pk'}) {
# remove the primary key info from the arry and hash
$self->{'pk_index'} = delete $self->{'fields_hash'}->{$self->{'pk'}};
splice(@{$self->{'fields_arry'}},$self->{'pk_index'},1) if defined $self->{'pk_index'};
}
return $self;
}
sub output {
my ($self,$config,$no_ws) = @_;
carp "can't call output(): no data" and return '' unless $self->{'rows'};
# have to deprecate old arguments ...
if ($no_ws) {
carp "scalar arguments to output() are deprecated, use hash reference";
$N = $T = '';
}
if ($config and not ref $config) {
carp "scalar arguments to output() are deprecated, use hash reference";
$self->{'no_head'} = $config;
}
elsif ($config) {
$self->{'no_head'} = $config->{'no_head'};
$self->{'no_ucfirst'} = $config->{'no_ucfirst'};
$N = $T = '' if $config->{'no_indent'};
if ($config->{'no_whitespace'}) {
carp "no_whitespace attrib deprecated, use no_indent";
$N = $T = '';
}
}
return $self->_build_table();
}
sub modify {
my ($self,$tag,$attribs,$cols) = @_;
$tag = lc $tag;
# apply attributes to specified columns
if (ref $attribs eq 'HASH') {
$cols ||= 'global';
$cols = $self->_refinate($cols);
while (my($attr,$val) = each %$attribs) {
$self->{lc $_}->{$tag}->{$attr} = $val for @$cols;
}
}
# or handle a special case (e.g.
)
else {
# cols is really attribs now, attribs is just a scalar
$self->{'global'}->{$tag} = $attribs;
# there is only one caption - no need to rotate attribs
if (ref $cols->{'style'} eq 'HASH') {
$cols->{'style'} = join('; ',map { "$_: ".$cols->{'style'}->{$_} } keys %{$cols->{'style'}}) . ';';
}
$self->{'global'}->{$tag."_attribs"} = $cols;
}
return $self;
}
sub map_cell {
my ($self,$sub,$cols) = @_;
carp "map_cell() is being ignored - no data" and return $self unless $self->{'rows'};
$cols = $self->_refinate($cols);
$self->{'map_cell'}->{$_} = $sub for @$cols;
return $self;
}
sub map_head {
my ($self,$sub,$cols) = @_;
carp "map_head() is being ignored - no data" and return $self unless $self->{'rows'};
$cols = $self->_refinate($cols);
$self->{'map_head'}->{$_} = $sub for @$cols;
return $self;
}
sub add_col_tag {
my ($self,$attribs) = @_;
$self->{'global'}->{'colgroup'} = {} unless $self->{'colgroups'};
push @{$self->{'colgroups'}}, $attribs;
return $self;
}
sub calc_totals {
my ($self,$cols,$mask) = @_;
return undef unless $self->{'rows'};
$self->{'totals_mask'} = $mask;
$cols = $self->_refinate($cols);
my @indexes = map { $self->{'fields_hash'}->{lc $_} } @$cols;
$self->{'totals'} = $self->_total_chunk($self->{'rows'},\@indexes);
return $self;
}
sub calc_subtotals {
my ($self,$cols,$mask,$nodups) = @_;
return undef unless $self->{'rows'};
$self->{'subtotals_mask'} = $mask;
$cols = $self->_refinate($cols);
my @indexes = map { $self->{'fields_hash'}->{lc $_} } @$cols;
my $beg = 0;
foreach my $end (@{$self->{'body_breaks'}}) {
my $chunk = ([@{$self->{'rows'}}[$beg..$end]]);
push @{$self->{'sub_totals'}}, $self->_total_chunk($chunk,\@indexes);
$beg = $end + 1;
}
return $self;
}
sub set_row_colors {
my ($self,$colors,$myattrib) = @_;
return $self unless ref $colors eq 'ARRAY';
return $self unless $#$colors >= 1;
my $ref = ($myattrib)
? { $myattrib => [@$colors] }
: { style => {background => [@$colors]} }
;
$self->modify(tr => $ref, 'body');
# maybe that should be global?
#$self->modify(tr => $ref);
return $self;
}
sub set_col_colors {
my ($self,$colors,$myattrib) = @_;
return $self unless ref $colors eq 'ARRAY';
return $self unless $#$colors >= 1;
my $cols = $self->_refinate();
# trick #1: truncate colors to cols
$#$colors = $#$cols if $#$colors > $#$cols;
# trick #2: keep adding colors
#unless ($#$cols % 2 and $#$colors % 2) {
my $temp = [@$colors];
push(@$colors,_rotate($temp)) until $#$colors == $#$cols;
#}
my $ref = ($myattrib)
? { $myattrib => [@$colors] }
: { style => {background => [@$colors]} }
;
$self->modify(td => $ref, $_) for @$cols;
return $self;
}
sub set_group {
my ($self,$group,$nodup,$value) = @_;
$self->{'nodup'} = $value || $self->{'null_value'} if $nodup;
$self->{'group'} = $group = $group =~ /^\d+$/
? $self->_lookup_name($group) || lc $group : lc $group;
my $index = $self->{'fields_hash'}->{$group} || 0;
# initialize the first 'repetition'
my $rep = $self->{'rows'}->[0]->[$index];
# loop through the whole rows array, storing
# the points at which a new group starts
for my $i (0..$self->get_row_count - 1) {
my $new = $self->{'rows'}->[$i]->[$index];
push @{$self->{'body_breaks'}}, $i - 1 unless ($rep eq $new);
$rep = $new;
}
push @{$self->{'body_breaks'}}, $self->get_row_count - 1;
return $self;
}
sub set_pk {
my $self = shift;
my $pk = shift || 'id';
$pk = $pk =~ /^\d+$/ ? $self->_lookup_name($pk) || $pk : $pk;
carp "can't call set_pk(): too late to set primary key" if exists $self->{'rows'};
$self->{'pk'} = lc $pk;
return $self;
}
sub set_null_value {
my ($self,$value) = @_;
$self->{'null_value'} = $value;
return $self;
}
sub get_col_count {
my ($self) = @_;
my $count = scalar @{$self->{'fields_arry'}};
return $count;
}
sub get_row_count {
my ($self) = @_;
my $count = scalar @{$self->{'rows'}};
return $count;
}
sub get_current_row {
return shift->{'current_row'};
}
sub get_current_col {
return shift->{'current_col'};
}
sub reset {
my ($self) = @_;
}
sub add_cols {
my ($self,$config) = @_;
$config = [$config] unless ref $config eq 'ARRAY';
foreach (@$config) {
next unless ref $_ eq 'HASH';
my ($name,$data,$pos) = @$_{(qw(name data before))};
my $max_pos = $self->get_col_count();
$pos = $self->_lookup_index(ucfirst $pos || '') || $max_pos unless defined $pos && $pos =~ /^\d+$/;
$pos = $max_pos if $pos > $max_pos;
$data = [$data] unless ref $data eq 'ARRAY';
splice(@{$self->{'fields_arry'}},$pos,0,$name);
$self->_reset_fields_hash();
splice(@$_,$pos,0,_rotate($data)) for (@{$self->{rows}});
}
return $self;
}
sub drop_cols {
my ($self,$cols) = @_;
$cols = $self->_refinate($cols);
foreach my $col (@$cols) {
my $index = delete $self->{'fields_hash'}->{$col};
splice(@{$self->{'fields_arry'}},$index,1);
$self->_reset_fields_hash();
splice(@$_,$index,1) for (@{$self->{'rows'}});
}
return $self;
}
###################### DEPRECATED ##################################
sub get_table {
carp "get_table() is deprecated. Use output() instead";
output(@_);
}
sub modify_tag {
carp "modify_tag() is deprecated. Use modify() instead";
modify(@_);
}
sub map_col {
carp "map_col() is deprecated. Use map_cell() instead";
map_cell(@_);
}
#################### UNDER THE HOOD ################################
# repeat: it only looks complicated
sub _build_table {
my ($self) = @_;
my $attribs = $self->{'global'}->{'table'};
my ($head,$body,$foot);
$head = $self->_build_head;
$body = $self->{'rows'} ? $self->_build_body : '';
$foot = $self->{'totals'} ? $self->_build_foot : '';
# w3c says tfoot comes before tbody ...
my $cdata = $head . $foot . $body;
return _tag_it('table', $attribs, $cdata) . $N;
}
sub _build_head {
my ($self) = @_;
my ($attribs,$cdata,$caption);
my $output = '';
# build the
tag if applicable
if ($caption = $self->{'global'}->{'caption'}) {
$attribs = $self->{'global'}->{'caption_attribs'};
$cdata = $self->_xml_encode($caption);
$output .= $N.$T . _tag_it('caption', $attribs, $cdata);
}
# build the
tags if applicable
if ($attribs = $self->{'global'}->{'colgroup'}) {
$cdata = $self->_build_head_colgroups();
$output .= $N.$T . _tag_it('colgroup', $attribs, $cdata);
}
# go ahead and stop if they don't want the head
return "$output\n" if $self->{'no_head'};
# prepare
tag info
my $tr_attribs = _merge_attribs(
$self->{'head'}->{'tr'}, $self->{'global'}->{'tr'}
);
my $tr_cdata = $self->_build_head_row();
# prepare the tag info
$attribs = $self->{'head'}->{'thead'} || $self->{'global'}->{'thead'};
$cdata = $N.$T . _tag_it('tr', $tr_attribs, $tr_cdata) . $N.$T;
# add the tag to the output
$output .= $N.$T . _tag_it('thead', $attribs, $cdata) . $N;
}
sub _build_head_colgroups {
my ($self) = @_;
my (@cols,$output);
return unless $self->{'colgroups'};
return undef unless @cols = @{$self->{'colgroups'}};
foreach (@cols) {
$output .= $N.$T.$T . _tag_it('col', $_);
}
$output .= $N.$T;
return $output;
}
sub _build_head_row {
my ($self) = @_;
my $output = $N;
my @copy = @{$self->{'fields_arry'}};
foreach my $field (@copy) {
my $attribs = _merge_attribs(
$self->{$field}->{'th'} || $self->{'head'}->{'th'},
$self->{'global'}->{'th'} || $self->{'head'}->{'th'},
);
if (my $sub = $self->{'map_head'}->{$field}) {
$field = $sub->($field);
}
else {
$field = ucfirst $field unless $self->{'no_ucfirst'};
}
$output .= $T.$T . _tag_it('th', $attribs, $field) . $N;
}
return $output . $T;
}
sub _build_body {
my ($self) = @_;
my $beg = 0;
my $output;
# if a group was not set via set_group(), then use the entire 2-d array
my @indicies = exists $self->{'body_breaks'}
? @{$self->{'body_breaks'}}
: ($self->get_row_count - 1);
# the skinny here is to grab a slice of the rows, one for each group
foreach my $end (@indicies) {
my $body_group = $self->_build_body_group([@{$self->{'rows'}}[$beg..$end]]) || '';
my $attribs = $self->{'global'}->{'tbody'} || $self->{'body'}->{'tbody'};
my $cdata = $N . $body_group . $T;
$output .= $T . _tag_it('tbody',$attribs,$cdata) . $N;
$beg = $end + 1;
}
return $output;
}
sub _build_body_group {
my ($self,$chunk) = @_;
my ($output,$cdata);
my $attribs = _merge_attribs(
$self->{'body'}->{'tr'}, $self->{'global'}->{'tr'}
);
my $pk_col = '';
# build the rows
for my $i (0..$#$chunk) {
my @row = @{$chunk->[$i]};
$pk_col = splice(@row,$self->{'pk_index'},1) if defined $self->{'pk_index'};
$cdata = $self->_build_body_row(\@row, ($i and $self->{'nodup'} or 0), $pk_col);
$output .= $T . _tag_it('tr',$attribs,$cdata) . $N;
}
# build the subtotal row if applicable
if (my $subtotals = shift @{$self->{'sub_totals'}}) {
$cdata = $self->_build_body_subtotal($subtotals);
$output .= $T . _tag_it('tr',$attribs,$cdata) . $N;
}
return $output;
}
sub _build_body_row {
my ($self,$row,$nodup,$pk) = @_;
my $group = $self->{'group'};
my $index = $self->_lookup_index($group) if $group;
my $output = $N;
$self->{'current_row'} = $pk;
for (0..$#$row) {
my $name = $self->_lookup_name($_);
my $attribs = _merge_attribs(
$self->{$name}->{'td'} || $self->{'body'}->{'td'},
$self->{'global'}->{'td'} || $self->{'body'}->{'td'},
);
# suppress warnings AND keep 0 from becoming
$row->[$_] = '' unless defined($row->[$_]);
# escape ampersands ... should i escape more?
$row->[$_] =~ s/&/&/g;
my $cdata = ($row->[$_] =~ /^.+$/)
? $row->[$_]
: $self->{'null_value'}
;
$self->{'current_col'} = $name;
$cdata = ($nodup and $index == $_)
? $self->{'nodup'}
: _map_it($self->{'map_cell'}->{$name},$cdata)
;
$output .= $T.$T . _tag_it('td', $attribs, $cdata) . $N;
}
return $output . $T;
}
sub _build_body_subtotal {
my ($self,$row) = @_;
my $output = $N;
return '' unless $row;
for (0..$#$row) {
my $name = $self->_lookup_name($_);
my $sum = ($row->[$_]);
my $attribs = _merge_attribs(
$self->{$name}->{'th'} || $self->{'body'}->{'th'},
$self->{'global'}->{'th'} || $self->{'body'}->{'th'},
);
# use sprintf if mask was supplied
if ($self->{'subtotals_mask'} and defined $sum) {
$sum = sprintf($self->{'subtotals_mask'},$sum);
}
else {
$sum = (defined $sum) ? $sum : $self->{'null_value'};
}
$output .= $T.$T . _tag_it('th', $attribs, $sum) . $N;
}
return $output . $T;
}
sub _build_foot {
my ($self) = @_;
my $tr_attribs = _merge_attribs(
# notice that foot is 1st and global 2nd - different than rest
$self->{'foot'}->{'tr'}, $self->{'global'}->{'tr'}
);
my $tr_cdata = $self->_build_foot_row();
my $attribs = $self->{'foot'}->{'tfoot'} || $self->{'global'}->{'tfoot'};
my $cdata = $N.$T . _tag_it('tr', $tr_attribs, $tr_cdata) . $N.$T;
return $T . _tag_it('tfoot',$attribs,$cdata) . $N;
}
sub _build_foot_row {
my ($self) = @_;
my $output = $N;
my $row = $self->{'totals'};
for (0..$#$row) {
my $name = $self->_lookup_name($_);
my $attribs = _merge_attribs(
$self->{$name}->{'th'} || $self->{'foot'}->{'th'},
$self->{'global'}->{'th'} || $self->{'foot'}->{'th'},
);
my $sum = ($row->[$_]);
# use sprintf if mask was supplied
if ($self->{'totals_mask'} and defined $sum) {
$sum = sprintf($self->{'totals_mask'},$sum)
}
else {
$sum = defined $sum ? $sum : $self->{'null_value'};
}
$output .= $T.$T . _tag_it('th', $attribs, $sum) . $N;
}
return $output . $T;
}
# builds a tag and it's enclosed data
sub _tag_it {
my ($name,$attribs,$cdata) = @_;
my $text = "<\L$name\E";
# build the attributes if any - skip blank vals
while(my ($k,$v) = each %{$attribs}) {
if (ref $v eq 'HASH') {
$v = join('; ', map {
my $attrib = $_;
my $value = (ref $v->{$_} eq 'ARRAY')
? _rotate($v->{$_})
: $v->{$_};
join(': ',$attrib,$value||'');
} keys %$v) . ';';
}
$v = _rotate($v) if (ref $v eq 'ARRAY');
$text .= qq| \L$k\E="$v"| unless $v =~ /^$/;
}
$text .= (defined $cdata) ? ">$cdata\L$name\E>" : '/>';
}
# used by map_cell() and map_head()
sub _map_it {
my ($sub,$datum) = @_;
return $datum unless $sub;
return $datum = $sub->($datum);
}
# used by calc_totals() and calc_subtotals()
sub _total_chunk {
my ($self,$chunk,$indexes) = @_;
my %totals;
foreach my $row (@$chunk) {
foreach (@$indexes) {
$totals{$_} += $row->[$_] if $row->[$_] =~ /^[-0-9\.]+$/;
}
}
return [ map { defined $totals{$_} ? $totals{$_} : undef } sort (0..$self->get_col_count() - 1) ];
}
# uses %ESCAPES to convert the '4 Horsemen' of XML
# big thanks to Matt Sergeant
sub _xml_encode {
my ($self,$str) = @_;
$str =~ s/([&<>"])/$ESCAPES{$1}/ge;
return $str;
}
# returns value of and moves first element to last
sub _rotate {
my $ref = shift;
my $next = shift @$ref;
push @$ref, $next;
return $next;
}
# always returns an array ref
sub _refinate {
my ($self,$ref) = @_;
$ref = [@{$self->{'fields_arry'}}] unless defined $ref;
$ref = [$ref] unless ref $ref eq 'ARRAY';
return [map {$_ =~ /^\d+$/ ? $self->_lookup_name($_) || $_ : $_} @$ref];
}
sub _merge_attribs {
my ($hash1,$hash2) = @_;
return $hash1 unless $hash2;
return $hash2 unless $hash1;
return {%$hash2,%$hash1};
}
sub _lookup_name {
my ($self,$index) = @_;
return $self->{'fields_arry'}->[$index];
}
sub _lookup_index {
my ($self,$name) = @_;
return $self->{'fields_hash'}->{$name};
}
sub _reset_fields_hash {
my $self = shift;
my $i = 0;
$self->{fields_hash} = { map { $_ => $i++ } @{$self->{fields_arry}} };
}
# assigns a non-DBI supplied data table (2D array ref)
sub _do_black_magic {
my ($self,$ref,$headers) = @_;
$self->{'fields_arry'} = $headers ? [@$headers] : [ map { lc } @{ shift @$ref } ];
$self->{'fields_hash'} = $self->_reset_fields_hash();
$self->{'rows'} = $ref;
}
# disconnect database handle if i created it
sub DESTROY {
my ($self) = @_;
unless ($self->{'keep_alive'}) {
$self->{'dbh'}->disconnect if defined $self->{'dbh'};
}
}
1;
__END__
=head1 NAME
DBIx::XHTML_Table - SQL query result set to XML-based HTML table.
=head1 SYNOPSIS
use DBIx::XHTML_Table;
# database credentials - fill in the blanks
my ($data_source,$usr,$pass) = ();
my $table = DBIx::XHTML_Table->new($data_source,$usr,$pass);
$table->exec_query("
select foo from bar
where baz='qux'
order by foo
");
print $table->output();
# stackable method calls:
print DBIx::XHTML_Table
->new($data_source,$usr,$pass)
->exec_query('select foo,baz from bar')
->output();
# and much more - read on ...
=head1 DESCRIPTION
B is a DBI extension that creates an XHTML
table from a database query result set. It was created to fill
the gap between fetching rows from a database and transforming
them into a web browser renderable table. DBIx::XHTML_Table is
intended for programmers who want the responsibility of presenting
(decorating) data, easily. This module is meant to be used in situations
where the concern for presentation and logic seperation is overkill.
Providing logic or editable data is beyond the scope of this module,
but it is capable of doing such.
=head1 HOME PAGE
The DBIx::XHTML_Table homepage is now available, but still under
construction. A partially complete FAQ and Cookbook are available
there, as well as the Tutorial, Download and Support info:
http://www.unlocalhost.com/XHTML_Table/
http://jeffa.perlmonk.org/XHTML_Table/
=head1 CONSTRUCTOR
=over 4
=item B
$obj_ref = new DBIx::XHTML_Table(@credentials[,$attribs])
Note - all optional arguments are denoted inside brackets.
The constructor will simply pass the credentials to the DBI::connect
method - read the DBI documentation as well as the docs for your
corresponding DBI driver module (DBD::Oracle, DBD::Sybase,
DBD::mysql, etc).
# MySQL example
my $table = DBIx::XHTML_Table->new(
'DBI:mysql:database:host', # datasource
'user', # user name
'password', # user password
) or die "couldn't connect to database";
The last argument, $attribs, is an optional hash reference
and should not be confused with the DBI::connect method's
similar 'attributes' hash reference.'
# valid example for last argument
my $attribs = {
table => {
border => 1,
cellspacing => 0,
rules => 'groups',
},
caption => 'Example',
td => {
style => 'text-align: right',
},
};
my $table = DBIx::XHTML_Table->new(
$data_source,$user,$pass,$attribs
) or die "couldn't connect to database";
But it is still experimental and unpleasantly limiting.
The purpose of $table_attribs is to bypass having to
call modify() multiple times. However, if you find
yourself calling modify() more than 4 or 5 times,
then DBIx::XHTML_Table might be the wrong tool. I recommend
HTML::Template or Template-Toolkit, both available at CPAN.
=item B
$obj_ref = new DBIx::XHTML_Table($DBH[,$attribs])
The first style will result in the database handle being created
and destroyed 'behind the scenes'. If you need to keep the database
connection open after the XHTML_Table object is destroyed, then
create one yourself and pass it to the constructor:
my $dbh = DBI->connect(
$data_source,$usr,$passwd,
{RaiseError => 1},
);
my $table = DBIx::XHTML_Table->new($dbh);
# do stuff
$dbh->disconnect;
You can also use any class that isa() DBI::db object, such
as Apache::DBI or DBIx::Password objects:
my $dbh = DBIx::Password->connect($user);
my $table = DBIx::XHTML_Table->new($dbh);
=item B
$obj_ref = new DBIx::XHTML_Table($rows[,$headers])
The final style allows you to bypass a database altogether if need
be. Simply pass a LoL (list of lists) such as the one passed back
from the DBI method C. The first row will be
treated as the table heading. You are responsible for supplying the
column names. Here is one way to create a table after modifying the
result set from a database query:
my $dbh = DBI->connect($dsource,$usr,$passwd);
my $sth = $dbh->prepare('select foo,baz from bar');
$sth->execute();
# order is essential here
my $headers = $sth->{'NAME'};
my $rows = $sth->fetchall_arrayref();
# do something to $rows
my $table = DBIx::XHTML_Table->new($rows,$headers);
If $headers is not supplied, then the first row from the
first argument will be shifted off and used instead.
While obtaining the data from a database is the entire
point of this module, there is nothing stopping you from
simply hard coding it:
my $rows = [
[ qw(Head1 Head2 Head3) ],
[ qw(foo bar baz) ],
[ qw(one two three) ],
[ qw(un deux trois) ]
];
my $table = DBIx::XHTML_Table->new($rows);
And that is why $headers is optional.
=back
=head1 OBJECT METHODS
=over 4
=item B
$table->exec_query($sql[,$bind_vars])
Pass the query off to the database with hopes that data will be
returned. The first argument is scalar that contains the SQL
code, the optional second argument can either be a scalar for one
bind variable or an array reference for multiple bind vars:
$table->exec_query('
select bar,baz from foo
where bar = ?
and baz = ?
',[$foo,$bar]);
exec_query() also accepts a prepared DBI::st handle:
my $sth = $dbh->prepare('
select bar,baz from foo
where bar = ?
and baz = ?
');
$table->exec_query($sth,[$foo,$bar]);
Consult the DBI documentation for more details on bind vars.
After the query successfully executes, the results will be
stored interally as a 2-D array. The XHTML table tags will
not be generated until the output() method is invoked.
=item B