| 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__ |