File Coverage

blib/lib/DBD/SQLeet/VirtualTable/PerlData.pm
Criterion Covered Total %
statement 106 126 84.1
branch 33 50 66.0
condition 9 11 81.8
subroutine 22 24 91.6
pod 4 4 100.0
total 174 215 80.9


line stmt bran cond sub pod time code
1             #======================================================================
2             package DBD::SQLeet::VirtualTable::PerlData;
3             #======================================================================
4 3     3   3736 use strict;
  3         6  
  3         77  
5 3     3   14 use warnings;
  3         5  
  3         106  
6 3     3   14 use base 'DBD::SQLeet::VirtualTable';
  3         6  
  3         1385  
7 3     3   17 use DBD::SQLeet;
  3         4  
  3         98  
8 3 50   3   14 use constant SQLITE_3010000 => $DBD::SQLeet::sqlite_version_number >= 3010000 ? 1 : 0;
  3         4  
  3         217  
9 3 50   3   16 use constant SQLITE_3021000 => $DBD::SQLeet::sqlite_version_number >= 3021000 ? 1 : 0;
  3         4  
  3         626  
10              
11             # private data for translating comparison operators from Sqlite to Perl
12             my $TXT = 0;
13             my $NUM = 1;
14             my %SQLOP2PERLOP = (
15             # TXT NUM
16             '=' => [ 'eq', '==' ],
17             '<' => [ 'lt', '<' ],
18             '<=' => [ 'le', '<=' ],
19             '>' => [ 'gt', '>' ],
20             '>=' => [ 'ge', '>=' ],
21             'MATCH' => [ '=~', '=~' ],
22             (SQLITE_3010000 ? (
23             'LIKE' => [ 'DBD::SQLeet::strlike', 'DBD::SQLeet::strlike' ],
24             'GLOB' => [ 'DBD::SQLeet::strglob', 'DBD::SQLeet::strglob' ],
25             'REGEXP'=> [ '=~', '=~' ],
26             ) : ()),
27             (SQLITE_3021000 ? (
28             'NE' => [ 'ne', '!=' ],
29             'ISNOT' => [ 'defined', 'defined' ],
30             'ISNOTNULL' => [ 'defined', 'defined' ],
31             'ISNULL' => [ '!defined', '!defined' ],
32             'IS' => [ '!defined', '!defined' ],
33             ) : ()),
34             );
35              
36             #----------------------------------------------------------------------
37             # instanciation methods
38             #----------------------------------------------------------------------
39              
40             sub NEW {
41 6     6 1 11 my $class = shift;
42 6         22 my $self = $class->_PREPARE_SELF(@_);
43              
44             # verifications
45 6         9 my $n_cols = @{$self->{columns}};
  6         12  
46 6 50       18 $n_cols > 0
47             or die "$class: no declared columns";
48 6 50 66     24 !$self->{options}{colref} || $n_cols == 1
49             or die "$class: must have exactly 1 column when using 'colref'";
50             my $symbolic_ref = $self->{options}{arrayrefs}
51             || $self->{options}{hashrefs}
52             || $self->{options}{colref}
53 6 50 66     38 or die "$class: missing option 'arrayrefs' or 'hashrefs' or 'colref'";
54              
55             # bind to the Perl variable
56 3     3   25 no strict "refs";
  3         5  
  3         2600  
57 6 50       10 defined ${$symbolic_ref}
  6         24  
58             or die "$class: can't find global variable \$$symbolic_ref";
59 6         10 $self->{rows} = \ ${$symbolic_ref};
  6         27  
60              
61 6         38 bless $self, $class;
62             }
63              
64             sub _build_headers_optypes {
65 6     6   12 my $self = shift;
66              
67 6         29 my $cols = $self->sqlite_table_info;
68              
69             # headers : names of columns, without type information
70 6         808 $self->{headers} = [ map {$_->{name}} @$cols ];
  16         48  
71              
72             # optypes : either $NUM or $TEXT for each column
73             # (applying algorithm from datatype3.html" for type affinity)
74             $self->{optypes}
75 6 100       14 = [ map {$_->{type} =~ /INT|REAL|FLOA|DOUB/i ? $NUM : $TXT} @$cols ];
  16         76  
76             }
77              
78             #----------------------------------------------------------------------
79             # method for initiating a search
80             #----------------------------------------------------------------------
81              
82             sub BEST_INDEX {
83 47     47 1 146 my ($self, $constraints, $order_by) = @_;
84              
85 47 100       161 $self->_build_headers_optypes if !$self->{headers};
86              
87             # for each constraint, build a Perl code fragment. Those will be gathered
88             # in FILTER() for deciding which rows match the constraints.
89 47         69 my @conditions;
90 47         69 my $ix = 0;
91 47         87 foreach my $constraint (grep {$_->{usable}} @$constraints) {
  47         156  
92 47         77 my $col = $constraint->{col};
93 47         64 my ($member, $optype);
94              
95             # build a Perl code fragment. Those fragments will be gathered
96             # and eval-ed in FILTER(), for deciding which rows match the constraints.
97 47 50       90 if ($col == -1) {
98             # constraint on rowid
99 0         0 $member = '$i';
100 0         0 $optype = $NUM;
101             }
102             else {
103             # constraint on regular column
104 47         69 my $opts = $self->{options};
105             $member = $opts->{arrayrefs} ? "\$row->[$col]"
106             : $opts->{hashrefs} ? "\$row->{$self->{headers}[$col]}"
107 47 50       142 : $opts->{colref} ? "\$row"
    100          
    100          
108             : die "corrupted data in ->{options}";
109 47         86 $optype = $self->{optypes}[$col];
110             }
111 47         132 my $op = $SQLOP2PERLOP{$constraint->{op}}[$optype];
112 47 100       135 if (SQLITE_3021000 && $op =~ /defined/) {
    100          
113 6 100       20 if ($constraint->{op} =~ /NULL/) {
114 4         11 push @conditions,
115             "($op($member))";
116             } else {
117 2         9 push @conditions,
118             "($op($member) && !defined(\$vals[$ix]))";
119             }
120             } elsif (SQLITE_3010000 && $op =~ /str/) {
121 1         11 push @conditions,
122             "(defined($member) && defined(\$vals[$ix]) && !$op(\$vals[$ix], $member))";
123             } else {
124 40         120 push @conditions,
125             "(defined($member) && defined(\$vals[$ix]) && $member $op \$vals[$ix])";
126             }
127             # Note : $vals[$ix] refers to an array of values passed to the
128             # FILTER method (see below); so the eval-ed perl code will be a
129             # closure on those values
130             # info passed back to the SQLite core -- see vtab.html in sqlite doc
131 47         102 $constraint->{argvIndex} = $ix++;
132 47         103 $constraint->{omit} = 1;
133             }
134              
135             # further info for the SQLite core
136 47   100     247 my $outputs = {
137             idxNum => 1,
138             idxStr => (join(" && ", @conditions) || "1"),
139             orderByConsumed => 0,
140             estimatedCost => 1.0,
141             estimatedRows => undef,
142             };
143              
144 47         1187 return $outputs;
145             }
146              
147              
148             #----------------------------------------------------------------------
149             # methods for data update
150             #----------------------------------------------------------------------
151              
152             sub _build_new_row {
153 5     5   8 my ($self, $values) = @_;
154              
155 5         10 my $opts = $self->{options};
156             return $opts->{arrayrefs} ? $values
157 0         0 : $opts->{hashrefs} ? { map {$self->{headers}->[$_], $values->[$_]}
158 0         0 (0 .. @{$self->{headers}} - 1) }
159 5 50       19 : $opts->{colref} ? $values->[0]
    50          
    100          
160             : die "corrupted data in ->{options}";
161             }
162              
163             sub INSERT {
164 5     5 1 9 my ($self, $new_rowid, @values) = @_;
165              
166 5         11 my $new_row = $self->_build_new_row(\@values);
167              
168 5 50       9 if (defined $new_rowid) {
169 0 0       0 not ${$self->{rows}}->[$new_rowid]
  0         0  
170             or die "can't INSERT : rowid $new_rowid already in use";
171 0         0 ${$self->{rows}}->[$new_rowid] = $new_row;
  0         0  
172             }
173             else {
174 5         6 push @${$self->{rows}}, $new_row;
  5         10  
175 5         7 return $#${$self->{rows}};
  5         27  
176             }
177             }
178              
179             sub DELETE {
180 0     0   0 my ($self, $old_rowid) = @_;
181              
182 0         0 delete ${$self->{rows}}->[$old_rowid];
  0         0  
183             }
184              
185             sub UPDATE {
186 0     0 1 0 my ($self, $old_rowid, $new_rowid, @values) = @_;
187              
188 0         0 my $new_row = $self->_build_new_row(\@values);
189              
190 0 0       0 if ($new_rowid == $old_rowid) {
191 0         0 ${$self->{rows}}->[$old_rowid] = $new_row;
  0         0  
192             }
193             else {
194 0         0 delete ${$self->{rows}}->[$old_rowid];
  0         0  
195 0         0 ${$self->{rows}}->[$new_rowid] = $new_row;
  0         0  
196             }
197             }
198              
199              
200             #======================================================================
201             package DBD::SQLeet::VirtualTable::PerlData::Cursor;
202             #======================================================================
203 3     3   19 use strict;
  3         12  
  3         51  
204 3     3   13 use warnings;
  3         3  
  3         102  
205 3     3   13 use base "DBD::SQLeet::VirtualTable::Cursor";
  3         5  
  3         1043  
206              
207              
208             sub row {
209 1279     1279   1548 my ($self, $i) = @_;
210 1279         1333 return ${$self->{vtable}{rows}}->[$i];
  1279         16846  
211             }
212              
213             sub FILTER {
214 56     56   155 my ($self, $idxNum, $idxStr, @vals) = @_;
215              
216             # build a method coderef to fetch matching rows
217 56         122 my $perl_code = 'sub {my ($self, $i) = @_; my $row = $self->row($i); '
218             . $idxStr
219             . '}';
220              
221             # print STDERR "PERL CODE:\n", $perl_code, "\n";
222              
223 3 50   3   19 $self->{is_wanted_row} = do { no warnings; eval $perl_code }
  3         5  
  3         909  
  56         65  
  56         5277  
224             or die "couldn't eval q{$perl_code} : $@";
225              
226             # position the cursor to the first matching row (or to eof)
227 56         126 $self->{row_ix} = -1;
228 56         132 $self->NEXT;
229             }
230              
231              
232             sub EOF {
233 1235     1235   1691 my ($self) = @_;
234              
235 1235         1405 return $self->{row_ix} > $#${$self->{vtable}{rows}};
  1235         4055  
236             }
237              
238             sub NEXT {
239 157     157   1440 my ($self) = @_;
240              
241             do {
242 1078         2221 $self->{row_ix} += 1
243             } until $self->EOF
244 157   100     191 || eval {$self->{is_wanted_row}->($self, $self->{row_ix})};
  1022         17014  
245              
246             # NOTE: the eval above is required for cases when user data, injected
247             # into Perl comparison operators, generates errors; for example
248             # WHERE col MATCH '(foo' will die because the regex is not well formed
249             # (no matching parenthesis). In such cases no row is selected and the
250             # query just returns an empty list.
251             }
252              
253              
254             sub COLUMN {
255 257     257   410 my ($self, $idxCol) = @_;
256              
257 257         359 my $row = $self->row($self->{row_ix});
258              
259 257         318 my $opts = $self->{vtable}{options};
260             return $opts->{arrayrefs} ? $row->[$idxCol]
261             : $opts->{hashrefs} ? $row->{$self->{vtable}{headers}[$idxCol]}
262 257 50       1210 : $opts->{colref} ? $row
    100          
    100          
263             : die "corrupted data in ->{options}";
264             }
265              
266             sub ROWID {
267 8     8   14 my ($self) = @_;
268              
269 8         29 return $self->{row_ix} + 1; # rowids start at 1 in SQLite
270             }
271              
272              
273             1;
274              
275             __END__