File Coverage

blib/lib/SQLite/VirtualTable/Pivot.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             SQLite::VirtualTable::Pivot -- use SQLite's virtual tables to represent pivot tables.
4              
5             =head1 SYNOPSIS
6              
7             $ export SQLITE_CURRENT_DB=/tmp/foo.db
8             sqlite3 $SQLITE_CURRENT_DB
9             sqlite> .load perlvtab.so
10             sqlite> create table object_attributes (id integer, name varchar, value integer);
11             sqlite> insert into object_attributes values ( 1, "length", 20 );
12             sqlite> insert into object_attributes values ( 1, "color", "red" );
13             sqlite> create virtual table object_pivot using perl
14             ("SQLite::VirtualTable::Pivot", "object_attributes" );
15             sqlite> select * from object_pivot;
16             id|color|length
17             1|red|20
18              
19             =head1 DESCRIPTION
20              
21             A pivot table is a table in which the distinct row values of a column
22             in one table are used as the names of the columns in another table.
23              
24             Here's an example:
25              
26             Given this table :
27              
28             Student Subject Grade
29             ------- ------- -----
30             Joe Reading A
31             Joe Writing B
32             Joe Arithmetic C
33             Mary Reading B-
34             Mary Writing A+
35             Mary Arithmetic C+
36              
37             A pivot table created using the columns "Student" and "Subject"
38             and the value "Grade" would yield :
39              
40             Student Arithmetic Reading Writing
41             ------- ---------- ------- ----------
42             Joe C A B
43             Mary C+ B- A+
44              
45             To create a table, use the following syntax :
46              
47             create virtual table object_pivot using perl
48             ("SQLite::VirtualTable::Pivot", "base_table" );
49              
50             To specify the three columns, use :
51              
52             create virtual table object_pivot using perl
53             ("SQLite::VirtualTable::Pivot", "base_table",
54             "pivot_row", "pivot_column", "pivot_value" );
55              
56             where pivot_row, pivot_column and pivot_value are three columns
57             in the base_table. The distinct values of pivot_column will be
58             the names of the new columns in the pivot table. (The values may
59             be sanitized to create valid column names.)
60              
61             If any of the three columns are foreign keys, these may be
62             collapsed in the pivot table, as described below.
63              
64             The list of distinct columns is calculated the first
65             time a pivot table is used (or created) in a database session.
66             So, if the list changes, you may need to re-connect.
67              
68             =head1 Entity-Atribute-Value models
69              
70             The Entity-Attribute-Value model is a representation of data in
71             a table containing three columns representing an entity, an attribute,
72             and a value. For instance :
73              
74             Entity Attribute Value
75             ------ --------- -----
76             1 color red
77             1 length 20
78             2 color blue
79              
80             To reduce redundancy or to constrain the possible attributes/values,
81             some or all of the three columns may be foreign keys. Consider for
82             instance, the following :
83              
84             create table entities (
85             id integer primary key,
86             entity varchar,
87             unique (entity) );
88              
89             create table attributes (
90             id integer primary key,
91             attribute varchar,
92             unique (attribute) );
93              
94             create table value_s (
95             id integer primary key,
96             value integer, -- nb: "integer" is only the column affinity
97             unique (value) );
98              
99             create table eav (
100             entity integer references entities(id),
101             attribute integer references attributes(id),
102             value integer references value_s(id),
103             primary key (entity,attribute)
104             );
105              
106             Then the foreign keys may be "flattened" into the pivot table
107             by using this SQL :
108              
109             create virtual table
110             eav_pivot using perl ("SQLite::VirtualTable::Pivot",
111             "eav",
112             "entity->entity(id).entity",
113             "attribute->attributes(id).attribute",
114             "value->value_s(id).value"
115             );
116              
117             Then the columns in eav_pivot would be the entries in
118             attributes.attribute corresponding to the distinct
119             values in eav.attribute.
120              
121             Moreover, queries against the pivot table will do the right
122             thing, in the sense that restrictions will use the values in the
123             value_s table, not in the eav table.
124              
125             =head1 EXAMPLE
126              
127             create table students (student, subject, grade, primary key (student,subject));
128             insert into students values ("Joe", "Reading", "A");
129             insert into students values ("Joe", "Writing", "B");
130             insert into students values ("Joe", "Arithmetic", "C");
131             insert into students values ("Mary", "Reading", "B-");
132             insert into students values ("Mary", "Writing", "A+");
133             insert into students values ("Mary", "Arithmetic", "C+");
134              
135             select load_extension("perlvtab.so");
136             create virtual table roster using perl ("SQLite::VirtualTable::Pivot", "students", "student", "subject", "grade");
137             select * from roster;
138              
139             Student Reading Writing Arithmetic
140             ------- ------- ------- ----------
141             Joe A B C
142             Mary B- A+ C+
143              
144             select student from roster where writing = "A+";
145             Mary
146              
147             =head1 FUNCTIONS (called by sqlite, see SQLite::VirtualTable)
148              
149             =cut
150              
151             package SQLite::VirtualTable::Pivot;
152              
153             # from CPAN
154 1     1   23949 use DBI;
  1         18735  
  1         75  
155 1     1   1055 use DBIx::Simple;
  1         7437  
  1         33  
156 1     1   1009 use Data::Dumper;
  1         7651  
  1         99  
157 1     1   10 use Scalar::Util qw/looks_like_number/;
  1         2  
  1         92  
158 1     1   524 use SQLite::VirtualTable::Util qw/unescape/;
  0            
  0            
159              
160             # base modules
161             use base 'SQLite::VirtualTable';
162             use base 'Class::Accessor::Contextual';
163              
164             # local module
165             use SQLite::VirtualTable::Pivot::Cursor;
166             use strict;
167              
168             our $VERSION = 0.02;
169              
170             # Create r/w accessors for everything that we store in the class hash
171             __PACKAGE__->mk_accessors(qw| table |); # base_table name and distinct values
172             __PACKAGE__->mk_accessors(qw| columns |); # distinct values in base_table.$pivot_row
173             __PACKAGE__->mk_accessors(qw| vcolumns |); # valid column names based on the above
174             __PACKAGE__->mk_accessors(qw| indexes counts |); # populated by BEST_INDEX, used by FILTER
175             __PACKAGE__->mk_accessors(qw| pivot_row pivot_row_ref |); # entity (in EAV) + fk info
176             __PACKAGE__->mk_accessors(qw| pivot_column pivot_column_ref |); # attribute + fk info
177             __PACKAGE__->mk_accessors(qw| pivot_value pivot_value_ref |); # value + fk info
178             __PACKAGE__->mk_accessors(qw| pivot_row_type |); # column affinity for entity
179              
180             # We need to use an env variable until DBD::SQLite + SQLite::VirtualTable
181             # work together to pass one to CREATE()
182             our $dbfile = $ENV{SQLITE_CURRENT_DB} or die "please set SQLITE_CURRENT_DB";
183             our $db; # handle: DBIx::Simple object
184              
185             # debug setup
186             #$ENV{TRACE} = 1;
187             #$ENV{DEBUG} = 1;
188             sub debug($) { return unless $ENV{DEBUG}; print STDERR "# $_[0]\n"; }
189             sub trace($) { return unless $ENV{TRACE}; print STDERR "# $_[0]\n"; }
190              
191             # Initialize the database handle. Send force => 1 to force a reconnect
192             sub _init_db {
193             my %args = @_;
194             our $db;
195             return if defined($db) && !$args{force};
196             debug "connect to $dbfile";
197             $db = DBIx::Simple->connect( "dbi:SQLite:dbname=$dbfile", "", "" )
198             or die DBIx::Simple->error;
199             $db->dbh->do("PRAGMA temp_store = 2"); # use in-memory temp tables
200             }
201              
202             # Parse the string indicating a foreign key relationship in the base_table.
203             # Given "entity->entity_ref(id).value",
204             # return ("entity" , { table=>"entity_ref", child_key => "id", child_label => "value"} ).
205             sub _parse_refspec {
206             my $str = shift;
207             $str =~ /^(.*)->(.*)\((.*)\)\.(.*)$/
208             and return ( $1, { table => $2, child_key => $3, child_label => $4 } );
209             return $str;
210             }
211              
212             =head1 CREATE (constructor)
213              
214             Arguments :
215             module : "perl",
216             caller : "main"
217             virtual_table : the name of the table being created
218             base_table : the table being pivoted
219             @pivot_columns (optional) : entity, attribute, value
220              
221             Returns :
222             A new SQLite::VirtualTable::Pivot object.
223              
224             Description :
225             Create a new SQLite::VirtualTable::Pivot object. The base_table
226             is the table to be pivoted. If this table contains only three
227             columns, then they will be used in order as the pivot_row,
228             pivot_column, and pivot_value columns (aka entity, attribute, value).
229             Alternatively, these columns may be specified in the create
230             statement by passing them as parameters. If one of the values
231             is a foreign key and the pivot table should instead use a column
232             in the child table, that may be specified using the following
233             notation :
234              
235             base_table_column->child_table(child_key).child_column_to_use
236              
237             If a column name contains a space, then the portion after the
238             space should be the column affinity.
239              
240             Examples :
241              
242             CREATE VIRTUAL TABLE pivot_table USING perl
243             ("SQLite::VirtualTable::Pivot","base_table" );
244              
245             CREATE VIRTUAL TABLE pivot_table USING perl
246             ("SQLite::VirtualTable::Pivot","base_table",
247             "entity","attribute","value");
248              
249             CREATE VIRTUAL TABLE pivot_table USING perl
250             ("SQLite::VirtualTable::Pivot","base_table",
251             "entity integer","attribute varchar","value integer");
252              
253             CREATE VIRTUAL TABLE pivot_table USING perl
254             ("SQLite::VirtualTable::Pivot","base_table",
255             "entty",
256             "attribute->attribute_lookup(id).attr",
257             "value->value_lookup(id).value" );
258             =cut
259              
260             sub CREATE {
261             my ( $class, $module, $caller, $virtual_table, $base_table, @pivot_columns ) = @_;
262             trace "(CREATE, got @_)";
263              
264             # connect
265             _init_db();
266              
267             # Get the base_table and its metadata. Parse the sql used to create it.
268             $base_table = unescape($base_table);
269             my ($createsql) =
270             $db->select( 'sqlite_master', ['sql'], { name => $base_table } )->list
271             or die "Could not find table '$base_table' " . $db->error;
272             $createsql =~ s/^[^\(]*\(//; # remove leading
273             $createsql =~ s/\)[^\)]*$//; # and trailing "CREATE" declaration, to get columns
274             my @columns_and_contraints = split /,/, $createsql;
275              
276             # Set up the pivot_row (entity), pivot_column (attribute) and
277             # pivot_value (value) columns, including foreign key specifications.
278             my ($pivot_row, $pivot_row_type, $pivot_column, $pivot_value );
279             my ($pivot_row_ref, $pivot_column_ref, $pivot_value_ref);
280             if (@pivot_columns == 3) {
281             ($pivot_row, $pivot_column, $pivot_value ) = map unescape($_), @pivot_columns;
282             if ($pivot_row =~ / /) {
283             ($pivot_row,$pivot_row_type) = split / /, $pivot_row;
284             }
285             ($pivot_row ,$pivot_row_ref) = _parse_refspec($pivot_row);
286             ($pivot_column,$pivot_column_ref) = _parse_refspec($pivot_column);
287             ($pivot_value ,$pivot_value_ref) = _parse_refspec($pivot_value);
288             } else {
289             ($pivot_row, $pivot_column, $pivot_value ) = @columns_and_contraints;
290             ($pivot_row_type) = $pivot_row =~ /^\s*\S* (.*)$/;
291             }
292             for my $col ($pivot_row, $pivot_column, $pivot_value ) {
293             $col =~ s/^\s*//;
294             $col =~ s/ .*$//;
295             next if grep /$col/i, @columns_and_contraints;
296             warn "could not find $col in columns for $base_table\n";
297             }
298              
299             # Now compute the distinct values of pivot_row (attribute).
300             debug "pivot_column (attribute) is $pivot_column";
301             my @columns = (
302             $pivot_row,
303             $db->query( sprintf(
304             "SELECT DISTINCT(%s) FROM %s",
305             $pivot_column, $base_table))->flat
306             );
307             debug "distinct values for $pivot_column in $base_table are @columns";
308              
309             my @vcolumns = @columns; # virtual table column names
310              
311             # Maybe apply foreign key transform to make vcolumns.
312             if ($pivot_column_ref) {
313             @vcolumns = ($vcolumns[0]);
314             for my $c (@columns) {
315             my ($next) = $db->select(
316             $pivot_column_ref->{table},
317             $pivot_column_ref->{child_label},
318             { $pivot_column_ref->{child_key} => $c }
319             )->flat or next;
320             push @vcolumns, $next;
321             }
322             }
323             # Ensure that they are valid sqlite column names
324             for (@vcolumns) {
325             tr/a-zA-Z0-9_//dc;
326             $_ = "$pivot_column\_$_" unless $_=~/^[a-zA-Z]/;
327             }
328              
329             $pivot_row_type ||= "varchar"; # default entity type
330             bless {
331             name => $virtual_table, # the virtual pivot table name
332             table => $base_table, # the base table name
333             columns => \@columns, # the base table distinct(pivot_column) values
334             vcolumns => \@vcolumns, # the names of the virtual pivot table columns
335             pivot_row => $pivot_row, # the name of the "pivot row" column in the base table
336             pivot_row_type => $pivot_row_type, # the column affinity for the pivot row
337             pivot_row_ref => $pivot_row_ref, # hash (see _parse_refspec)
338             pivot_column => $pivot_column, # the name of the "pivot column" column in the base table
339             pivot_column_ref => $pivot_column_ref, # hash (see _parse_refspec)
340             pivot_value => $pivot_value, # the name of the "pivot value" column in the base table
341             pivot_value_ref => $pivot_value_ref, # hash (see _parse_refspec)
342             }, $class;
343             }
344             *CONNECT = \&CREATE;
345              
346             =over
347              
348             =item DECLARE_SQL
349              
350             Arguments: none
351             Returns: a CREATE TABLE statement that specifies the columns of
352             the virtual table.
353              
354             =cut
355              
356             sub DECLARE_SQL {
357             trace "DECLARE_SQL";
358             my $self = shift;
359             return sprintf "CREATE TABLE %s (%s)", $self->table, join ',', $self->vcolumns;
360             }
361              
362             # Map from incoming operators to sql operators
363             our %OpMap = ( 'eq' => '=', 'lt' => '<', 'gt' => '>',
364             'ge' => '>=', 'le' => '<=', 'match' => 'like',);
365              
366             # Create a new temporary table and return its name.
367             sub _new_temp_table {
368             my ($count) = $db->select('sqlite_temp_master','count(1)')->list;
369             debug "made temp table number ".($count + 1 );
370             return sprintf("temp_%d_%d",$count + 1,$$);
371             }
372              
373             # Generate and run a query using information created during BEST_INDEX
374             # calls. This is called during a FILTER call.
375             #
376             # Arguments :
377             # cursor : an SQLite::VirtualTable::Pivot::Cursor object
378             # constraints : an array ref of hashrefs whose keys are :
379             # column_name - the name of the column
380             # operator - one of the keys of %OpMap above
381             # bind : an arrayref of bind values, one per constraint.
382             #
383             sub _do_query {
384             my ($self, $cursor, $constraints, $args) = @_;
385             my @values = @$args; # bind values for constraints
386             my $ref = $self->pivot_value_ref;
387             # Set up join clauses and table in case the value is a foreign key.
388             my $join_clause = sprintf(
389             " INNER JOIN %s ON %s.%s=%s.%s ",
390             #e.g. " INNER JOIN value_s ON value_s.id=eav.value ";
391             $ref->{table}, $ref->{table}, $ref->{child_key},
392             $self->table, $ref->{child_label}
393             ) if $self->pivot_row_ref;
394             my $value_table = $ref->{table} || $self->table;
395             my $value_column = $ref->{child_label} || $self->pivot_column;
396             for my $constraint (@$constraints) {
397             my $value = shift @values;
398             my $temp_table = _new_temp_table();
399             push @{ $cursor->temp_tables }, $temp_table;
400             debug "creating temporary table $temp_table ";
401             my $key = $self->pivot_row_type =~ /int/i ? " INTEGER PRIMARY KEY" : "";
402             $db->query( sprintf("CREATE TEMPORARY TABLE %s (%s $key)",
403             $temp_table, $self->pivot_row)
404             ) or die $db->error;
405              
406             my ($query,@bind);
407             if ($constraint->{column_name} eq $self->pivot_row) {
408             $query = sprintf( "INSERT INTO %s SELECT DISTINCT(%s) FROM %s WHERE %s %s ?",
409             $temp_table, $self->pivot_row, $self->table, $self->pivot_row, $OpMap{$constraint->{operator}} );
410             @bind = ($value);
411             } else {
412             $query = sprintf( "INSERT INTO %s SELECT %s FROM %s %s WHERE %s = ? AND %s.%s %s ?",
413             $temp_table,
414             $self->pivot_row,
415             $self->table, $join_clause,
416             $self->pivot_column,
417             $value_table, $self->pivot_value, $OpMap{$constraint->{operator}});
418             @bind = ( $constraint->{column_name}, $value);
419             }
420             debug "ready to run $query with @bind";
421             $db->query($query, @bind ) or die $db->error;
422              
423             debug ("temp table $temp_table is for $constraint->{column_name} $constraint->{operator} $value");
424             #info ("temp table $temp_table has : ".join ",", $db->select($temp_table,"*")->list);
425             }
426             debug "created ".scalar @{ $cursor->temp_tables }." temp table(s)";
427              
428             # Now we have created the temp tables, join them together to make the final query.
429              
430             my $value_table_or_a = $self->pivot_value_ref ? $self->pivot_value_ref->{table} : 'a';
431             my $sql = sprintf( "SELECT a.%s, %s, %s.%s AS %s FROM %s a",
432             $self->pivot_row, # == entity
433             $self->pivot_column, # == attribute
434             $value_table_or_a,
435             ( $self->pivot_value_ref
436             ? $self->pivot_value_ref->{child_label}
437             : $self->pivot_value ),
438             $self->pivot_value,
439             $self->table);
440              
441             $sql .= sprintf(" INNER JOIN %s ON a.%s = %s.id ",
442             $value_table_or_a, $self->pivot_value, $value_table_or_a ) if $self->pivot_value_ref;
443              
444             for my $temp_table ($cursor->temp_tables) {
445             $sql .= sprintf( " INNER JOIN %s ON %s.%s=a.%s ",
446             $temp_table, $temp_table,
447             $self->pivot_row, $self->pivot_row
448             );
449             }
450             $sql .= sprintf(" ORDER BY a.%s", $self->pivot_row);
451              
452             # TODO move into cursor.pm
453             my (@current_row);
454             $cursor->reset;
455             $cursor->{sth} = $db->dbh->prepare( $sql) or die "error in $sql : $DBI::errstr";
456             $cursor->sth->execute or die $DBI::errstr;
457             $cursor->set( "last" => !( @current_row = $cursor->sth->fetchrow_array ) );
458             $cursor->set( current_row => \@current_row );
459             debug "ran query, first row is : @current_row";
460             }
461              
462             =item OPEN
463              
464             Create and return a new cursor.
465             This returns a new SQLite::VirtualTable::Pivot::Cursor object.
466              
467             This is called before BEST_INDEX or FILTER, just to create the
468             new empty object.
469              
470             =cut
471              
472             sub OPEN {
473             my $self = shift;
474             trace "(OPEN $self->{name})";
475             return SQLite::VirtualTable::Pivot::Cursor->new({virtual_table => $self})->reset;
476             }
477              
478             =item BEST_INDEX
479              
480             Given a set of constraints and an order, return the name
481             (and number) of the best index that should be used to
482             run this query, and the cost of using this index.
483              
484             See SQLite::VirtualTable for a more complete description of
485             the incoming and outgoing parameters.
486              
487             =cut
488              
489             sub BEST_INDEX {
490             my ($self,$constraints,$order_bys) = @_;
491             trace "(BEST_INDEX)";
492             # $order_bys is an arrayref of hashrefs with keys "column" and "direction".
493             $self->{indexes} ||= [];
494             $self->{counts} ||= {};
495             my $index_number = @{ $self->indexes };
496             my $index_name = "index_".$index_number;
497             ( $self->counts->{__table__} ) = $db->select( $self->table, 'count(1)', )->list;
498             my $cost = $self->counts->{__table__};
499             my $i = 0;
500             my @index_constraints;
501             # We are going to build an "index" (in name only) for this set of
502             # constraints. The cost will be the total number of matching attributes
503             # in the table for each of the constraints.
504             my %seen_column;
505             for my $constraint (@$constraints) {
506             # Keys of $constraint are : operator, usable, column.
507             # We must fill in : arg_index, omit.
508             next unless $constraint->{usable};
509             $cost ||= 0;
510             my $column_name = $self->{columns}[$constraint->{column}];
511             debug "evaluating cost of using column $column_name, operator $constraint->{operator}";
512             $constraint->{arg_index} = $i++; # index of this constraint as it comes through in @args to FILTER
513             $constraint->{omit} = 1;
514             push @index_constraints, {
515             operator => $constraint->{operator},
516             column_name => $column_name
517             };
518             unless (defined($self->counts->{$column_name})) {
519             # TODO cache these (when creating the table?)
520             ( $self->counts->{$column_name} ) =
521             $db->select( $self->table, 'count(1)',
522             { $self->pivot_column => $column_name } )->list;
523             }
524             my $this_cost = $self->counts->{$column_name};
525             #debug "this cost is $this_cost";
526             $cost -= $this_cost unless $seen_column{$column_name}++;
527             }
528             push @{ $self->indexes }, { constraints => \@index_constraints, name => $index_name, cost => $cost };
529             unless (defined($cost)) {
530             ($cost) = $db->select($self->{table},'count(1)')->flat;
531             debug "cost is num of rows which is $cost";
532             }
533             my $order_by_consumed = 0;
534             if ( @$order_bys == 1 )
535             { # only consumed if we are ordering by the pivot_row in ascending order
536             if ( $self->columns->[ $order_bys->[0]{column} ] eq $self->pivot_row
537             && $order_bys->[0]{direction} == 1 ) {
538             $order_by_consumed = 1;
539             }
540             }
541             debug "returning: index $index_number ($index_name) has cost $cost (orderconsumed: $order_by_consumed)";
542             return ( $index_number, $index_name, $order_by_consumed, $cost );
543             }
544              
545             =item FILTER
546              
547             Given a cursor and an index number (created dynamically in BEST_FILTER)
548             and the @args to pass to the index, run the query on the base table,
549             joining as necessary to filter the results.
550              
551             =cut
552              
553             sub FILTER {
554             # called after OPEN, before NEXT
555             my ($self, $cursor, $idxnum, $idxstr, @args) = @_;
556             trace "(FILTER $cursor)";
557             debug "filter -- index chosen was $idxnum ($idxstr) ";
558             my $constraints = $self->indexes->[$idxnum]{constraints};
559             debug "FILTER Is calling _do_query for $cursor";
560             $cursor->reset;
561             $self->_do_query( $cursor, $constraints, \@args );
562             $self->NEXT($cursor);
563             }
564              
565             =item EOF
566              
567             Are there any more rows left?
568              
569             =cut
570              
571             sub EOF {
572             my ($self, $cursor ) = @_;
573             $cursor->done;
574             };
575              
576             sub _row_values_are_equal {
577             my $self = shift;
578             my ($val1,$val2) = @_;
579             return $val1==$val2 if $self->pivot_row_type =~ /integer/i;
580             return $val1 eq $val2;
581             }
582              
583             =item NEXT
584              
585             Advance the cursor one row.
586              
587             =cut
588              
589             sub NEXT {
590             my ($self,$cursor) = @_;
591             trace "(NEXT $cursor)";
592             $cursor->get_next_row;
593             }
594              
595             =item COLUMN
596              
597             Get a piece of data from a given column (and the current row).
598              
599             =cut
600              
601             sub COLUMN {
602             my ($self, $cursor, $n) = @_;
603             my $value = $cursor->column_value( $self->{columns}[$n] );
604             return looks_like_number($value) ? 0 + $value : $value;
605             }
606              
607             =item ROWID
608              
609             Generate a unique id for this row.
610              
611             =cut
612              
613             sub ROWID {
614             my ($self, $cursor) = @_;
615             return $cursor->row_id;
616             }
617              
618             =item CLOSE
619              
620             Close the cursor.
621              
622             =cut
623              
624             sub CLOSE {
625             my ($self,$cursor) = @_;
626             trace "(CLOSE $cursor)";
627             for ($cursor->temp_tables) {
628             $db->query("drop table $_") or warn "error dropping $_: ".$db->error;
629             }
630             }
631              
632             =item DROP
633              
634             Drop the virtual table.
635              
636             =cut
637              
638             sub DROP {
639              
640             }
641              
642             =item DISCONNECT
643              
644             Disconnect from the database.
645              
646             =cut
647              
648             sub DISCONNECT {}
649              
650             *DESTROY = \&DISCONNECT;
651              
652             =back
653              
654             =head1 TODO
655              
656             - re-use the existing database handle (requires changes
657             to SQLite::VirtualTable and DBD::SQLite)
658             - allow modification of the data in the virtual table
659             - allow value column to not have integer affinity
660             - more optimization
661              
662             =head1 SEE ALSO
663              
664             L
665              
666             L
667              
668             =cut
669              
670             1;
671              
672