File Coverage

blib/lib/Algorithm/SpatialIndex/Storage/DBI.pm
Criterion Covered Total %
statement 27 281 9.6
branch 0 48 0.0
condition 0 8 0.0
subroutine 9 26 34.6
pod 9 9 100.0
total 45 372 12.1


line stmt bran cond sub pod time code
1             package Algorithm::SpatialIndex::Storage::DBI;
2 4     4   5730 use 5.008001;
  4         12  
  4         141  
3 4     4   21 use strict;
  4         5  
  4         117  
4 4     4   19 use warnings;
  4         13  
  4         99  
5 4     4   19 use Carp qw(croak);
  4         8  
  4         268  
6              
7             our $VERSION = '0.02';
8              
9 4     4   19 use parent 'Algorithm::SpatialIndex::Storage';
  4         9  
  4         21  
10 4     4   218 use constant DEBUG => 0;
  4         6  
  4         353  
11              
12             =head1 NAME
13              
14             Algorithm::SpatialIndex::Storage::DBI - DBI storage backend
15              
16             =head1 SYNOPSIS
17              
18             use Algorithm::SpatialIndex;
19             my $dbh = ...;
20             my $idx = Algorithm::SpatialIndex->new(
21             storage => 'DBI',
22             dbh_rw => $dbh,
23             dbh_ro => $dbh, # defaults to dbh_rw
24             table_prefix => 'si_',
25             );
26              
27             =head1 DESCRIPTION
28              
29             B
30             EXPERIMENTAL AND IN A PROOF-OF-CONCEPT STATE.> Unsurprisingly, it is also
31             20x slower when using SQLite as the storage engine then when using the
32             memory storage backend. Has only been tested with SQLite but has
33             mysql-specific and SQLite specific code paths as well as a general
34             SQL code path which is less careful about race conditions.
35              
36             Inherits from L.
37              
38             This storage backend is persistent.
39              
40             No implementation of schema migration yet, so expect to have to
41             reinitialize the index after a module upgrade!
42              
43             =head1 ACCESSORS
44              
45             =cut
46              
47              
48 4     4   19 use constant NODE_ID_TYPE => 'INTEGER';
  4         13  
  4         150  
49 4     4   17 use constant ITEM_ID_TYPE => 'INTEGER';
  4         9  
  4         274  
50              
51             use Class::XSAccessor {
52 4         50 getters => [qw(
53             dbh_rw
54             table_prefix
55              
56             no_of_coords
57             coord_types
58             node_coord_create_sql
59             node_coord_select_sql
60             node_coord_insert_sql
61              
62             no_of_subnodes
63             subnodes_create_sql
64             subnodes_select_sql
65             subnodes_insert_sql
66              
67             bucket_size
68             item_coord_types
69              
70             config
71              
72             dbms_name
73             is_mysql
74             is_sqlite
75              
76             )],
77 4     4   31 };
  4         8  
78              
79             =head2 table_prefix
80              
81             Returns the prefix of the table names.
82              
83             =head2 coord_types
84              
85             Returns an array reference containing the coordinate type strings.
86              
87             =head2 item_coord_types
88              
89             Returns an array reference containing the item coordinate type strings.
90              
91             =head2 node_coord_create_sql
92              
93             Returns the precomputed SQL fragment of the node coordinate
94             columns (C syntax).
95              
96             =head2 no_of_subnodes
97              
98             Returns the no. of subnodes per node.
99              
100             =head2 subnodes_create_sql
101              
102             Returns the precomputed SQL fragment of the subnode id
103             columns (C syntax).
104              
105             =head2 config
106              
107             Returns the hash reference of configuration options
108             read from the config table.
109              
110             =head2 dbh_rw
111              
112             Returns the read/write database handle.
113              
114             =head2 dbh_ro
115              
116             Returns the read-only database handle. Falls back
117             to the read/write handle if not defined.
118              
119             =cut
120              
121             sub dbh_ro {
122 0     0 1   my $self = shift;
123 0 0         if (defined $self->{dbh_ro}) {
124 0           return $self->{dbh_ro};
125             }
126 0           return $self->{dbh_rw};
127             }
128              
129             =head1 OTHER METHODS
130              
131             =head2 init
132              
133             Reads the options from the database for previously existing indexes.
134             Creates tables and writes default configuration for those that didn't
135             exist before.
136              
137             Doesn't do any schema migration at this point.
138              
139             =cut
140              
141             sub init {
142 0     0 1   my $self = shift;
143              
144 0           my $opt = $self->{opt};
145 0           $self->{dbh_rw} = $opt->{dbh_rw};
146 0           $self->{dbh_ro} = $opt->{dbh_ro};
147 0 0         my $table_prefix = defined($opt->{table_prefix})
148             ? $opt->{table_prefix} : 'spatialindex';
149 0           $self->{table_prefix} = $table_prefix;
150              
151             # Dear SQL. Please go away. Thank you.
152 0 0         $self->{dbms_name} = $self->dbh_ro->get_info(17) if not defined $self->{dbms_name};
153 0           $self->{is_mysql} = 0;
154 0           $self->{is_sqlite} = 0;
155              
156 0           my $option_table_name = $table_prefix . '_options';
157 0           my $node_table_name = $table_prefix . '_nodes';
158              
159 0 0         if ($self->{dbms_name} =~ /mysql/i) {
    0          
160 0           $self->{is_mysql} = 1;
161 0           $self->{_write_config_sql} = [
162             qq{
163             INSERT INTO $option_table_name
164             SET id=?, value=?
165             ON DUPLICATE KEY UPDATE id=?, value=?
166             }, 0, 1, 0, 1
167             ];
168             }
169             elsif ($self->{dbms_name} =~ /sqlite/i) {
170 0           $self->{is_sqlite} = 1;
171 0           $self->{_write_config_sql} = [qq{INSERT OR REPLACE INTO $option_table_name (id, value) VALUES(?, ?)}, 0, 1 ];
172             }
173             else {
174             $self->{_write_config_sql} = sub {
175 0     0     my $dbh = shift;
176 0           eval {
177 0           $dbh->do(qq{INSERT INTO $option_table_name (id, value) VALUES(?, ?)}, {}, $_[0], $_[1]);
178 0           $dbh->do(qq{UPDATE $option_table_name SET id=?, value=?}, {}, $_[0], $_[1]);
179 0           1;
180             };
181 0           };
182             }
183              
184 0           my $config_existed = $self->_read_config_table;
185 0           $self->{no_of_coords} = scalar(@{$self->coord_types});
  0            
186 0           $self->_coord_types_to_sql($self->coord_types);
187 0           $self->_subnodes_sql($self->no_of_subnodes);
188 0           $self->{_fetch_node_sql} = qq(SELECT id, $self->{node_coord_select_sql}, $self->{subnodes_select_sql} FROM ${table_prefix}_nodes WHERE id=?);
189 0           my $qlist = '?,' x ($self->no_of_subnodes + @{$self->coord_types});
  0            
190 0           $qlist =~ s/,$//;
191 0           $self->{_write_new_node_sql} = qq{INSERT INTO $node_table_name (}
192             . $self->node_coord_select_sql . ', '
193             . $self->subnodes_select_sql
194             . qq{) VALUES($qlist)};
195 0           $self->{_write_node_sql} = qq{UPDATE $node_table_name SET id=?, }
196             . $self->node_coord_insert_sql . ', '
197             . $self->subnodes_insert_sql
198             . ' WHERE id=?';
199 0           $self->_bucket_sql; # init sql for bucket operations
200              
201 0           $self->_init_tables();
202 0 0         $self->_write_config() if not $config_existed;
203             }
204              
205             =head2 _read_config_table
206              
207             Reads the configuration table.
208             Returns whether this succeeded or not.
209             In case of failure, this initializes some of the
210             configuration options from other sources.
211              
212             =cut
213              
214             sub _read_config_table {
215 0     0     my $self = shift;
216 0           my $dbh = $self->dbh_ro;
217 0           my $table_prefix = $self->table_prefix;
218              
219 0           my $find_sth = $dbh->table_info('%', '%', "${table_prefix}_options", 'TABLE');
220 0           my $opt;
221             my $success;
222 0 0         if ($find_sth->fetchrow_arrayref()) {
223 0           my $sql = qq#
224             SELECT id, value
225             FROM ${table_prefix}_options
226             #;
227 0           $success = eval {
228 0           $opt = $dbh->selectall_hashref($sql, 'id');
229 0           my $err = $dbh->errstr;
230 0 0         die $err if $err;
231 0           1;
232             };
233             }
234 0   0       $opt ||= {};
235 0           $opt->{$_} = $opt->{$_}{value} for keys %$opt;
236 0           $self->{config} = $opt;
237              
238 0 0         if (defined $opt->{coord_types}) {
239 0           $self->{coord_types} = [split / /, $opt->{coord_types}];
240             }
241             else {
242 0           $self->{coord_types} = [$self->index->strategy->coord_types];
243 0           $opt->{coord_types} = join ' ', @{$self->{coord_types}};
  0            
244             }
245              
246 0 0         if (defined $opt->{item_coord_types}) {
247 0           $self->{item_coord_types} = [split / /, $opt->{item_coord_types}];
248             }
249             else {
250 0           $self->{item_coord_types} = [$self->index->strategy->item_coord_types];
251 0           $opt->{item_coord_types} = join ' ', @{$self->{item_coord_types}};
  0            
252             }
253              
254 0   0       $opt->{no_of_subnodes} ||= $self->index->strategy->no_of_subnodes;
255 0           $self->{no_of_subnodes} = $opt->{no_of_subnodes};
256              
257 0   0       $opt->{bucket_size} ||= $self->index->strategy->bucket_size;
258 0           $self->{bucket_size} = $opt->{bucket_size};
259              
260 0           return $success;
261             }
262              
263             =head2 _init_tables
264              
265             Creates the index's tables.
266              
267             =cut
268              
269             sub _init_tables {
270 0     0     my $self = shift;
271              
272 0           my $dbh = $self->dbh_rw;
273              
274 0           my $table_prefix = $self->table_prefix;
275 0           my $sql_opt = qq(
276             CREATE TABLE IF NOT EXISTS ${table_prefix}_options (
277             id VARCHAR(255) PRIMARY KEY,
278             value VARCHAR(1023)
279             )
280             );
281 0           warn $sql_opt if DEBUG;
282 0           $dbh->do($sql_opt);
283              
284 0           my $node_id_type = NODE_ID_TYPE;
285 0           my $coord_sql = $self->node_coord_create_sql;
286 0           my $subnodes_sql = $self->subnodes_create_sql;
287 0           my $sql = qq(
288             CREATE TABLE IF NOT EXISTS ${table_prefix}_nodes (
289             id $node_id_type PRIMARY KEY AUTOINCREMENT,
290             $coord_sql,
291             $subnodes_sql
292             )
293             );
294 0           warn $sql if DEBUG;
295 0           $dbh->do($sql);
296              
297 0           my $bsql = $self->{buckets_create_sql};
298 0           warn $bsql if DEBUG;
299 0           $dbh->do($bsql);
300             }
301              
302             =head2 _write_config
303              
304             Writes the index's configuration to the
305             configuration table.
306              
307             =cut
308              
309             sub _write_config {
310 0     0     my $self = shift;
311 0           my $dbh = $self->dbh_rw;
312              
313 0           my $table_prefix = $self->table_prefix;
314              
315 0           my $sql_struct = $self->{_write_config_sql};
316 0           my $is_sub = ref($sql_struct) eq 'CODE';
317 0           my $sth;
318 0 0         $sth = $dbh->prepare_cached($sql_struct->[0]) if not $is_sub;
319              
320 0           my $success = eval {
321 0           foreach my $key (keys %{$self->{config}}) {
  0            
322 0 0         if ($is_sub) {
323 0           $sql_struct->($key, $self->{config}{$key});
324             } else {
325 0           my $d = [$key, $self->{config}{$key}];
326 0           $sth->execute(map $d->[$_], @{$sql_struct}[1..$#$sql_struct]);
  0            
327 0 0         my $err = $sth->errstr; die $err if $err;
  0            
328             }
329             }
330 0           1;
331             };
332 0           $sth->finish;
333             }
334              
335             sub fetch_node {
336 0     0 1   my $self = shift;
337 0           my $index = shift;
338 0           my $dbh = $self->dbh_ro;
339 0           my $str = $self->{_fetch_node_sql};
340 0           my $sth = $dbh->prepare_cached($str);
341 0           $sth->execute($index);
342 0           my $struct = $sth->fetchrow_arrayref;
343 0           $sth->finish;
344 0 0         return if not defined $struct;
345 0           my $coords = $self->no_of_coords;
346 0           my $snodes = [@{$struct}[1+$coords..$coords+$self->no_of_subnodes]];
  0            
347 0 0         $snodes = [] if not defined $snodes->[0];
348 0           my $node = Algorithm::SpatialIndex::Node->new(
349             id => $struct->[0],
350 0           coords => [@{$struct}[1..$coords]],
351             subnode_ids => $snodes,
352             );
353             #use Data::Dumper; warn "FETCH: " . Dumper($node);
354 0           return $node;
355             }
356              
357             sub store_node {
358 0     0 1   my $self = shift;
359 0           my $node = shift;
360             #use Data::Dumper;
361             #use Data::Dumper; warn "STORE: " . Dumper($node);
362 0           my $id = $node->id;
363 0           my $dbh = $self->dbh_rw;
364 0           my $tname = $self->table_prefix . '_nodes';
365 0           my $sth;
366 0 0         if (not defined $id) {
367 0           $sth = $dbh->prepare_cached($self->{_write_new_node_sql});
368 0           my $coords = $node->coords;
369 0           my $snids = $node->subnode_ids;
370 0           my @args = (
371             @$coords,
372             ((undef) x ($self->no_of_coords - @$coords)),
373             @$snids,
374             ((undef) x ($self->no_of_subnodes - @$snids))
375             );
376 0           $sth->execute(@args);
377 0           $id = $dbh->last_insert_id('', '', '', ''); # FIXME NOT PORTABLE LIKE THAT
378 0           $node->id($id);
379             }
380             else {
381 0           $sth = $dbh->prepare_cached($self->{_write_node_sql});
382 0           $sth->execute($id, @{$node->coords}, @{$node->subnode_ids}, $id);
  0            
  0            
383             }
384 0           $sth->finish();
385 0           return $id;
386             }
387              
388             sub get_option {
389 0     0 1   my $self = shift;
390 0           return $self->{config}->{shift()}; # We assume this data changes RARELY
391             }
392              
393             sub set_option {
394 0     0 1   my $self = shift;
395 0           my $key = shift;
396 0           my $value = shift;
397              
398 0           $self->{config}->{$key} = $value;
399 0           $self->_write_config(); # FIXME wasteful
400             }
401              
402             sub store_bucket {
403 0     0 1   my $self = shift;
404 0           my $bucket = shift;
405 0           my $dbh = $self->dbh_rw;
406 0           my $id = $bucket->node_id;
407 0           my $sql = $self->{buckets_insert_sql};
408 0           my $is_sub = ref($sql) eq 'CODE';
409 0 0         if (!$is_sub) {
410 0           my $sth = $dbh->prepare_cached($sql->[0]);
411 0           my $d = [$id, map {@$_} @{$bucket->items}];
  0            
  0            
412 0           $sth->execute(map $d->[$_], @{$sql}[1..$#$sql]);
  0            
413 0 0         my $err = $sth->errstr; die $err if $err;
  0            
414 0           $sth->finish;
415             }
416             else {
417 0           $sql->($id, map {@$_} @{$bucket->items});
  0            
  0            
418             }
419             }
420              
421             sub fetch_bucket {
422 0     0 1   my $self = shift;
423 0           my $node_id = shift;
424 0           my $dbh = $self->dbh_ro;
425 0           my $selsql = $self->{buckets_select_sql};
426             # This throws SEGV in the driver
427             #my $sth = $dbh->prepare_cached($selsql);
428             #$sth->execute($node_id) or die $dbh->errstr;
429             #my $row = $sth->fetchrow_arrayref;
430             #$sth->finish;
431 0           my $rows = $dbh->selectall_arrayref($selsql, {}, $node_id);
432 0           my $row = $rows->[0];
433 0 0         return undef if not defined $row;
434 0           my $items = [];
435 0           my $n = scalar(@{$self->item_coord_types}) + 1;
  0            
436 0           while (@$row > 1) {
437 0           my $item = [splice(@$row, 1, $n)];
438 0 0         next if not defined $item->[0];
439 0           push @$items, $item;
440             }
441 0           my $bucket = $self->bucket_class->new(node_id => $node_id, items => $items);
442 0           return $bucket;
443             }
444              
445             sub delete_bucket {
446 0     0 1   my $self = shift;
447 0           my $node_id = shift;
448 0 0         $node_id = $node_id->node_id if ref($node_id);
449 0           my $tname = $self->table_prefix . '_buckets';
450 0           $self->dbh_rw->do(qq{DELETE FROM $tname WHERE node_id=?}, {}, $node_id);
451 0           return();
452             }
453              
454              
455             =head2 _coord_types_to_sql
456              
457             Given an array ref containing coordinate type strings
458             (cf. L),
459             stores the SQL fragments for C
460             and C for the node coordinates.
461              
462             The coordinates will be called C where C<$i>
463             starts at 0.
464              
465             =cut
466              
467             sub _coord_types_to_sql {
468 0     0     my $self = shift;
469 0           my $types = shift;
470              
471 0           my %types = (
472             float => 'FLOAT',
473             double => 'DOUBLE',
474             integer => 'INTEGER',
475             unsigned => 'INTEGER UNSIGNED',
476             );
477 0           my $create_sql = '';
478 0           my $select_sql = '';
479 0           my $insert_sql = '';
480 0           my $i = 0;
481 0           foreach my $type (@$types) {
482 0           my $sql_type = $types{lc($type)};
483 0 0         die "Invalid coord type '$type'" if not defined $sql_type;
484 0           $create_sql .= " c$i $sql_type, ";
485 0           $select_sql .= " c$i, ";
486 0           $insert_sql .= " c$i=?, ";
487 0           $i++;
488             }
489 0           $create_sql =~ s/, \z//;
490 0           $select_sql =~ s/, \z//;
491 0           $insert_sql =~ s/, \z//;
492 0           $self->{node_coord_create_sql} = $create_sql;
493 0           $self->{node_coord_select_sql} = $select_sql;
494 0           $self->{node_coord_insert_sql} = $insert_sql;
495             }
496              
497             =head2 _subnodes_sql
498              
499             Given the number of subnodes per node,
500             creates a string of column specifications
501             for interpolation into a C
502             and one for interpolation into a C
503             Saves those strings into the object.
504              
505             The columns are named C with C<$i>
506             starting at 0.
507              
508             =cut
509              
510             sub _subnodes_sql {
511 0     0     my $self = shift;
512 0           my $no_subnodes = shift;
513 0           my $create_sql = '';
514 0           my $select_sql = '';
515 0           my $insert_sql = '';
516 0           my $i = 0;
517 0           my $node_id_type = NODE_ID_TYPE;
518 0           foreach my $i (0..$no_subnodes-1) {
519 0           $create_sql .= " sn$i $node_id_type, ";
520 0           $select_sql .= " sn$i, ";
521 0           $insert_sql .= " sn$i=?, ";
522 0           $i++;
523             }
524 0           $create_sql =~ s/, \z//;
525 0           $select_sql =~ s/, \z//;
526 0           $insert_sql =~ s/, \z//;
527 0           $self->{subnodes_create_sql} = $create_sql;
528 0           $self->{subnodes_select_sql} = $select_sql;
529 0           $self->{subnodes_insert_sql} = $insert_sql;
530             }
531              
532             sub _bucket_sql {
533 0     0     my $self = shift;
534 0           my $bsize = $self->bucket_size;
535 0           my $tname = $self->table_prefix . '_buckets';
536              
537 0           my %types = (
538             float => 'FLOAT',
539             double => 'DOUBLE',
540             integer => 'INTEGER',
541             unsigned => 'INTEGER UNSIGNED',
542             );
543 0           my $item_coord_types = [map $types{$_}, @{$self->item_coord_types}];
  0            
544              
545             # i0 INTEGER, i0c0 DOUBLE, i0c1 DOUBLE, ...
546 0           $self->{buckets_create_sql} = qq{CREATE TABLE IF NOT EXISTS $tname ( node_id INTEGER PRIMARY KEY, }
547             . join(
548             ', ',
549             map {
550 0           my $i = $_;
551 0           my $c = 0;
552 0           ("i$i INTEGER", map "i${i}c".$c++." $_", @$item_coord_types)
553             } 0..$bsize-1
554             )
555             . ')';
556 0           $self->{buckets_select_sql} = qq{SELECT * FROM $tname WHERE node_id=?};
557              
558 0           my $insert_id_list = join(
559             ', ',
560             map {
561 0           my $i = $_;
562 0           "i$i", map "i${i}c$_", 0..$#$item_coord_types
563             } 0..$bsize-1
564             );
565 0           my $nentries = 1 + $bsize * (1+@$item_coord_types);
566             #my $idlist = join(', ', map "i$_" 0..$bsize-1);
567 0           my $qlist = '?,' x $nentries;
568 0           $qlist =~ s/,$//;
569 0 0         if ($self->is_mysql) {
    0          
570 0           $self->{buckets_insert_sql} = [
571             qq{
572             INSERT INTO $tname
573             VALUES ($qlist)
574             ON DUPLICATE KEY UPDATE $insert_id_list
575             }, 0..$nentries-1
576             ];
577             }
578             elsif ($self->is_sqlite) {
579 0           $self->{buckets_insert_sql} = [qq{INSERT OR REPLACE INTO $tname VALUES($qlist)}, 0..$nentries-1 ];
580             }
581             else {
582 0           my $insert_sql = qq{INSERT INTO $tname VALUES(?, $qlist)};
583 0           my $update_sql = qq{UPDATE $tname SET id=?, $insert_id_list};
584             $self->{buckets_insert_sql} = sub {
585 0     0     my $dbh = shift;
586 0           eval {
587 0           $dbh->do($insert_sql, {}, @_, (undef) x ($nentries-@_));
588 0           $dbh->do($update_sql, {}, @_, (undef) x ($nentries-@_));
589 0           1;
590             };
591 0           };
592             }
593             #use Data::Dumper;
594             #warn Dumper $self->{buckets_insert_sql};
595             }
596              
597             1;
598             __END__