mirror of
https://github.com/DBD-SQLite/DBD-SQLite
synced 2025-06-07 14:19:10 -04:00
various code refactorings, completion of the doc
This commit is contained in:
parent
d6a77c88ea
commit
9018a4683c
9 changed files with 973 additions and 294 deletions
57
dbdimp.c
57
dbdimp.c
|
@ -2950,8 +2950,8 @@ static int perl_vt_New(const char *method,
|
|||
|
||||
/* check the return value */
|
||||
if ( count != 1 ) {
|
||||
*pzErr = sqlite3_mprintf("vtab->NEW() should return one value, got %d",
|
||||
count );
|
||||
*pzErr = sqlite3_mprintf("vtab->%s() should return one value, got %d",
|
||||
method, count );
|
||||
SP -= count; /* Clear the stack */
|
||||
goto cleanup;
|
||||
}
|
||||
|
@ -2959,11 +2959,12 @@ static int perl_vt_New(const char *method,
|
|||
/* get the VirtualTable instance */
|
||||
SV *perl_instance = POPs;
|
||||
if ( !sv_isobject(perl_instance) ) {
|
||||
*pzErr = sqlite3_mprintf("vtab->NEW() should return a blessed reference");
|
||||
*pzErr = sqlite3_mprintf("vtab->%s() should return a blessed reference",
|
||||
method);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* call the ->DECLARE_VTAB() method */
|
||||
/* call the ->VTAB_TO_DECLARE() method */
|
||||
PUSHMARK(SP);
|
||||
XPUSHs(perl_instance);
|
||||
PUTBACK;
|
||||
|
@ -3047,12 +3048,12 @@ op2str(unsigned char op) {
|
|||
return "=";
|
||||
case SQLITE_INDEX_CONSTRAINT_GT:
|
||||
return ">";
|
||||
case SQLITE_INDEX_CONSTRAINT_LE:
|
||||
return "<=";
|
||||
case SQLITE_INDEX_CONSTRAINT_LT:
|
||||
return "<";
|
||||
case SQLITE_INDEX_CONSTRAINT_GE:
|
||||
return ">=";
|
||||
case SQLITE_INDEX_CONSTRAINT_LT:
|
||||
return "<";
|
||||
case SQLITE_INDEX_CONSTRAINT_LE:
|
||||
return "<=";
|
||||
case SQLITE_INDEX_CONSTRAINT_MATCH:
|
||||
return "MATCH";
|
||||
default:
|
||||
|
@ -3197,14 +3198,8 @@ static int perl_vt_Close(sqlite3_vtab_cursor *pVtabCursor){
|
|||
SAVETMPS;
|
||||
int count;
|
||||
|
||||
/* call the close() method */
|
||||
PUSHMARK(SP);
|
||||
XPUSHs(((perl_vtab_cursor *) pVtabCursor)->perl_cursor_instance);
|
||||
PUTBACK;
|
||||
count = call_method("CLOSE", G_VOID);
|
||||
SPAGAIN;
|
||||
SP -= count;
|
||||
|
||||
/* Note : no call to a CLOSE() method; if needed, the Perl class
|
||||
can implement a DESTROY() method */
|
||||
|
||||
perl_vtab_cursor *perl_pVTabCursor = (perl_vtab_cursor *) pVtabCursor;
|
||||
SvREFCNT_dec(perl_pVTabCursor->perl_cursor_instance);
|
||||
|
@ -3542,8 +3537,12 @@ sqlite_db_destroy_module_data(void *pAux)
|
|||
int
|
||||
sqlite_db_create_module(pTHX_ SV *dbh, const char *name, const char *perl_class)
|
||||
{
|
||||
dSP;
|
||||
ENTER;
|
||||
SAVETMPS;
|
||||
|
||||
D_imp_dbh(dbh);
|
||||
int rc;
|
||||
int count, rc, retval = TRUE;
|
||||
|
||||
if (!DBIc_ACTIVE(imp_dbh)) {
|
||||
sqlite_error(dbh, -2, "attempt to create module on inactive database handle");
|
||||
|
@ -3553,7 +3552,7 @@ sqlite_db_create_module(pTHX_ SV *dbh, const char *name, const char *perl_class)
|
|||
/* load the module if needed */
|
||||
char *module_ISA = sqlite3_mprintf("%s::ISA", perl_class);
|
||||
if (!get_av(module_ISA, 0)) {
|
||||
char *loading_code = sqlite3_mprintf("require %s", perl_class);
|
||||
char *loading_code = sqlite3_mprintf("use %s", perl_class);
|
||||
eval_pv(loading_code, TRUE);
|
||||
sqlite3_free(loading_code);
|
||||
}
|
||||
|
@ -3566,20 +3565,34 @@ sqlite_db_create_module(pTHX_ SV *dbh, const char *name, const char *perl_class)
|
|||
sv_rvweaken(init_data->dbh);
|
||||
init_data->perl_class = sqlite3_mprintf(perl_class);
|
||||
|
||||
|
||||
/* register within sqlite */
|
||||
rc = sqlite3_create_module_v2( imp_dbh->db,
|
||||
name,
|
||||
&perl_vt_Module,
|
||||
init_data,
|
||||
sqlite_db_destroy_module_data
|
||||
);
|
||||
|
||||
if ( rc != SQLITE_OK ) {
|
||||
sqlite_error(dbh, rc, form("sqlite_create_module failed with error %s",
|
||||
sqlite3_errmsg(imp_dbh->db)));
|
||||
return FALSE;
|
||||
retval = FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
|
||||
|
||||
/* call the CREATE_MODULE() method */
|
||||
PUSHMARK(SP);
|
||||
XPUSHs(sv_2mortal(newSVpv(perl_class, 0)));
|
||||
XPUSHs(sv_2mortal(newSVpv(name, 0)));
|
||||
PUTBACK;
|
||||
count = call_method("CREATE_MODULE", G_VOID);
|
||||
SPAGAIN;
|
||||
SP -= count;
|
||||
|
||||
PUTBACK;
|
||||
FREETMPS;
|
||||
LEAVE;
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -2413,8 +2413,9 @@ tables but are implemented internally through specific functions.
|
|||
The fulltext or R* tree features described in the previous chapters
|
||||
are examples of such virtual tables, implemented in C code.
|
||||
|
||||
C<DBD::SQLite> also supports virtual tables implemented in Perl code:
|
||||
see L<DBD::SQLite::VirtualTable>. This can have many interesting uses
|
||||
C<DBD::SQLite> also supports virtual tables implemented in I<Perl code>:
|
||||
see L<DBD::SQLite::VirtualTable> for using or implementing such
|
||||
virtual tables. These can have many interesting uses
|
||||
for joining regular DBMS data with some other kind of data within your
|
||||
Perl programs. Bundled with the present distribution are :
|
||||
|
||||
|
@ -2423,13 +2424,13 @@ Perl programs. Bundled with the present distribution are :
|
|||
=item *
|
||||
|
||||
L<DBD::SQLite::VirtualTable::FileContent> : implements a virtual
|
||||
column that exposes content from files. This is especially useful
|
||||
in conjuction with a fulltext index; see L<DBD::SQLite::Fulltext_search>.
|
||||
column that exposes file contents. This is especially useful
|
||||
in conjunction with a fulltext index; see L<DBD::SQLite::Fulltext_search>.
|
||||
|
||||
=item *
|
||||
|
||||
L<DBD::SQLite::VirtualTable::PerlData> : binds to a Perl array
|
||||
within your main program. This can be used for simple import/export
|
||||
within the Perl program. This can be used for simple import/export
|
||||
operations, for debugging purposes, for joining data from different
|
||||
sources, etc.
|
||||
|
||||
|
@ -2437,7 +2438,6 @@ sources, etc.
|
|||
|
||||
Other Perl virtual tables may also be published separately on CPAN.
|
||||
|
||||
|
||||
=head1 FOR DBD::SQLITE EXTENSION AUTHORS
|
||||
|
||||
Since 1.30_01, you can retrieve the bundled sqlite C source and/or
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# TODO : fix bug with column name / type
|
||||
|
||||
#======================================================================
|
||||
package DBD::SQLite::VirtualTable;
|
||||
#======================================================================
|
||||
use strict;
|
||||
use warnings;
|
||||
use Scalar::Util qw/weaken/;
|
||||
|
@ -12,21 +12,21 @@ our $VERSION = '0.01';
|
|||
our @ISA;
|
||||
|
||||
|
||||
sub DESTROY_MODULE {
|
||||
my $class = shift;
|
||||
}
|
||||
#----------------------------------------------------------------------
|
||||
# methods for registering/destroying the module
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
sub CREATE {
|
||||
my $class = shift;
|
||||
return $class->NEW(@_);
|
||||
}
|
||||
sub CREATE_MODULE { my ($class, $mod_name) = @_; }
|
||||
sub DESTROY_MODULE { my ($class, $mod_name) = @_; }
|
||||
|
||||
sub CONNECT {
|
||||
my $class = shift;
|
||||
return $class->NEW(@_);
|
||||
}
|
||||
#----------------------------------------------------------------------
|
||||
# methods for creating/destroying instances
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
sub NEW { # called when instanciating a virtual table
|
||||
sub CREATE { my $class = shift; return $class->NEW(@_); }
|
||||
sub CONNECT { my $class = shift; return $class->NEW(@_); }
|
||||
|
||||
sub _PREPARE_SELF {
|
||||
my ($class, $dbh_ref, $module_name, $db_name, $vtab_name, @args) = @_;
|
||||
|
||||
my @columns;
|
||||
|
@ -44,7 +44,7 @@ sub NEW { # called when instanciating a virtual table
|
|||
}
|
||||
}
|
||||
|
||||
# build $self and initialize
|
||||
# build $self
|
||||
my $self = {
|
||||
dbh_ref => $dbh_ref,
|
||||
module_name => $module_name,
|
||||
|
@ -54,31 +54,15 @@ sub NEW { # called when instanciating a virtual table
|
|||
options => \%options,
|
||||
};
|
||||
weaken $self->{dbh_ref};
|
||||
bless $self, $class;
|
||||
$self->initialize();
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub dbh {
|
||||
my $self = shift;
|
||||
return ${$self->{dbh_ref}};
|
||||
}
|
||||
sub NEW {
|
||||
my $class = shift;
|
||||
|
||||
|
||||
sub initialize {
|
||||
my $self = shift;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
sub DROP {
|
||||
my $self = shift;
|
||||
}
|
||||
|
||||
sub DISCONNECT {
|
||||
my $self = shift;
|
||||
my $self = $class->_PREPARE_SELF(@_);
|
||||
bless $self, $class;
|
||||
}
|
||||
|
||||
|
||||
|
@ -91,27 +75,27 @@ sub VTAB_TO_DECLARE {
|
|||
return $sql;
|
||||
}
|
||||
|
||||
sub DROP { my $self = shift; }
|
||||
sub DISCONNECT { my $self = shift; }
|
||||
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# methods for initiating a search
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
sub BEST_INDEX {
|
||||
my ($self, $constraints, $order_by) = @_;
|
||||
|
||||
# print STDERR Dump [BEST_INDEX => {
|
||||
# where => $constraints,
|
||||
# order => $order_by,
|
||||
# }];
|
||||
|
||||
my $ix = 0;
|
||||
|
||||
foreach my $constraint (@$constraints) {
|
||||
# TMP HACK -- should put real values instead
|
||||
foreach my $constraint (grep {$_->{usable}} @$constraints) {
|
||||
$constraint->{argvIndex} = $ix++;
|
||||
$constraint->{omit} = 0;
|
||||
}
|
||||
|
||||
# TMP HACK -- should put real values instead
|
||||
# stupid default values -- subclasses should put real values instead
|
||||
my $outputs = {
|
||||
idxNum => 1,
|
||||
idxStr => "foobar",
|
||||
idxStr => "",
|
||||
orderByConsumed => 0,
|
||||
estimatedCost => 1.0,
|
||||
estimatedRows => undef,
|
||||
|
@ -126,11 +110,13 @@ sub OPEN {
|
|||
my $class = ref $self;
|
||||
|
||||
my $cursor_class = $class . "::Cursor";
|
||||
|
||||
return $cursor_class->NEW($self, @_);
|
||||
}
|
||||
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# methods for insert/delete/update
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
sub _SQLITE_UPDATE {
|
||||
my ($self, $old_rowid, $new_rowid, @values) = @_;
|
||||
|
@ -164,22 +150,41 @@ sub UPDATE {
|
|||
die "UPDATE() should be redefined in subclass";
|
||||
}
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# remaining methods of the sqlite API
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
sub BEGIN_TRANSACTION {return 0}
|
||||
sub SYNC_TRANSACTION {return 0}
|
||||
sub COMMIT_TRANSACTION {return 0}
|
||||
sub ROLLBACK_TRANSACTION {return 0}
|
||||
|
||||
sub SAVEPOINT {return 0}
|
||||
sub RELEASE {return 0}
|
||||
sub ROLLBACK_TO {return 0}
|
||||
sub FIND_METHOD {return 0}
|
||||
sub RENAME {return 0}
|
||||
|
||||
sub DESTROY {
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# utility methods
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
sub dbh {
|
||||
my $self = shift;
|
||||
return ${$self->{dbh_ref}};
|
||||
}
|
||||
|
||||
|
||||
sub sqlite_table_info {
|
||||
my $self = shift;
|
||||
|
||||
my $sql = "PRAGMA table_info($self->{vtab_name})";
|
||||
return $self->dbh->selectall_arrayref($sql, {Slice => {}});
|
||||
}
|
||||
|
||||
#======================================================================
|
||||
package DBD::SQLite::VirtualTable::Cursor;
|
||||
#======================================================================
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
|
@ -190,42 +195,13 @@ sub NEW {
|
|||
bless $self, $class;
|
||||
}
|
||||
|
||||
sub FILTER {
|
||||
my ($self, $idxNum, $idxStr, @values) = @_;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
sub EOF {
|
||||
my ($self) = @_;
|
||||
|
||||
# stupid implementation, to be redefined in subclasses
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
sub NEXT {
|
||||
my ($self) = @_;
|
||||
}
|
||||
|
||||
|
||||
sub COLUMN {
|
||||
my ($self, $idxCol) = @_;
|
||||
}
|
||||
|
||||
sub ROWID {
|
||||
my ($self) = @_;
|
||||
|
||||
# stupid implementation, to be redefined in subclasses
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
sub CLOSE {
|
||||
my ($self) = @_;
|
||||
}
|
||||
|
||||
# methods to be redefined in subclasses (here are stupid implementations)
|
||||
sub FILTER { my ($self, $idxNum, $idxStr, @values) = @_; return }
|
||||
sub EOF { my ($self) = @_; return 1 }
|
||||
sub NEXT { my ($self) = @_; return }
|
||||
sub COLUMN { my ($self, $idxCol) = @_; return }
|
||||
sub ROWID { my ($self) = @_; return 1 }
|
||||
|
||||
|
||||
1;
|
||||
|
@ -234,23 +210,572 @@ __END__
|
|||
|
||||
=head1 NAME
|
||||
|
||||
DBD::SQLite::VirtualTable -- Abstract parent class for implementing virtual tables
|
||||
DBD::SQLite::VirtualTable -- SQLite virtual tables implemented in Perl
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
package My::Virtual::Table;
|
||||
use parent 'DBD::SQLite::VirtualTable';
|
||||
# register the virtual table module within sqlite
|
||||
$dbh->sqlite_create_module(mod_name => "DBD::SQLite::VirtualTable::Subclass");
|
||||
|
||||
# create a virtual table
|
||||
$dbh->do("CREATE VIRTUAL TABLE vtbl USING mod_name(arg1, arg2, ...)")
|
||||
|
||||
# use it as any regular table
|
||||
my $sth = $dbh->prepare("SELECT * FROM vtbl WHERE ...");
|
||||
|
||||
B<Note> : VirtualTable subclasses or instances are not called
|
||||
directly from Perl code; everything happens indirectly through SQL
|
||||
statements within SQLite.
|
||||
|
||||
sub ...
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
TODO
|
||||
This module is an abstract class for implementing SQLite virtual tables,
|
||||
written in Perl. Such tables look like regular tables, and are accessed
|
||||
through regular SQL instructions and regular L<DBI> API; but the implementation
|
||||
is done through hidden calls to a Perl class.
|
||||
This is the same idea as Perl's L<tied variables|perltie>, but
|
||||
at the SQLite level.
|
||||
|
||||
=head1 METHODS
|
||||
The current abstract class cannot be used directly, so the
|
||||
synopsis above is just to give a general idea. Concrete, usable
|
||||
classes bundled with the present distribution are :
|
||||
|
||||
TODO
|
||||
=over
|
||||
|
||||
=item *
|
||||
|
||||
L<DBD::SQLite::VirtualTable::FileContent> : implements a virtual
|
||||
column that exposes file contents. This is especially useful
|
||||
in conjunction with a fulltext index; see L<DBD::SQLite::Fulltext_search>.
|
||||
|
||||
=item *
|
||||
|
||||
L<DBD::SQLite::VirtualTable::PerlData> : binds to a Perl array
|
||||
within the Perl program. This can be used for simple import/export
|
||||
operations, for debugging purposes, for joining data from different
|
||||
sources, etc.
|
||||
|
||||
=back
|
||||
|
||||
Other Perl virtual tables may also be published separately on CPAN.
|
||||
|
||||
The following chapters document the structure of the abstract class
|
||||
and explain how to write new subclasses; this is meant for
|
||||
B<module authors>, not for end users. If you just need to use a
|
||||
virtual table module, refer to that module's documentation.
|
||||
|
||||
|
||||
=head1 ARCHITECTURE
|
||||
|
||||
=head2 Classes
|
||||
|
||||
A virtual table module for SQLite is implemented through a pair
|
||||
of classes :
|
||||
|
||||
=over
|
||||
|
||||
=item *
|
||||
|
||||
the B<table> class implements methods for creating or connecting
|
||||
a virtual table, for destroying it, for opening new searches, etc.
|
||||
|
||||
=item *
|
||||
|
||||
the B<cursor> class implements methods for performing a specific
|
||||
SQL statement
|
||||
|
||||
=back
|
||||
|
||||
|
||||
=head2 Methods
|
||||
|
||||
Most methods in both classes are not called directly from Perl
|
||||
code : instead, they are callbacks, called from the sqlite kernel.
|
||||
Following common Perl conventions, such methods have names in
|
||||
uppercase.
|
||||
|
||||
|
||||
=head1 TABLE METHODS
|
||||
|
||||
=head2 Class methods for registering the module
|
||||
|
||||
=head3 CREATE_MODULE
|
||||
|
||||
$class->CREATE_MODULE($sqlite_module_name);
|
||||
|
||||
Called when the client code invokes
|
||||
|
||||
$dbh->sqlite_create_module($sqlite_module_name => $class);
|
||||
|
||||
The default implementation is empty.
|
||||
|
||||
|
||||
=head3 DESTROY_MODULE
|
||||
|
||||
$class->DESTROY_MODULE();
|
||||
|
||||
Called automatically when the database handle is disconnected.
|
||||
The default implementation is empty.
|
||||
|
||||
|
||||
=head2 Class methods for creating a vtable instance
|
||||
|
||||
|
||||
=head3 CREATE
|
||||
|
||||
$class->CREATE($dbh_ref, $module_name, $db_name, $vtab_name, @args);
|
||||
|
||||
Called when sqlite receives a statement
|
||||
|
||||
CREATE VIRTUAL TABLE $db_name.$vtab_name USING $module_name(@args)
|
||||
|
||||
The default implementation just calls L</NEW>.
|
||||
|
||||
=head3 CONNECT
|
||||
|
||||
$class->CONNECT($dbh_ref, $module_name, $db_name, $vtab_name, @args);
|
||||
|
||||
Called when attempting to access a virtual table that had been created
|
||||
during previous database connection. The creation arguments were stored
|
||||
within the sqlite database and are passed again to the CONNECT method.
|
||||
|
||||
The default implementation just calls L</NEW>.
|
||||
|
||||
|
||||
=head3 _PREPARE_SELF
|
||||
|
||||
$class->_PREPARE_SELF($dbh_ref, $module_name, $db_name, $vtab_name, @args);
|
||||
|
||||
Prepares the datastructure for a virtual table instance.
|
||||
C<@args> is just the collection
|
||||
of strings (comma-separated) that were given within the
|
||||
C<CREATE VIRTUAL TABLE> statement; each subclass should decide
|
||||
what to do with this information,
|
||||
|
||||
The method parses C<@args> to differentiate between I<options>
|
||||
(strings of shape C<$key>=C<$value> or C<$key>=C<"$value">, stored in
|
||||
C<< $self->{options} >>), and I<columns> (other C<@args>, stored in
|
||||
C<< $self->{columns} >>). It creates a hashref with the following fields :
|
||||
|
||||
=over
|
||||
|
||||
=item C<dbh_ref>
|
||||
|
||||
a weak reference to the C<$dbh> database handle (see
|
||||
L<Scalar::Util> for an explanation of weak references).
|
||||
|
||||
=item C<module_name>
|
||||
|
||||
name of the module as declared to sqlite (not to be confounded
|
||||
with the Perl class name).
|
||||
|
||||
=item C<db_name>
|
||||
|
||||
name of the database (usuallly C<'main'> or C<'temp'>), but it
|
||||
may also be an attached database
|
||||
|
||||
=item C<vtab_name>
|
||||
|
||||
name of the virtual table
|
||||
|
||||
=item C<columns>
|
||||
|
||||
arrayref of column declarations
|
||||
|
||||
=item C<options>
|
||||
|
||||
hashref of option declarations
|
||||
|
||||
=back
|
||||
|
||||
This method should not be redefined, since it performs
|
||||
general work which is supposed to be useful for all subclasses.
|
||||
Instead, subclasses may override the L</NEW> method.
|
||||
|
||||
|
||||
=head3 NEW
|
||||
|
||||
$class->NEW($dbh_ref, $module_name, $db_name, $vtab_name, @args);
|
||||
|
||||
Instantiates a virtual table.
|
||||
|
||||
|
||||
=head2 Instance methods called from the sqlite kernel
|
||||
|
||||
|
||||
=head3 DROP
|
||||
|
||||
Called whenever a virtual table is destroyed from the
|
||||
database through the C<DROP TABLE> SQL instruction.
|
||||
|
||||
Just after the C<DROP()> call, the Perl instance
|
||||
will be destroyed (and will therefore automatically
|
||||
call the C<DESTROY()> method if such a method is present).
|
||||
|
||||
The default implementation for DROP is empty.
|
||||
|
||||
B<Note> : this corresponds to the C<xDestroy> method
|
||||
in the SQLite documentation; here it was not named
|
||||
C<DESTROY>, to avoid any confusion with the standard
|
||||
Perl method C<DESTROY> for object destruction.
|
||||
|
||||
|
||||
=head3 DISCONNECT
|
||||
|
||||
Called for every virtual table just before the database handle
|
||||
is disconnected.
|
||||
|
||||
Just after the C<DISCONNECT()> call, the Perl instance
|
||||
will be destroyed (and will therefore automatically
|
||||
call the C<DESTROY()> method if such a method is present).
|
||||
|
||||
The default implementation for DISCONNECT is empty.
|
||||
|
||||
=head3 VTAB_TO_DECLARE
|
||||
|
||||
This method is called automatically just after L</CREATE> or L</CONNECT>,
|
||||
to register the columns of the virtual table within the sqlite kernel.
|
||||
The method should return a string containing a SQL C<CREATE TABLE> statement;
|
||||
but only the column declaration parts will be considered (see
|
||||
L<http://sqlite.org/c3ref/declare_vtab.html>).
|
||||
|
||||
The default implementation returns:
|
||||
|
||||
CREATE TABLE $self->{vtab_name}(@{$self->{columns}})
|
||||
|
||||
=head3 BEST_INDEX
|
||||
|
||||
my $index_info = $vtab->BEST_INDEX($constraints, $order_by)
|
||||
|
||||
This is the most complex method to redefined in subclasses.
|
||||
This method will be called at the beginning of a new query on the
|
||||
virtual table; the job of the method is to assemble some information
|
||||
that will be used
|
||||
|
||||
=over
|
||||
|
||||
=item a)
|
||||
|
||||
by the sqlite kernel to decide about the best search strategy
|
||||
|
||||
=item b)
|
||||
|
||||
by the cursor L</FILTER> method to produce the desired subset
|
||||
of rows from the virtual table.
|
||||
|
||||
=back
|
||||
|
||||
By calling this method, the SQLite core is saying to the virtual table
|
||||
that it needs to access some subset of the rows in the virtual table
|
||||
and it wants to know the most efficient way to do that access. The
|
||||
C<BEST_INDEX> method replies with information that the SQLite core can
|
||||
then use to conduct an efficient search of the virtual table.
|
||||
|
||||
The method takes as input a list of C<$constraints> and a list
|
||||
of C<$order_by> instructions. It returns a hashref of indexing
|
||||
properties, described below; furthermore, the method also adds
|
||||
supplementary information within the input C<$constraints>.
|
||||
Detailed explanations are given in
|
||||
L<http://sqlite.org/vtab.html#xbestindex>.
|
||||
|
||||
=head4 Input constraints
|
||||
|
||||
Elements of the C<$constraints> arrayref correspond to
|
||||
specific clauses of the C<WHERE ...> part of the SQL query.
|
||||
Each constraint is a hashref with keys :
|
||||
|
||||
=over
|
||||
|
||||
=item C<col>
|
||||
|
||||
the integer index of the column on the left-hand side of the constraint
|
||||
|
||||
=item C<op>
|
||||
|
||||
the comparison operator, expressed as string containing
|
||||
C<< '=' >>, C<< '>' >>, C<< '>=' >>, C<< '<' >>, C<< '<=' >> or C<< 'MATCH' >>.
|
||||
|
||||
=item C<usable>
|
||||
|
||||
a boolean indicating if that constraint is usable; some constraints
|
||||
might not be usable because of the way tables are ordered in a join.
|
||||
|
||||
=back
|
||||
|
||||
The C<$constraints> arrayref is used both for input and for output.
|
||||
While iterating over the array, the method should
|
||||
add the following keys into usable constraints :
|
||||
|
||||
=over
|
||||
|
||||
=item C<argvIndex>
|
||||
|
||||
An index into the C<@values> array that will be passed to
|
||||
the cursor's L</FILTER> method. In other words, if the current
|
||||
constraint corresponds to the SQL fragment C<WHERE ... AND foo < 123 ...>,
|
||||
and the corresponding C<argvIndex> takes value 5, this means that
|
||||
the C<FILTER> method will receive C<123> in C<$values[5]>.
|
||||
|
||||
=item C<omit>
|
||||
|
||||
A boolean telling to the sqlite core that it can safely omit
|
||||
to double check that constraint before returning the resultset
|
||||
to the calling program; this means that the FILTER method has fulfilled
|
||||
the filtering job on that constraint and there is no need to do any
|
||||
further checking.
|
||||
|
||||
=back
|
||||
|
||||
The C<BEST_INDEX> method will not necessarily receive all constraints
|
||||
from the SQL C<WHERE> clause : for example a constraint like
|
||||
C<< col1 < col2 + col3 >> cannot be handled at this level.
|
||||
Furthemore, the C<BEST_INDEX> might decide to ignore some of the
|
||||
received constraints. This is why a second pass over the results
|
||||
will be performed by the sqlite core.
|
||||
|
||||
|
||||
=head4 "order_by" input information
|
||||
|
||||
The C<$order_by> arrayref corresponds to the C<ORDER BY> clauses
|
||||
in the SQL query. Each entry is a hashref with keys :
|
||||
|
||||
=over
|
||||
|
||||
=item C<col>
|
||||
|
||||
the integer index of the column being ordered
|
||||
|
||||
=item C<desc>
|
||||
|
||||
a boolean telling of the ordering is DESCending or ascending
|
||||
|
||||
=back
|
||||
|
||||
This information could be used by some subclasses for
|
||||
optimizing the query strategfy; but usually the sqlite core will
|
||||
perform another sorting pass once all results are gathered.
|
||||
|
||||
=head4 Hashref information returned by BEST_INDEX
|
||||
|
||||
The method should return a hashref with the following keys :
|
||||
|
||||
=over
|
||||
|
||||
=item C<idxNum>
|
||||
|
||||
An arbitrary integer associated with that index; this information will
|
||||
be passed back to L</FILTER>.
|
||||
|
||||
=item C<idxStr>
|
||||
|
||||
An arbitrary str associated with that index; this information will
|
||||
be passed back to L</FILTER>.
|
||||
|
||||
=item C<orderByConsumed>
|
||||
|
||||
A boolean telling the sqlite core if the C<$order_by> information
|
||||
has been taken into account or not.
|
||||
|
||||
=item C<estimatedCost>
|
||||
|
||||
A float that should be set to the estimated number of disk access
|
||||
operations required to execute this query against the virtual
|
||||
table. The SQLite core will often call BEST_INDEX multiple times with
|
||||
different constraints, obtain multiple cost estimates, then choose the
|
||||
query plan that gives the lowest estimate.
|
||||
|
||||
=item C<estimatedRows>
|
||||
|
||||
An integer giving the estimated number of rows returned by that query.
|
||||
|
||||
=back
|
||||
|
||||
|
||||
|
||||
=head3 OPEN
|
||||
|
||||
Called to instanciate a new cursor.
|
||||
The default implementation appends C<"::Cursor"> to the current
|
||||
classname and calls C<NEW()> within that cursor class.
|
||||
|
||||
=head3 _SQLITE_UPDATE
|
||||
|
||||
This is the dispatch method implementing the C<xUpdate()> callback
|
||||
for virtual tables. The default implementation applies the algorithm
|
||||
described in L<http://sqlite.org/vtab.html#xupdate> to decide
|
||||
to call L</INSERT>, L</DELETE> or L</UPDATE>; so there is no reason
|
||||
to override this method in subclasses.
|
||||
|
||||
=head3 INSERT
|
||||
|
||||
my $rowid = $vtab->INSERT($new_rowid, @values);
|
||||
|
||||
This method should be overridden in subclasses to implement
|
||||
insertion of a new row into the virtual table.
|
||||
The size of the C<@values> array corresponds to the
|
||||
number of columns declared through L</VTAB_TO_DECLARE>.
|
||||
The C<$new_rowid> may be explicitly given, or it may be
|
||||
C<undef>, in which case the method must compute a new id
|
||||
and return it as the result of the method call.
|
||||
|
||||
=head3 DELETE
|
||||
|
||||
$vtab->INSERT($old_rowid);
|
||||
|
||||
This method should be overridden in subclasses to implement
|
||||
deletion of a row from the virtual table.
|
||||
|
||||
=head3 UPDATE
|
||||
|
||||
$vtab->UPDATE($old_rowid, $new_rowid, @values);
|
||||
|
||||
This method should be overridden in subclasses to implement
|
||||
a row update within the virtual table. Usually C<$old_rowid> is equal
|
||||
to C<$new_rowid>, which is a regular update; however, the rowid
|
||||
could be changed from a SQL statement such as
|
||||
|
||||
UPDATE table SET rowid=rowid+1 WHERE ...;
|
||||
|
||||
=head3 BEGIN_TRANSACTION
|
||||
|
||||
Called to begin a transaction on the virtual table.
|
||||
|
||||
=head3 SYNC_TRANSACTION
|
||||
|
||||
Called to signal the start of a two-phase commit on the virtual table.
|
||||
|
||||
=head3 SYNC_TRANSACTION
|
||||
|
||||
Called to commit a virtual table transaction.
|
||||
|
||||
=head3 ROLLBACK_TRANSACTION
|
||||
|
||||
Called to rollback a virtual table transaction.
|
||||
|
||||
=head3 RENAME
|
||||
|
||||
$vtab->RENAME($new_name)
|
||||
|
||||
Called to rename a virtual table.
|
||||
|
||||
=head3 SAVEPOINT
|
||||
|
||||
$vtab->SAVEPOINT($savepoint)
|
||||
|
||||
Called to signal the virtual table to save its current state
|
||||
at savepoint C<$savepoint> (an integer).
|
||||
|
||||
=head3 ROLLBACK_TO
|
||||
|
||||
$vtab->ROLLBACK_TO($savepoint)
|
||||
|
||||
Called to signal the virtual table to return to the state
|
||||
C<$savepoint>. This will invalidate all savepoints with values
|
||||
greater than C<$savepoint>.
|
||||
|
||||
=head3 RELEASE
|
||||
|
||||
$vtab->RELEASE($savepoint)
|
||||
|
||||
Called to invalidate all savepoints with values
|
||||
greater or equal to C<$savepoint>.
|
||||
|
||||
|
||||
=head2 Utility instance methods
|
||||
|
||||
Methods in this section are in lower case, because they
|
||||
are not called directly from the sqlite kernel; these
|
||||
are utility methods to be called from other methods
|
||||
described above.
|
||||
|
||||
=head3 dbh
|
||||
|
||||
This method returns the database handle (C<$dbh>) associated with
|
||||
the current virtual table.
|
||||
|
||||
|
||||
=head1 CURSOR METHODS
|
||||
|
||||
=head2 Class methods
|
||||
|
||||
=head3 NEW
|
||||
|
||||
my $cursor = $cursor_class->NEW($vtable, @args)
|
||||
|
||||
Instanciates a new cursor.
|
||||
The default implementation just returns a blessed hashref
|
||||
with keys C<vtable> and C<args>.
|
||||
|
||||
=head2 Instance methods
|
||||
|
||||
=head3 FILTER
|
||||
|
||||
$cursor->FILTER($idxNum, $idxStr, @values);
|
||||
|
||||
This method begins a search of a virtual table.
|
||||
|
||||
The C<$idxNum> and C<$idxStr> arguments correspond to values returned
|
||||
by L</BEST_INDEX> for the chosen index. The specific meanings of
|
||||
those values are unimportant to SQLite, as long as C<BEST_INDEX> and
|
||||
C<FILTER> agree on what that meaning is.
|
||||
|
||||
The C<BEST_INDEX> method may have requested the values of certain
|
||||
expressions using the C<argvIndex> values of the
|
||||
C<$constraints> list. Those values are passed to C<FILTER> through
|
||||
the C<@values> array.
|
||||
|
||||
If the virtual table contains one or more rows that match the search
|
||||
criteria, then the cursor must be left point at the first
|
||||
row. Subsequent calls to L</EOF> must return false. If there are
|
||||
no rows match, then the cursor must be left in a state that will cause
|
||||
L</EOF> to return true. The SQLite engine will use the
|
||||
L</COLUMN> and L</ROWID> methods to access that row content. The L</NEXT>
|
||||
method will be used to advance to the next row.
|
||||
|
||||
|
||||
=head3 EOF
|
||||
|
||||
This method must return false if the cursor currently points to a
|
||||
valid row of data, or true otherwise. This method is called by the SQL
|
||||
engine immediately after each L</FILTER> and L</NEXT> invocation.
|
||||
|
||||
=head3 NEXT
|
||||
|
||||
This method advances the cursor to the next row of a
|
||||
result set initiated by L</FILTER>. If the cursor is already pointing at
|
||||
the last row when this method is called, then the cursor no longer
|
||||
points to valid data and a subsequent call to the L</EOF> method must
|
||||
return true. If the cursor is successfully advanced to
|
||||
another row of content, then subsequent calls to L</EOF> must return
|
||||
false.
|
||||
|
||||
=head3 COLUMN
|
||||
|
||||
my $value = $cursor->COLUMN($idxCol);
|
||||
|
||||
The SQLite core invokes this method in order to find the value for the
|
||||
N-th column of the current row. N is zero-based so the first column is
|
||||
numbered 0.
|
||||
|
||||
=head3 ROWID
|
||||
|
||||
my $value = $cursor->ROWID;
|
||||
|
||||
Returns the I<rowid> of row that the cursor is currently pointing at.
|
||||
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
L<SQLite::VirtualTable> is another module for virtual tables written
|
||||
in Perl, but designed for the reverse use case : instead of starting a
|
||||
Perl program, and embedding the SQLite library into it, the intended
|
||||
use is to start an sqlite program, and embed the Perl interpreter
|
||||
into it.
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Laurent Dami E<lt>dami@cpan.orgE<gt>
|
||||
|
||||
|
||||
=head1 COPYRIGHT AND LICENSE
|
||||
|
|
|
@ -1,92 +1,104 @@
|
|||
#======================================================================
|
||||
package DBD::SQLite::VirtualTable::FileContent;
|
||||
#======================================================================
|
||||
use strict;
|
||||
use warnings;
|
||||
use base 'DBD::SQLite::VirtualTable';
|
||||
use List::MoreUtils qw/none/;
|
||||
|
||||
my %option_ok = map {($_ => 1)} qw/source content_col path_col
|
||||
expose root get_content/;
|
||||
|
||||
my %defaults = (
|
||||
content_col => "content",
|
||||
path_col => "path",
|
||||
expose => "*",
|
||||
get_content => "DBD::SQLite::VirtualTable::FileContent::get_content",
|
||||
);
|
||||
|
||||
|
||||
=head1 NAME
|
||||
#----------------------------------------------------------------------
|
||||
# object instanciation
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
DBD::SQLite::VirtualTable::FileContent -- virtual table for viewing file contents
|
||||
sub NEW {
|
||||
my $class = shift;
|
||||
|
||||
my $self = $class->_PREPARE_SELF(@_);
|
||||
|
||||
=head1 SYNOPSIS
|
||||
local $" = ", "; # for array interpolation in strings
|
||||
|
||||
-- $dbh->sqlite_create_module(filesys => "DBD::SQLite::VirtualTable::FileContent");
|
||||
# initial parameter check
|
||||
!@{$self->{columns}}
|
||||
or die "${class}->NEW(): illegal options: @{$self->{columns}}";
|
||||
$self->{options}{source}
|
||||
or die "${class}->NEW(): missing (source=...)";
|
||||
my @bad_options = grep {!$option_ok{$_}} keys %{$self->{options}};
|
||||
!@bad_options
|
||||
or die "${class}->NEW(): bad options: @bad_options";
|
||||
|
||||
CREATE VIRTUAL TABLE tbl USING filesys(file_content,
|
||||
index_table = idx,
|
||||
path_col = path,
|
||||
expose = "path, col1, col2, col3",
|
||||
root = "/foo/bar")
|
||||
|
||||
|
||||
-- OR : expose = *
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
A "FileContent" virtual table is like a database view on some underlying
|
||||
I<index table>, which has a column containing paths to
|
||||
files; the virtual table then adds a supplementary column which exposes
|
||||
the content from those files.
|
||||
|
||||
This is especially useful as an "external content" to some
|
||||
fulltext table (see L<DBD::SQLite::Fulltext_search>) : the index
|
||||
table stores some metadata about files, and then the fulltext engine
|
||||
can index both the metadata and the file contents.
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=head2 new
|
||||
|
||||
|
||||
=cut
|
||||
|
||||
|
||||
sub initialize {
|
||||
my $self = shift;
|
||||
|
||||
# verifications
|
||||
@{$self->{columns}} == 1
|
||||
or die "FileContent virtual table should declare exactly 1 content column";
|
||||
for my $opt (qw/index_table path_col/) {
|
||||
$self->{options}{$opt}
|
||||
or die "FileContent virtual table: option '$opt' is missing";
|
||||
# defaults ... tempted to use //= but we still want to support perl 5.8 :-(
|
||||
foreach my $k (keys %defaults) {
|
||||
defined $self->{options}{$k}
|
||||
or $self->{options}{$k} = $defaults{$k};
|
||||
}
|
||||
|
||||
# get list of columns from the index table
|
||||
my $ix_table = $self->{options}{index_table};
|
||||
my $sql = "PRAGMA table_info($ix_table)";
|
||||
my $base_cols = $self->dbh->selectcol_arrayref($sql, {Columns => [2]});
|
||||
@$base_cols
|
||||
or die "wrong index table: $ix_table";
|
||||
# get list of columns from the source table
|
||||
my $src_table = $self->{options}{source};
|
||||
my $sql = "PRAGMA table_info($src_table)";
|
||||
my $dbh = ${$self->{dbh_ref}}; # can't use method ->dbh, not blessed yet
|
||||
my $src_info = $dbh->selectall_arrayref($sql, {Slice => [1, 2]});
|
||||
@$src_info
|
||||
or die "${class}->NEW(source=$src_table): no such table in database";
|
||||
|
||||
# associate each source colname with its type info or " " (should eval true)
|
||||
my %src_col = map { ($_->[0] => $_->[1] || " ") } @$src_info;
|
||||
|
||||
|
||||
# check / complete the exposed columns
|
||||
$self->{options}{expose} = "*" if not exists $self->{options}{expose};
|
||||
my @exposed_cols;
|
||||
if ($self->{options}{expose} eq '*') {
|
||||
@exposed_cols = @$base_cols;
|
||||
@exposed_cols = map {$_->[0]} @$src_info;
|
||||
}
|
||||
else {
|
||||
@exposed_cols = split /\s*,\s*/, ($self->{options}{expose} || "");
|
||||
my %is_ok_col = map {$_ => 1} @$base_cols;
|
||||
my @bad_cols = grep {!$is_ok_col{$_}} @exposed_cols;
|
||||
local $" = ", ";
|
||||
die "table $ix_table has no column named @bad_cols" if @bad_cols;
|
||||
@exposed_cols = split /\s*,\s*/, $self->{options}{expose};
|
||||
my @bad_cols = grep { !$src_col{$_} } @exposed_cols;
|
||||
die "table $src_table has no column named @bad_cols" if @bad_cols;
|
||||
}
|
||||
push @{$self->{columns}}, @exposed_cols;
|
||||
none {$_ eq $self->{options}{content_col}} @exposed_cols
|
||||
or die "$class: $self->{options}{content_col} cannot be both the "
|
||||
. "content_col and an exposed col";
|
||||
|
||||
# build the list of columns for this table
|
||||
$self->{columns} = [ "$self->{options}{content_col} TEXT",
|
||||
map {"$_ $src_col{$_}"} @exposed_cols ];
|
||||
|
||||
# acquire a coderef to the get_content() implementation
|
||||
no strict 'refs';
|
||||
$self->{get_content} = \ &{$self->{options}{get_content}};
|
||||
|
||||
bless $self, $class;
|
||||
}
|
||||
|
||||
sub _build_headers {
|
||||
my $self = shift;
|
||||
|
||||
my $cols = $self->sqlite_table_info;
|
||||
|
||||
# headers : names of columns, without type information
|
||||
$self->{headers} = [ map {$_->{name}} @$cols ];
|
||||
}
|
||||
|
||||
|
||||
sub _SQLITE_UPDATE {
|
||||
my ($self, $old_rowid, $new_rowid, @values) = @_;
|
||||
|
||||
die "readonly database";
|
||||
}
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# method for initiating a search
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
sub BEST_INDEX {
|
||||
my ($self, $constraints, $order_by) = @_;
|
||||
|
||||
$self->_build_headers if !$self->{headers};
|
||||
|
||||
my @conditions;
|
||||
my $ix = 0;
|
||||
foreach my $constraint (grep {$_->{usable}} @$constraints) {
|
||||
|
@ -96,12 +108,14 @@ sub BEST_INDEX {
|
|||
next if $col == 0;
|
||||
|
||||
# for other columns, build a fragment for SQL WHERE on the underlying table
|
||||
my $colname = $col == -1 ? "rowid" : $self->{columns}[$col];
|
||||
my $colname = $col == -1 ? "rowid" : $self->{headers}[$col];
|
||||
push @conditions, "$colname $constraint->{op} ?";
|
||||
$constraint->{argvIndex} = $ix++;
|
||||
$constraint->{omit} = 1; # SQLite doesn't need to re-check the op
|
||||
}
|
||||
|
||||
# TODO : exploit $order_by to add ordering clauses within idxStr
|
||||
|
||||
my $outputs = {
|
||||
idxNum => 1,
|
||||
idxStr => join(" AND ", @conditions),
|
||||
|
@ -113,8 +127,46 @@ sub BEST_INDEX {
|
|||
return $outputs;
|
||||
}
|
||||
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# method for preventing updates
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
sub _SQLITE_UPDATE {
|
||||
my ($self, $old_rowid, $new_rowid, @values) = @_;
|
||||
|
||||
die "attempt to update a readonly virtual table";
|
||||
}
|
||||
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# file slurping function (not a method!)
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
sub get_content {
|
||||
my ($path, $root) = @_;
|
||||
|
||||
$path = "$root/$path" if $root;
|
||||
|
||||
my $content = "";
|
||||
if (open my $fh, "<", $path) {
|
||||
local $/; # slurp the whole file into a scalar
|
||||
$content = <$fh>;
|
||||
close $fh;
|
||||
}
|
||||
else {
|
||||
warn "can't open $path";
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#======================================================================
|
||||
package DBD::SQLite::VirtualTable::FileContent::Cursor;
|
||||
use 5.010;
|
||||
#======================================================================
|
||||
use strict;
|
||||
use warnings;
|
||||
use base "DBD::SQLite::VirtualTable::Cursor";
|
||||
|
@ -127,10 +179,10 @@ sub FILTER {
|
|||
|
||||
# build SQL
|
||||
local $" = ", ";
|
||||
my @cols = @{$vtable->{columns}};
|
||||
my @cols = @{$vtable->{headers}};
|
||||
$cols[0] = 'rowid'; # replace the content column by the rowid
|
||||
push @cols, $vtable->{options}{path_col}; # path col in last position
|
||||
my $sql = "SELECT @cols FROM $vtable->{options}{index_table}";
|
||||
my $sql = "SELECT @cols FROM $vtable->{options}{source}";
|
||||
$sql .= " WHERE $idxStr" if $idxStr;
|
||||
|
||||
# request on the index table
|
||||
|
@ -175,35 +227,107 @@ sub file_content {
|
|||
|
||||
my $root = $self->{vtable}{options}{root};
|
||||
my $path = $self->{row}[-1];
|
||||
$path = "$root/$path" if $root;
|
||||
|
||||
my $content = "";
|
||||
if (open my $fh, "<", $path) {
|
||||
local $/; # slurp the whole file into a scalar
|
||||
$content = <$fh>;
|
||||
close $fh;
|
||||
}
|
||||
else {
|
||||
warn "can't open $path";
|
||||
}
|
||||
|
||||
return $content;
|
||||
return $self->{vtable}{get_content}->($path, $root);
|
||||
}
|
||||
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
|
||||
=head1 NAME
|
||||
|
||||
DBD::SQLite::VirtualTable::FileContent -- virtual table for viewing file contents
|
||||
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
Within Perl :
|
||||
|
||||
$dbh->sqlite_create_module(fcontent => "DBD::SQLite::VirtualTable::FileContent");
|
||||
|
||||
Then, within SQL :
|
||||
|
||||
CREATE VIRTUAL TABLE tbl USING fcontent(
|
||||
source = src_table,
|
||||
content_col = content,
|
||||
path_col = path,
|
||||
expose = "path, col1, col2, col3", -- or "*"
|
||||
root = "/foo/bar"
|
||||
get_content = Foo::Bar::read_from_file
|
||||
);
|
||||
|
||||
SELECT col1, path, content FROM tbl WHERE ...;
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
A "FileContent" virtual table is bound to some underlying I<source
|
||||
table>, which has a column containing paths to files. The virtual
|
||||
table behaves like a database view on the source table, with an added
|
||||
column which exposes the content from those files.
|
||||
|
||||
This is especially useful as an "external content" to some
|
||||
fulltext table (see L<DBD::SQLite::Fulltext_search>) : the index
|
||||
table stores some metadata about files, and then the fulltext engine
|
||||
can index both the metadata and the file contents.
|
||||
|
||||
=head1 PARAMETERS
|
||||
|
||||
Parameters for creating a C<FileContent> virtual table are
|
||||
specified within the C<CREATE VIRTUAL TABLE> statement, just
|
||||
like regular column declarations, but with an '=' sign.
|
||||
Authorized parameters are :
|
||||
|
||||
=over
|
||||
|
||||
=item C<source>
|
||||
|
||||
The name of the I<source table>.
|
||||
This parameter is mandatory. All other parameters are optional.
|
||||
|
||||
=item C<content_col>
|
||||
|
||||
The name of the virtual column exposing file contents.
|
||||
The default is C<content>.
|
||||
|
||||
=item C<path_col>
|
||||
|
||||
The name of the column in C<source> that contains paths to files.
|
||||
The default is C<path>.
|
||||
|
||||
=item C<expose>
|
||||
|
||||
A comma-separated list (within double quotes) of source column names
|
||||
to be exposed by the virtual table. The default is C<"*">, which means
|
||||
all source columns.
|
||||
|
||||
=item C<root>
|
||||
|
||||
An optional root directory that will be prepended to the I<path> column
|
||||
when opening files.
|
||||
|
||||
=item C<get_content>
|
||||
|
||||
Fully qualified name of a Perl function for reading file contents.
|
||||
The default implementation just slurps the entire file into a string;
|
||||
but this hook can point to more sophisticated implementations, like for
|
||||
example a function that would remove html tags. The hooked function is
|
||||
called like this :
|
||||
|
||||
$file_content = $get_content->($path, $root);
|
||||
|
||||
=back
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Laurent Dami E<lt>dami@cpan.orgE<gt>
|
||||
|
||||
=head1 COPYRIGHT AND LICENSE
|
||||
|
||||
Copyright Laurent Dami, 2014.
|
||||
|
||||
Parts of the code are borrowed from L<SQLite::VirtualTable>,
|
||||
copyright (C) 2006, 2009 by Qindel Formacion y Servicios, S. L.
|
||||
|
||||
This library is free software; you can redistribute it and/or modify
|
||||
it under the same terms as Perl itself.
|
||||
|
||||
|
|
|
@ -1,40 +1,12 @@
|
|||
#======================================================================
|
||||
package DBD::SQLite::VirtualTable::PerlData;
|
||||
#======================================================================
|
||||
use strict;
|
||||
use warnings;
|
||||
use base 'DBD::SQLite::VirtualTable';
|
||||
use List::MoreUtils qw/mesh/;
|
||||
|
||||
|
||||
=head1 NAME
|
||||
|
||||
DBD::SQLite::VirtualTable::PerlData -- virtual table for connecting to perl data
|
||||
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
-- $dbh->sqlite_create_module(perl => "DBD::SQLite::VirtualTable::PerlData");
|
||||
|
||||
CREATE VIRTUAL TABLE tbl USING perl(foo, bar, etc,
|
||||
arrayrefs="some_global_variable")
|
||||
|
||||
CREATE VIRTUAL TABLE tbl USING perl(foo, bar, etc,
|
||||
hashrefs="some_global_variable")
|
||||
|
||||
CREATE VIRTUAL TABLE tbl USING perl(single_col
|
||||
colref="some_global_variable")
|
||||
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=head2 new
|
||||
|
||||
=cut
|
||||
|
||||
|
||||
|
||||
# private data for translating comparison operators from Sqlite to Perl
|
||||
my $TXT = 0;
|
||||
my $NUM = 1;
|
||||
|
@ -48,10 +20,13 @@ my %SQLOP2PERLOP = (
|
|||
'MATCH' => [ '=~', '=~' ],
|
||||
);
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# instanciation methods
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
sub initialize {
|
||||
my $self = shift;
|
||||
my $class = ref $self;
|
||||
sub NEW {
|
||||
my $class = shift;
|
||||
my $self = $class->_PREPARE_SELF(@_);
|
||||
|
||||
# verifications
|
||||
my $n_cols = @{$self->{columns}};
|
||||
|
@ -68,36 +43,33 @@ sub initialize {
|
|||
no strict "refs";
|
||||
defined ${$symbolic_ref}
|
||||
or die "$class: can't find global variable \$$symbolic_ref";
|
||||
$self->{rows} = \${$symbolic_ref};
|
||||
$self->{rows} = \ ${$symbolic_ref};
|
||||
|
||||
bless $self, $class;
|
||||
}
|
||||
|
||||
|
||||
sub initialize_bis {
|
||||
sub _build_headers_optypes {
|
||||
my $self = shift;
|
||||
|
||||
# the code below cannot happen within initialize() because VTAB_TO_DECLARE()
|
||||
# has not been called until the end of NEW(). So we do it here, which is
|
||||
# called lazily at the first invocation if BEST_INDEX().
|
||||
my $cols = $self->sqlite_table_info;
|
||||
|
||||
# get names and types of columns after they have been parsed by sqlite
|
||||
my $sth = $self->dbh->prepare("PRAGMA table_info($self->{vtab_name})");
|
||||
$sth->execute;
|
||||
# headers : names of columns, without type information
|
||||
$self->{headers} = [ map {$_->{name}} @$cols ];
|
||||
|
||||
# build private data 'headers' and 'optypes'
|
||||
while (my $row = $sth->fetch) {
|
||||
my ($colname, $coltype) = @{$row}[1, 2];
|
||||
push @{$self->{headers}}, $colname;
|
||||
|
||||
# apply algorithm from datatype3.html" for type affinity
|
||||
push @{$self->{optypes}}, $coltype =~ /INT|REAL|FLOA|DOUB/i ? $NUM : $TXT;
|
||||
}
|
||||
# optypes : either $NUM or $TEXT for each column
|
||||
# (applying algorithm from datatype3.html" for type affinity)
|
||||
$self->{optypes}
|
||||
= [ map {$_->{type} =~ /INT|REAL|FLOA|DOUB/i ? $NUM : $TXT} @$cols ];
|
||||
}
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# method for initiating a search
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
sub BEST_INDEX {
|
||||
my ($self, $constraints, $order_by) = @_;
|
||||
|
||||
$self->initialize_bis if not exists $self->{headers};
|
||||
$self->_build_headers_optypes if !$self->{headers};
|
||||
|
||||
# for each constraint, build a Perl code fragment. Those will be gathered
|
||||
# in FILTER() for deciding which rows match the constraints.
|
||||
|
@ -107,8 +79,8 @@ sub BEST_INDEX {
|
|||
my $col = $constraint->{col};
|
||||
my ($member, $optype);
|
||||
|
||||
# build a Perl code fragment. Those will be gathered
|
||||
# in FILTER() for deciding which rows match the constraints.
|
||||
# build a Perl code fragment. Those fragments will be gathered
|
||||
# and eval-ed in FILTER(), for deciding which rows match the constraints.
|
||||
if ($col == -1) {
|
||||
# constraint on rowid
|
||||
$member = '$i';
|
||||
|
@ -126,12 +98,12 @@ sub BEST_INDEX {
|
|||
my $quote = $op eq '=~' ? 'qr' : 'q';
|
||||
push @conditions, "($member $op ${quote}{%s})";
|
||||
|
||||
# info passed back to the sqlite kernel -- see vtab.html in sqlite doc
|
||||
# info passed back to the SQLite core -- see vtab.html in sqlite doc
|
||||
$constraint->{argvIndex} = $ix++;
|
||||
$constraint->{omit} = 1;
|
||||
}
|
||||
|
||||
# further info for the sqlite kernel
|
||||
# further info for the SQLite core
|
||||
my $outputs = {
|
||||
idxNum => 1,
|
||||
idxStr => (join(" && ", @conditions) || "1"),
|
||||
|
@ -144,6 +116,10 @@ sub BEST_INDEX {
|
|||
}
|
||||
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# methods for data update
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
sub _build_new_row {
|
||||
my ($self, $values) = @_;
|
||||
|
||||
|
@ -191,10 +167,9 @@ sub UPDATE {
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#======================================================================
|
||||
package DBD::SQLite::VirtualTable::PerlData::Cursor;
|
||||
use 5.010;
|
||||
#======================================================================
|
||||
use strict;
|
||||
use warnings;
|
||||
use base "DBD::SQLite::VirtualTable::Cursor";
|
||||
|
@ -211,8 +186,6 @@ sub FILTER {
|
|||
# build a method coderef to fetch matching rows
|
||||
my $perl_code = sprintf "sub {my (\$self, \$i) = \@_; $idxStr}", @values;
|
||||
|
||||
# print STDERR "PERL $perl_code\n";
|
||||
|
||||
$self->{is_wanted_row} = eval $perl_code
|
||||
or die "couldn't eval q{$perl_code} : $@";
|
||||
|
||||
|
@ -263,24 +236,74 @@ __END__
|
|||
|
||||
=head1 NAME
|
||||
|
||||
DBD::SQLite::VirtualTable -- Abstract parent class for implementing virtual tables
|
||||
DBD::SQLite::VirtualTable::PerlData -- virtual table hooked to Perl data
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
package My::Virtual::Table;
|
||||
use parent 'DBD::SQLite::VirtualTable';
|
||||
Within Perl :
|
||||
|
||||
$dbh->sqlite_create_module(perl => "DBD::SQLite::VirtualTable::PerlData");
|
||||
|
||||
Then, within SQL :
|
||||
|
||||
|
||||
CREATE VIRTUAL TABLE atbl USING perl(foo, bar, etc,
|
||||
arrayrefs="some::global::var::aref")
|
||||
|
||||
CREATE VIRTUAL TABLE htbl USING perl(foo, bar, etc,
|
||||
hashrefs="some::global::var::href")
|
||||
|
||||
CREATE VIRTUAL TABLE ctbl USING perl(single_col
|
||||
colref="some::global::var::ref")
|
||||
|
||||
|
||||
SELECT foo, bar FROM atbl WHERE ...;
|
||||
|
||||
sub ...
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
TODO
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
TODO
|
||||
A C<PerlData> virtual table is a database view on some datastructure
|
||||
within a Perl program. The data can be read or modified both from SQL
|
||||
and from Perl. This is useful for simple import/export
|
||||
operations, for debugging purposes, for joining data from different
|
||||
sources, etc.
|
||||
|
||||
|
||||
=head1 PARAMETERS
|
||||
|
||||
Parameters for creating a C<PerlData> virtual table are specified
|
||||
within the C<CREATE VIRTUAL TABLE> statement, mixed with regular
|
||||
column declarations, but with an '=' sign.
|
||||
|
||||
The only authorized (and mandatory) parameter is the one that
|
||||
specifies the Perl datastructure to which the virtual table is bound.
|
||||
The Perl data must be given as a fully qualified name of a global variable;
|
||||
it can be one of three different kinds :
|
||||
|
||||
=over
|
||||
|
||||
=item C<arrayrefs>
|
||||
|
||||
arrayref that contains an arrayref for each row
|
||||
|
||||
=item C<hashrefs>
|
||||
|
||||
arrayref that contains a hashref for each row
|
||||
|
||||
=item C<colref>
|
||||
|
||||
arrayref that contains a single scalar for each row
|
||||
(obviously this is a single-column virtual table)
|
||||
|
||||
=back
|
||||
|
||||
=head1 USAGE
|
||||
|
||||
[TODO]
|
||||
|
||||
=head1 AUTHOR
|
||||
|
||||
Laurent Dami E<lt>dami@cpan.orgE<gt>
|
||||
|
||||
=head1 COPYRIGHT AND LICENSE
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ is $rows->[0]{bar}, "auto_vivify:1", "bar column";
|
|||
$sql = "SELECT * FROM foobar ";
|
||||
$rows = $dbh->selectall_arrayref($sql, {Slice => {}});
|
||||
is scalar(@$rows), 5, "got 5 rows again";
|
||||
|
||||
is_deeply([sort keys %{$rows->[0]}], [qw/bar foo/], "col list OK");
|
||||
|
||||
|
||||
|
@ -43,7 +44,7 @@ use warnings;
|
|||
use base 'DBD::SQLite::VirtualTable';
|
||||
use YAML;
|
||||
|
||||
sub initialize {
|
||||
sub INITIALIZE {
|
||||
my $self = shift;
|
||||
# stupid pragma call, just to check that the dbh is OK
|
||||
$self->dbh->do("PRAGMA application_id=999");
|
||||
|
|
|
@ -9,24 +9,25 @@ use t::lib::Test qw/connect_ok/;
|
|||
use Test::More;
|
||||
use Test::NoWarnings;
|
||||
|
||||
plan tests => 23;
|
||||
plan tests => 20;
|
||||
|
||||
my $dbfile = "tmp.sqlite";
|
||||
my $dbh = connect_ok( dbfile => $dbfile, RaiseError => 1, AutoCommit => 1 );
|
||||
|
||||
ok !$DBD::SQLite::VirtualTable::T::INITIALIZE_COUNT, "no vtab initialized";
|
||||
ok !$DBD::SQLite::VirtualTable::T::CREATE_COUNT &&
|
||||
!$DBD::SQLite::VirtualTable::T::CONNECT_COUNT, "no vtab created";
|
||||
|
||||
# create 2 separate SQLite modules from the same Perl class
|
||||
$dbh->sqlite_create_module(vtab1 => "DBD::SQLite::VirtualTable::T");
|
||||
$dbh->sqlite_create_module(vtab2 => "DBD::SQLite::VirtualTable::T");
|
||||
ok !$DBD::SQLite::VirtualTable::T::INITIALIZE_COUNT, "still no vtab";
|
||||
ok !$DBD::SQLite::VirtualTable::T::CREATE_COUNT &&
|
||||
!$DBD::SQLite::VirtualTable::T::CONNECT_COUNT, "still no vtab";
|
||||
|
||||
# create 2 virtual tables from module vtab1
|
||||
ok $dbh->do("CREATE VIRTUAL TABLE foobar USING vtab1(foo, bar)"), "create foobar";
|
||||
ok $dbh->do("CREATE VIRTUAL TABLE barfoo USING vtab1(foo, bar)"), "create barfoo";
|
||||
is $DBD::SQLite::VirtualTable::T::CREATE_COUNT, 2, "2 vtab created";
|
||||
ok !$DBD::SQLite::VirtualTable::T::CONNECT_COUNT, "no vtab connected";
|
||||
is $DBD::SQLite::VirtualTable::T::INITIALIZE_COUNT, 2, "2 vtab initialized";
|
||||
|
||||
# destructor is called when a vtable is dropped
|
||||
ok !$DBD::SQLite::VirtualTable::T::DESTROY_COUNT, "no vtab destroyed";
|
||||
|
@ -43,18 +44,15 @@ is $DBD::SQLite::VirtualTable::T::DESTROY_MODULE_COUNT, 2, "2 modules destroyed"
|
|||
# reconnect, check that we go through the CONNECT method
|
||||
undef $DBD::SQLite::VirtualTable::T::CREATE_COUNT;
|
||||
undef $DBD::SQLite::VirtualTable::T::CONNECT_COUNT;
|
||||
undef $DBD::SQLite::VirtualTable::T::INITIALIZE_COUNT;
|
||||
|
||||
$dbh = connect_ok( dbfile => $dbfile, RaiseError => 1, AutoCommit => 1 );
|
||||
$dbh->sqlite_create_module(vtab1 => "DBD::SQLite::VirtualTable::T");
|
||||
ok !$DBD::SQLite::VirtualTable::T::CREATE_COUNT, "no vtab created";
|
||||
ok !$DBD::SQLite::VirtualTable::T::CONNECT_COUNT, "no vtab connected";
|
||||
ok !$DBD::SQLite::VirtualTable::T::INITIALIZE_COUNT, "no vtab initialized";
|
||||
|
||||
my $sth = $dbh->prepare("SELECT * FROM barfoo");
|
||||
ok !$DBD::SQLite::VirtualTable::T::CREATE_COUNT, "no vtab created";
|
||||
is $DBD::SQLite::VirtualTable::T::CONNECT_COUNT, 1, "1 vtab connected";
|
||||
is $DBD::SQLite::VirtualTable::T::INITIALIZE_COUNT, 1, "1 vtab initialized";
|
||||
|
||||
|
||||
package DBD::SQLite::VirtualTable::T;
|
||||
|
@ -62,7 +60,6 @@ use base 'DBD::SQLite::VirtualTable';
|
|||
|
||||
our $CREATE_COUNT;
|
||||
our $CONNECT_COUNT;
|
||||
our $INITIALIZE_COUNT;
|
||||
our $DESTROY_COUNT;
|
||||
our $DESTROY_MODULE_COUNT;
|
||||
our $DROP_COUNT;
|
||||
|
@ -70,7 +67,6 @@ our $DISCONNECT_COUNT;
|
|||
|
||||
sub CREATE {$CREATE_COUNT++; return shift->SUPER::CREATE(@_)}
|
||||
sub CONNECT {$CONNECT_COUNT++; return shift->SUPER::CONNECT(@_)}
|
||||
sub initialize {$INITIALIZE_COUNT++}
|
||||
sub DROP {$DROP_COUNT++}
|
||||
sub DISCONNECT {$DISCONNECT_COUNT++}
|
||||
sub DESTROY {$DESTROY_COUNT++}
|
||||
|
|
|
@ -34,9 +34,7 @@ ok $dbh->sqlite_create_module(fs => "DBD::SQLite::VirtualTable::FileContent"),
|
|||
|
||||
|
||||
ok $dbh->do(<<""), "create vtable";
|
||||
CREATE VIRTUAL TABLE vfs USING fs(content,
|
||||
index_table = base,
|
||||
path_col = path,
|
||||
CREATE VIRTUAL TABLE vfs USING fs(source = base,
|
||||
expose = "path, foo, bar",
|
||||
root = "$FindBin::Bin")
|
||||
|
||||
|
|
|
@ -46,22 +46,20 @@ my @perl_files = grep {/\.(pl|pm|pod)$/} @files;
|
|||
# open database
|
||||
my $dbh = connect_ok( dbfile => $dbfile, RaiseError => 1, AutoCommit => 1 );
|
||||
|
||||
# create index table
|
||||
# create the source table and populate it
|
||||
$dbh->do("CREATE TABLE files (id INTEGER PRIMARY KEY, path TEXT)");
|
||||
my $sth = $dbh->prepare("INSERT INTO files(path) VALUES (?)");
|
||||
$sth->execute($_) foreach @perl_files;
|
||||
|
||||
|
||||
# create vtab table
|
||||
# create the virtual table
|
||||
$dbh->sqlite_create_module(fs => "DBD::SQLite::VirtualTable::FileContent");
|
||||
$dbh->do(<<"");
|
||||
CREATE VIRTUAL TABLE vfs USING fs(content,
|
||||
index_table = files,
|
||||
path_col = path,
|
||||
CREATE VIRTUAL TABLE vfs USING fs(source = files,
|
||||
expose = "path",
|
||||
root = "$distrib_dir")
|
||||
|
||||
# create fts table
|
||||
# create the fulltext indexing table and populate it
|
||||
$dbh->do('CREATE VIRTUAL TABLE fts USING fts4(content="vfs")');
|
||||
note "building fts index....";
|
||||
$dbh->do("INSERT INTO fts(fts) VALUES ('rebuild')");
|
||||
|
@ -89,6 +87,7 @@ foreach my $test (@tests) {
|
|||
}
|
||||
|
||||
# see if data was properly stored: disconnect, reconnect and test again
|
||||
$dbh->disconnect;
|
||||
undef $dbh;
|
||||
$dbh = connect_ok( dbfile => $dbfile, RaiseError => 1, AutoCommit => 1 );
|
||||
$dbh->sqlite_create_module(fs => "DBD::SQLite::VirtualTable::FileContent");
|
||||
|
|
Loading…
Add table
Reference in a new issue