File Coverage

lib/UR/DataSource/SQLite.pm
Criterion Covered Total %
statement 405 520 77.8
branch 125 238 52.5
condition 36 71 50.7
subroutine 51 57 89.4
pod 1 20 5.0
total 618 906 68.2


line stmt bran cond sub pod time code
1             package UR::DataSource::SQLite;
2 127     127   3159 use strict;
  127         173  
  127         3498  
3 127     127   442 use warnings;
  127         183  
  127         3338  
4              
5 127     127   52281 use IO::Dir;
  127         938724  
  127         5036  
6 127     127   717 use File::Spec;
  127         184  
  127         470  
7 127     127   2235 use File::Basename;
  127         165  
  127         6008  
8 127     127   50293 use version;
  127         179969  
  127         635  
9              
10             =pod
11              
12             =head1 NAME
13              
14             UR::DataSource::SQLite - base class for datasources using the SQLite3 RDBMS
15              
16             =head1 SYNOPSIS
17              
18             In the shell:
19              
20             ur define datasource sqlite
21              
22             Or write the singleton to represent the source directly:
23              
24             class Acme::DataSource::MyDB1 {
25             is => 'UR::DataSource::SQLite',
26             has_constant => [
27             server => '/var/lib/acme-app/mydb1.sqlitedb'
28             ]
29             };
30              
31             You may also use a directory containing *.sqlite3 files. The primary database
32             must be named main.sqlite3. All the other *.sqlite3 files are attached when
33             the database is opened.
34              
35             class Acme::DataSource::MyDB2 {
36             is => 'UR::DataSource::SQLite',
37             has_constant => [
38             server => '/path/to/directory/'
39             ]
40             };
41              
42             =cut
43              
44             require UR;
45             our $VERSION = "0.46"; # UR $VERSION;
46              
47             UR::Object::Type->define(
48             class_name => 'UR::DataSource::SQLite',
49             is => ['UR::DataSource::RDBMS'],
50             is_abstract => 1,
51             );
52              
53             # RDBMS API
54              
55 178     178 0 485 sub driver { "SQLite" }
56              
57             sub default_owner {
58 349     349 0 2564 return 'main';
59             }
60              
61 348     348 0 776 sub owner { default_owner() }
62              
63             sub login {
64             undef
65 206     206 0 491 }
66              
67             sub auth {
68             undef
69 203     203 0 394 }
70              
71             sub create_default_handle {
72 175     175 0 3199 my $self = shift->_singleton_object();
73              
74 175         1453 $self->_init_database;
75 175 100       946 if ($self->_db_path_specifies_a_directory($self->server)) {
76 2         14 return $self->_create_default_handle_from_directory();
77             } else {
78 173         1576 return $self->SUPER::create_default_handle(@_);
79             }
80             }
81              
82             sub _create_default_handle_from_directory {
83 2     2   3 my $self = shift;
84              
85 2         5 my $server_directory = $self->server;
86 2         6 my $ext = $self->_extension_for_db;
87 2         44 my $main_schema_file = File::Spec->catfile($server_directory, "main${ext}");
88 2 50 33     33 -f $main_schema_file
89             || UR::Util::touch_file($main_schema_file)
90             || die "Could not create main schema file $main_schema_file: $!";
91              
92 2         6 my $server_sub_name = join('::', ref($self), 'server');
93              
94 2         3 my $dbh = do {
95 127     127   32815 no strict 'refs';
  127         196  
  127         3196  
96 127     127   441 no warnings 'redefine';
  127         228  
  127         396944  
97 2     2   22 local *$server_sub_name = sub { $main_schema_file };
  2         3  
98              
99 2         19 $self->SUPER::create_default_handle();
100             };
101              
102 2         20 $self->_attach_all_schema_files_in_directory($dbh, $server_directory);
103 2         1494 return $dbh;
104             }
105              
106             sub _attach_all_schema_files_in_directory {
107 2     2   5 my($self, $dbh, $server_directory) = @_;
108 2         11 my @schema_files = $self->_schema_files_in_directory($server_directory);
109              
110 2         147 local $dbh->{AutoCommit} = 1;
111              
112 2         37 my $main_db_file = join('', 'main', $self->_extension_for_db);
113 2         5 foreach my $file ( @schema_files ) {
114 4 100       8 next if $file eq $main_db_file;
115 2         7 my $schema = $self->_schema_from_schema_filename($file);
116              
117 2         15 my $pathname = File::Spec->catfile($server_directory, $file);
118 2 50       25 $dbh->do("ATTACH DATABASE '$pathname' as $schema")
119             || Carp::croak("Could not attach schema file $file: ".$dbh->errstr);
120             }
121             }
122              
123             sub _schema_files_in_directory {
124 2     2   3 my($self, $dir) = @_;
125              
126 2         25 my $dh = IO::Dir->new($dir);
127              
128 2         174 my @files;
129 2         11 while (my $name = $dh->read) {
130 8         167 my $pathname = File::Spec->catfile($dir, $name);
131 8 100       89 next unless -f $pathname;
132 4 50       19 push(@files, $name) if $self->_schema_from_schema_filename($name);
133             }
134 2         22 return @files;
135             }
136              
137             sub _schema_from_schema_filename {
138 6     6   8 my($self, $pathname) = @_;
139              
140 6         13 my($schema, $dir, $ext) = File::Basename::fileparse($pathname, $self->_extension_for_db);
141 6 50       30 return $ext ? $schema : undef;
142             }
143              
144             sub database_exists {
145 0     0 0 0 my $self = shift;
146 0 0       0 return 1 if -e $self->server;
147 0 0       0 return 1 if -e $self->_data_dump_path; # exists virtually, and will dynamicaly instantiate
148 0         0 return;
149             }
150              
151             sub create_database {
152 0     0 0 0 my $self = shift;
153 0 0       0 die "Database exists!" if $self->database_exists;
154 0         0 my $path = $self->server;
155 0 0       0 return 1 if IO::File->new(">$path");
156             }
157              
158 217     217 0 626 sub can_savepoint { 0;} # Dosen't support savepoints
159              
160             # SQLite API
161              
162             sub _schema_path {
163 290     290   79536 return shift->_database_file_path() . '-schema';
164             }
165              
166             sub _data_dump_path {
167 147     147   591 return shift->_database_file_path() . '-dump';
168             }
169              
170             # FIXME is there a way to make this an object parameter instead of a method
171             sub server {
172 579     579 0 10930 my $self = shift->_singleton_object();
173 579         2213 my $path = $self->__meta__->module_path;
174 579         2284 my $ext = $self->_extension_for_db;
175 579 50       3287 $path =~ s/\.pm$/$ext/ or Carp::croak("Odd module path $path. Expected something endining in '.pm'");
176              
177 579         22873 my $dir = File::Basename::dirname($path);
178 579         2648 return $path;
179             }
180             *_database_file_path = \&server;
181              
182              
183             sub _extension_for_db {
184 654     654   9769 '.sqlite3';
185             }
186              
187             sub _journal_file_path {
188 115     115   2402 my $self = shift->_singleton_object();
189 115         466 return $self->server . "-journal";
190             }
191              
192             sub _init_database {
193 176     176   2785 my $self = shift->_singleton_object();
194              
195 176         910 my $db_file = $self->server;
196 176         4377 my $dump_file = $self->_data_dump_path;
197 176         1430 my $schema_file = $self->_schema_path;
198              
199 176         4514 my $db_time = (stat($db_file))[9];
200 176         1830 my $dump_time = (stat($dump_file))[9];
201 176         1939 my $schema_time = (stat($schema_file))[9];
202              
203 176 50 33     1761 if ($schema_time && ((-e $db_file and $schema_time > $db_time) or (-e $dump_file and $schema_time > $dump_time))) {
      66        
204 0         0 $self->warning_message("Schema file is newer than the db file or the dump file. Replacing db_file $db_file.");
205 0         0 my $dbbak_file = $db_file . '-bak';
206 0         0 my $dumpbak_file = $dump_file . '-bak';
207 0 0       0 unlink $dbbak_file if -e $dbbak_file;
208 0 0       0 unlink $dumpbak_file if -e $dumpbak_file;
209 0 0       0 rename $db_file, $dbbak_file if -e $db_file;
210 0 0       0 rename $dump_file, $dumpbak_file if -e $dump_file;
211 0 0       0 if (-e $db_file) {
212 0         0 Carp::croak "Failed to move out-of-date file $db_file out of the way for reconstruction! $!";
213             }
214 0 0       0 if (-e $dump_file) {
215 0         0 Carp::croak "Failed to move out-of-date file $dump_file out of the way for reconstruction! $!";
216             }
217             }
218 176 100       1584 if (-e $db_file) {
219 137 50 66     725 if ($dump_time && ($db_time < $dump_time)) {
220 0         0 my $bak_file = $db_file . '-bak';
221 0         0 $self->warning_message("Dump file is newer than the db file. Replacing db_file $db_file.");
222 0 0       0 unlink $bak_file if -e $bak_file;
223 0         0 rename $db_file, $bak_file;
224 0 0       0 if (-e $db_file) {
225 0         0 Carp::croak "Failed to move out-of-date file $db_file out of the way for reconstruction! $!";
226             }
227             }
228             }
229              
230             # NOTE: don't make this an "else", since we might go into both branches because we delete the file above.
231 176 100       1447 unless (-e $db_file) {
232             # initialize a new database from the one in the base class
233             # should this be moved to connect time?
234              
235             # TODO: auto re-create things as needed based on timestamp
236              
237 39 50 0     425 if (-e $dump_file) {
    0          
    0          
238             # create from dump
239 39         613 $self->warning_message("Re-creating $db_file from $dump_file.");
240 39         334 $self->_load_db_from_dump_internal($dump_file);
241 39 50       937 unless (-e $db_file) {
242 0         0 Carp::croak("Failed to import $dump_file into $db_file!");
243             }
244             }
245             elsif ( (not -e $db_file) and (-e $schema_file) ) {
246             # create from schema
247 0         0 $self->warning_message("Re-creating $db_file from $schema_file.");
248 0         0 $self->_load_db_from_dump_internal($schema_file);
249 0 0       0 unless (-e $db_file) {
250 0         0 Carp::croak("Failed to import $dump_file into $db_file!");
251             }
252             }
253             elsif ($self->class ne __PACKAGE__) {
254             # copy from the parent class (disabled)
255 0         0 Carp::croak("No schema or dump file found for $db_file.\n Tried schema path $schema_file\n and dump path $dump_file\nIf you still have *sqlite3n* SQLite database files please rename them to *sqlite3*, without the 'n'");
256              
257 0         0 my $template_database_file = $self->SUPER::server();
258 0 0       0 unless (-e $template_database_file) {
259 0         0 Carp::croak("Missing template database file: $db_file! Cannot initialize database for " . $self->class);
260             }
261 0 0       0 unless(File::Copy::copy($template_database_file,$db_file)) {
262 0         0 Carp::croak("Error copying $db_file to $template_database_file to initialize database!");
263             }
264 0 0       0 unless(-e $db_file) {
265 0         0 Carp::croak("File $db_file not found after copy from $template_database_file. Cannot initialize database!");
266             }
267             }
268             else {
269 0         0 Carp::croak("No db file found, and no dump or schema file found from which to re-construct a db file!");
270             }
271             }
272 176         454 return 1;
273             }
274              
275             *_init_created_dbh = \&init_created_handle;
276             sub init_created_handle
277             {
278 172     172 0 354 my ($self, $dbh) = @_;
279 172 50       609 return unless defined $dbh;
280 172         1247 $dbh->{LongTruncOk} = 0;
281             # wait one minute busy timeout
282 172         1400 $dbh->func(1800000,'busy_timeout');
283 172         357 return $dbh;
284             }
285              
286             sub _ignore_table {
287 2     2   4 my $self = shift;
288 2         4 my $table_name = shift;
289 2 50       13 return 1 if $table_name =~ /^(sqlite|\$|URMETA)/;
290             }
291              
292              
293             sub _get_sequence_name_for_table_and_column {
294 24     24   726 my $self = shift->_singleton_object;
295 24         61 my ($table_name,$column_name) = @_;
296            
297 24         139 my $dbh = $self->get_default_handle();
298            
299             # See if the sequence generator "table" is already there
300 24         171 my $seq_table = sprintf('URMETA_%s_%s_seq', $table_name, $column_name);
301 24 100 66     587 unless ($self->{'_has_sequence_generator'}->{$seq_table} or
302 42         73 grep {$_ eq $seq_table} $self->get_table_names() ) {
303 22 50       311 unless ($dbh->do("CREATE TABLE IF NOT EXISTS $seq_table (next_value integer PRIMARY KEY AUTOINCREMENT)")) {
304 0         0 die "Failed to create sequence generator $seq_table: ".$dbh->errstr();
305             }
306             }
307 24         6433 $self->{'_has_sequence_generator'}->{$seq_table} = 1;
308              
309 24         117 return $seq_table;
310             }
311              
312             sub _get_next_value_from_sequence {
313 94     94   333 my($self,$sequence_name) = @_;
314              
315 94         295 my $dbh = $self->get_default_handle();
316              
317             # FIXME can we use a statement handle with a wildcard as the table name here?
318 94 100       593 unless ($dbh->do("INSERT into $sequence_name values(null)")) {
319 3         47 die "Failed to INSERT into $sequence_name during id autogeneration: " . $dbh->errstr;
320             }
321              
322 91         7481 my $new_id = $dbh->last_insert_id(undef,undef,$sequence_name,'next_value');
323 91 50       219 unless (defined $new_id) {
324 0         0 die "last_insert_id() returned undef during id autogeneration after insert into $sequence_name: " . $dbh->errstr;
325             }
326              
327 91 50       466 unless($dbh->do("DELETE from $sequence_name where next_value = $new_id")) {
328 0         0 die "DELETE from $sequence_name for next_value $new_id failed during id autogeneration";
329             }
330              
331 91         3131 return $new_id;
332             }
333              
334              
335             # Overriding this so we can force the schema to 'main' for older versions of SQLite
336             #
337             # NOTE: table_info (called by SUPER::get_table_details_from_data_dictionary) in older
338             # versions of DBD::SQLite does not return data for tables in other attached databases.
339             #
340             # This probably isn't an issue... Due to the limited number of people using older DBD::SQLite
341             # (of particular note is that OSX 10.5 and earlier use such an old version), interseted with
342             # the limited number of people using attached databases, it's probably not a problem.
343             # The commit_between_schemas test does do this. If it turns out it is a problem, we could
344             # appropriate the code from recent DBD::SQLite::table_info
345             sub get_table_details_from_data_dictionary {
346 29     29 0 51 my $self = shift;
347              
348 29         214 my $sth = $self->SUPER::get_table_details_from_data_dictionary(@_);
349 29         502 my $sqlite_version = version->parse($DBD::SQLite::VERSION);
350 29         205 my $needed_version = version->parse("1.26_04");
351 29 50 33     295 if ($sqlite_version >= $needed_version || !$sth) {
352 29         122 return $sth;
353             }
354              
355 0         0 my($catalog,$schema,$table_name) = @_;
356              
357 0         0 my @tables;
358             my @returned_names;
359 0         0 while (my $info = $sth->fetchrow_hashref()) {
360             #@returned_names ||= (keys %$info);
361 0 0       0 unless (@returned_names) {
362 0         0 @returned_names = keys(%$info);
363             }
364 0   0     0 $info->{'TABLE_SCHEM'} ||= 'main';
365 0         0 push @tables, $info;
366             }
367              
368 0         0 my $dbh = $self->get_default_handle();
369 0 0       0 my $sponge = DBI->connect("DBI:Sponge:", '','')
370             or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr");
371              
372 0 0       0 unless (@returned_names) {
373 0         0 @returned_names = qw( TABLE_CAT TABLE_SCHEM TABLE_NAME TABLE_TYPE REMARKS );
374             }
375             my $returned_sth = $sponge->prepare("table_info $table_name", {
376 0 0       0 rows => [ map { [ @{$_}{@returned_names} ] } @tables ],
  0         0  
  0         0  
377             NUM_OF_FIELDS => scalar @returned_names,
378             NAME => \@returned_names,
379             }) or return $dbh->DBI::set_err($sponge->err(), $sponge->errstr());
380              
381 0         0 return $returned_sth;
382             }
383              
384              
385             # DBD::SQLite doesn't implement column_info. This is the UR::DataSource version of the same thing
386             sub get_column_details_from_data_dictionary {
387 29     29 0 75 my($self,$catalog,$schema,$table,$column) = @_;
388              
389 29         111 my $dbh = $self->get_default_handle();
390              
391             # Convert the SQL wildcards to regex wildcards
392 29 50       99 $column = '' unless defined $column;
393 29         112 $column =~ s/%/.*/;
394 29         60 $column =~ s/_/./;
395 29         532 my $column_regex = qr(^$column$);
396              
397 29         226 my $sth_tables = $dbh->table_info($catalog, $schema, $table, 'TABLE');
398 29         195 my @table_names = map { $_->{'TABLE_NAME'} } @{ $sth_tables->fetchall_arrayref({}) };
  29         120  
  29         132  
399              
400 29         107 my $override_owner;
401 29 50       164 if ($DBD::SQLite::VERSION < 1.26_04) {
402 0         0 $override_owner = 'main';
403             }
404              
405 29         43 my @columns;
406 29         65 foreach my $table_name ( @table_names ) {
407              
408 29 50       148 my $sth = $dbh->prepare("PRAGMA table_info($table_name)")
409             or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr");
410 29 50       116 $sth->execute() or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr");
411              
412 29         106 while (my $info = $sth->fetchrow_hashref()) {
413              
414 68 50       374 next unless $info->{'name'} =~ m/$column_regex/;
415              
416             # SQLite doesn't parse our that type varchar(255) actually means type varchar size 255
417 68         100 my $data_type = $info->{'type'};
418 68         66 my $column_size;
419 68 100       182 if ($data_type =~ m/(\S+)\s*\((\S+)\)/) {
420 1         3 $data_type = $1;
421 1         1 $column_size = $2;
422             }
423              
424 68         104 my $node = {};
425 68         139 $node->{'TABLE_CAT'} = $catalog;
426 68   66     243 $node->{'TABLE_SCHEM'} = $schema || $override_owner;
427 68         105 $node->{'TABLE_NAME'} = $table_name;
428 68         117 $node->{'COLUMN_NAME'} = $info->{'name'};
429 68         101 $node->{'DATA_TYPE'} = $data_type;
430 68         93 $node->{'TYPE_NAME'} = $data_type;
431 68         99 $node->{'COLUMN_SIZE'} = $column_size;
432 68         171 $node->{'NULLABLE'} = ! $info->{'notnull'};
433 68 100       178 $node->{'IS_NULLABLE'} = ($node->{'NULLABLE'} ? 'YES' : 'NO');
434 68         90 $node->{'REMARKS'} = "";
435 68         99 $node->{'SQL_DATA_TYPE'} = ""; # FIXME shouldn't this be something related to DATA_TYPE
436 68         108 $node->{'SQL_DATETIME_SUB'} = "";
437 68         88 $node->{'CHAR_OCTET_LENGTH'} = undef; # FIXME this should be the same as column_size, right?
438 68         87 $node->{'ORDINAL_POSITION'} = $info->{'cid'};
439 68         87 $node->{'COLUMN_DEF'} = $info->{'dflt_value'};
440             # Remove starting and ending 's that appear erroneously with string default values
441 68 100       152 $node->{'COLUMN_DEF'} =~ s/^'|'$//g if defined ( $node->{'COLUMN_DEF'});
442              
443 68         267 push @columns, $node;
444             }
445             }
446              
447 29 50       232 my $sponge = DBI->connect("DBI:Sponge:", '','')
448             or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr");
449              
450 29         7021 my @returned_names = qw( TABLE_CAT TABLE_SCHEM TABLE_NAME COLUMN_NAME DATA_TYPE TYPE_NAME COLUMN_SIZE
451             BUFFER_LENGTH DECIMAL_DIGITS NUM_PREC_RADIX NULLABLE REMARKS COLUMN_DEF
452             SQL_DATA_TYPE SQL_DATETIME_SUB CHAR_OCTET_LENGTH ORDINAL_POSITION IS_NULLABLE );
453             my $returned_sth = $sponge->prepare("column_info $table", {
454 29 50       106 rows => [ map { [ @{$_}{@returned_names} ] } @columns ],
  68         80  
  68         602  
455             NUM_OF_FIELDS => scalar @returned_names,
456             NAME => \@returned_names,
457             }) or return $dbh->DBI::set_err($sponge->err(), $sponge->errstr());
458              
459 29         2605 return $returned_sth;
460             }
461              
462              
463             # SQLite doesn't store the name of a foreign key constraint in its metadata directly.
464             # We can guess at it from the SQL used in the table creation. These regexes are probably
465             # sloppy. We could replace them if there were a good SQL parser.
466             sub _resolve_fk_name {
467 43     43   82 my($self, $table_name, $column_list, $r_table_name, $r_column_list) = @_;
468              
469 43 50       120 if (@$column_list != @$r_column_list) {
470 0         0 Carp::confess('There are '.scalar(@$column_list).' pk columns and '.scalar(@$r_column_list).' fk columns');
471             }
472              
473 43         110 my($table_info) = $self->_get_info_from_sqlite_master($table_name, 'table');
474 43 50       117 return unless $table_info;
475              
476 43         76 my $col_str = $table_info->{'sql'};
477 43         665 $col_str =~ s/^\s+|\s+$//g; # Remove leading and trailing whitespace
478 43         179 $col_str =~ s/\s{2,}/ /g; # Remove multiple spaces
479 43 50       243 if ($col_str =~ m/^CREATE TABLE (\w+)\s*?\((.*?)\)$/is) {
480 43 50       128 unless ($1 eq $table_name) {
481 0         0 Carp::croak("Table creation SQL for $table_name is inconsistent. Didn't find table name '$table_name' in string '$col_str'. Found $1 instead.");
482             }
483 43         102 $col_str = $2;
484             } else {
485 0         0 Carp::croak("Couldn't parse SQL for $table_name");
486             }
487              
488              
489 43         53 my $fk_name;
490 43 100       110 if (@$column_list > 1) {
491             # Multiple column FKs must be specified as a table-wide constraint, and has a well-known format
492 10         31 my $fk_list = '\s*' . join('\s*,\s*', @$column_list) . '\s*';
493 10         21 my $uk_list = '\s*' . join('\s*,\s*', @$r_column_list) . '\s*';
494 10         42 my $expected_to_find = sprintf('FOREIGN KEY\s*\(%s\) REFERENCES %s\s*\(%s\)',
495             $fk_list,
496             $r_table_name,
497             $uk_list);
498 10         111 my $regex = qr($expected_to_find)i;
499              
500 10 100       55 if ($col_str =~ m/$regex/) {
501 8         50 ($fk_name) = ($col_str =~ m/CONSTRAINT (\w+) FOREIGN KEY\s*\($fk_list\)/i);
502             } else {
503             # Didn't find anything...
504 2         12 return;
505             }
506              
507             } else {
508             # single-column FK constraints can be specified a couple of ways...
509             # First, try as a table-wide constraint
510 33         55 my $col = $column_list->[0];
511 33         46 my $r_col = $r_column_list->[0];
512 33 100       536 if ($col_str =~ m/FOREIGN KEY\s*\($col\)\s*REFERENCES $r_table_name\s*\($r_col\)/i) {
513 8         41 ($fk_name) = ($col_str =~ m/CONSTRAINT\s+(\w+)\s+FOREIGN KEY\s*\($col\)/i);
514             } else {
515 25         75 while ($col_str) {
516             # Try parsing each of the column definitions
517             # commas can't appear in here except to separate each column, right?
518 58         53 my $this_col;
519 58 100       258 if ($col_str =~ m/^(.*?)\s*,\s*(.*)/) {
520 43         85 $this_col = $1;
521 43         61 $col_str = $2;
522             } else {
523 15         26 $this_col = $col_str;
524 15         27 $col_str = '';
525             }
526            
527 58         173 my($col_name, $col_type) = ($this_col =~ m/^(\w+) (\w+)/);
528 58 100 100     271 next unless ($col_name and
529             $col_name eq $col);
530              
531 23 50       214 if ($this_col =~ m/REFERENCES $r_table_name\s*\($r_col\)/i) {
532             # It's the right column, and there's a FK constraint on it
533             # Did the FK get a name?
534 23         55 ($fk_name) = ($this_col =~ m/CONSTRAINT (\w+) REFERENCES/i);
535 23         50 last;
536             } else {
537             # It's the right column, but there's no FK
538 0         0 return;
539             }
540             }
541             }
542             }
543              
544             # The constraint didn't have a name. Make up something that'll likely be unique
545 41   66     195 $fk_name ||= join('_', $table_name, @$column_list, $r_table_name, @$r_column_list, 'fk');
546 41         121 return $fk_name;
547             }
548              
549              
550             # We'll only support specifying $fk_table or $pk_table but not both
551             # $fk_table refers to the table where the fk is attached
552             # $pk_table refers to the table the pk points to - where the primary key exists
553             sub get_foreign_key_details_from_data_dictionary {
554 88     88 0 36607 my($self, $pk_catalog, $pk_schema, $pk_table, $fk_catalog, $fk_schema, $fk_table) = @_;
555              
556             # first, build a data structure to collect columns of the same foreign key together
557 88         114 my @returned_fk_info;
558 88 100       232 if ($fk_table) {
    50          
559 50         175 @returned_fk_info = $self->_get_foreign_key_details_for_fk_table_name($fk_schema, $fk_table);
560              
561             } elsif ($pk_table) {
562             # We'll have to loop through each table in the DB and find FKs that reference
563             # the named table
564              
565 38         215 my @tables = $self->_get_info_from_sqlite_master(undef,'table');
566             TABLE:
567 38         89 foreach my $table_data ( @tables ) {
568 226         308 my $from_table = $table_data->{'table_name'};
569 226     220   1067 push @returned_fk_info, $self->_get_foreign_key_details_for_fk_table_name($fk_schema, $from_table, sub { $_[0]->{table} eq $pk_table });
  220         910  
570             }
571             } else {
572 0         0 Carp::croak("Can't get_foreign_key_details_from_data_dictionary(): either pk_table ($pk_table) or fk_table ($fk_table) are required");
573             }
574              
575 88         314 my $dbh = $self->get_default_handle;
576 88 50       743 my $sponge = DBI->connect("DBI:Sponge:", '','')
577             or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr");
578              
579 88         13296 my @returned_names = qw( UK_TABLE_CAT UK_TABLE_SCHEM UK_TABLE_NAME UK_COLUMN_NAME
580             FK_TABLE_CAT FK_TABLE_SCHEM FK_TABLE_NAME FK_COLUMN_NAME
581             ORDINAL_POSITION UPDATE_RULE DELETE_RULE FK_NAME UK_NAME DEFERABILITY );
582 88   66     308 my $table = $pk_table || $fk_table;
583             my $returned_sth = $sponge->prepare("foreign_key_info $table", {
584 88 50       560 rows => [ map { [ @{$_}{@returned_names} ] } @returned_fk_info ],
  53         55  
  53         477  
585             NUM_OF_FIELDS => scalar @returned_names,
586             NAME => \@returned_names,
587             }) or return $dbh->DBI::set_err($sponge->err(), $sponge->errstr());
588              
589 88         6108 return $returned_sth;
590             }
591              
592             # used by _get_foreign_key_details_for_fk_table_name to convert the on_delete or on_update
593             # string into the number code commonly returnd by DBI
594             my %update_delete_action_to_numeric_code = (
595             CASCADE => 0,
596             RESTRICT => 1,
597             'SET NULL' => 2,
598             'NO ACTION' => 3,
599             'SET DEFAULT' => 4,
600             );
601              
602             sub _get_foreign_key_details_for_fk_table_name {
603 276     276   416 my($self, $fk_schema_name, $fk_table_name, $accept_rows) = @_;
604 276   100 31   714 $accept_rows ||= sub { 1 }; # default is accept all
  31         78  
605              
606 276   100     837 $fk_schema_name ||= 'main';
607 276         464 my $qualified_table_name = join('.', $fk_schema_name, $fk_table_name);
608              
609 276         670 my $dbh = $self->get_default_handle;
610 276 50       938 my $fksth = $dbh->prepare("PRAGMA foreign_key_list($fk_table_name)")
611             or return $dbh->DBI::set_err($DBI::err, "DBI::Sponge: $DBI::errstr");
612 276 50       689 unless ($fksth->execute()) {
613 0         0 $self->error_message("foreign_key_list execute failed: $DBI::errstr");
614 0         0 return;
615             }
616              
617 276         287 my @fk_rows_this_table;
618 276         266 my(@column_list, @r_column_list);
619 276         653 while (my $row = $fksth->fetchrow_hashref) {
620 251 100       399 next unless ($accept_rows->($row));
621              
622             my %fk_info_row = ( FK_TABLE_NAME => $fk_table_name,
623             FK_TABLE_SCHEM => $fk_schema_name,
624             UK_TABLE_SCHEM => $fk_schema_name, # SQLite doesn't tell us what attached DB it's from, so we'll guess
625             UPDATE_RULE => $update_delete_action_to_numeric_code{$row->{on_update}},
626             DELETE_RULE => $update_delete_action_to_numeric_code{$row->{on_delete}},
627 53         382 ORDINAL_POSITION => $row->{seq} + 1,
628             );
629             @fk_info_row{'FK_COLUMN_NAME','UK_TABLE_NAME','UK_COLUMN_NAME'}
630 53         223 = @$row{'from','table','to'};
631              
632 53         87 push @fk_rows_this_table, \%fk_info_row;
633              
634 53         75 push @column_list, $row->{from};
635             push @r_column_list, $row->{to}
636 53         202 }
637              
638 276 100       475 if (@fk_rows_this_table) {
639             my $fk_name = $self->_resolve_fk_name($fk_rows_this_table[0]->{FK_TABLE_NAME},
640             \@column_list,
641             $fk_rows_this_table[0]->{UK_TABLE_NAME}, # They'll all have the same table, right?
642 43         244 \@r_column_list);
643 43         82 foreach my $fk_info_row ( @fk_rows_this_table ) {
644 53         122 $fk_info_row->{FK_NAME} = $fk_name;
645             }
646 43         103 @fk_rows_this_table = sort { $a->{ORDINAL_POSITION} <=> $b->{ORDINAL_POSITION} } @fk_rows_this_table;
  10         29  
647             }
648 276         694 return @fk_rows_this_table;
649             }
650              
651             sub get_bitmap_index_details_from_data_dictionary {
652             # SQLite dosen't support bitmap indicies, so there aren't any
653 0     0 0 0 return [];
654             }
655              
656              
657             sub get_unique_index_details_from_data_dictionary {
658 30     30 0 77 my($self, $owner_name, $table_name) = @_;
659              
660 30         135 my $dbh = $self->get_default_handle();
661 30 50       97 return undef unless $dbh;
662              
663 30         80 my($index_list_fcn, $index_info_fcn) = ('index_list','index_info');
664 30 100       81 if ($owner_name) {
665 11         51 $index_list_fcn = "${owner_name}.${index_list_fcn}";
666 11         22 $index_info_fcn = "${owner_name}.${index_info_fcn}";
667             }
668              
669 30         196 my $idx_sth = $dbh->prepare(qq(PRAGMA ${index_list_fcn}($table_name)));
670              
671 30 50       110 return undef unless $idx_sth;
672              
673 30         127 $idx_sth->execute();
674              
675 30         56 my $ret = {};
676 30         128 while(my $data = $idx_sth->fetchrow_hashref()) {
677 10 50       41 next unless ($data->{'unique'});
678              
679 10         21 my $idx_name = $data->{'name'};
680 10         50 my $idx_item_sth = $dbh->prepare(qq(PRAGMA ${index_info_fcn}($idx_name)));
681 10         38 $idx_item_sth->execute();
682 10         32 while(my $index_item = $idx_item_sth->fetchrow_hashref()) {
683 13   100     206 $ret->{$idx_name} ||= [];
684 13         15 push( @{$ret->{$idx_name}}, $index_item->{'name'});
  13         57  
685             }
686             }
687              
688 30         102 return $ret;
689             }
690              
691              
692             # By default, make a text dump of the database at commit time.
693             # This should really be a datasource property
694             sub dump_on_commit {
695 38     38 0 766 0;
696             }
697              
698             # We're overriding commit from UR::DS::commit() to add the behavior that after
699             # the actual commit happens, we also make a dump of the database in text format
700             # so that can be version controlled
701             sub commit {
702 115     115 1 152 my $self = shift;
703              
704 115         405 my $has_no_pending_trans = (!-f $self->_journal_file_path());
705              
706 115         1868 my $worked = $self->SUPER::commit(@_);
707 115 50       228 return unless $worked;
708              
709 115         384 my $db_filename = $self->server();
710 115         555 my $dump_filename = $self->_data_dump_path();
711              
712 115 100       541 return 1 if ($has_no_pending_trans);
713            
714 38 50 33     231 return 1 unless $self->dump_on_commit or -e $dump_filename;
715            
716 0         0 return $self->_dump_db_to_file_internal();
717             }
718              
719              
720             # Get info out of the sqlite_master table. Returns a hashref keyed by 'name'
721             # columns are:
722             # type - 'table' or 'index'
723             # name - Name of the object
724             # table_name - name of the table this object references. For tables, it's the same as name,
725             # for indexes, it's the name of the table it's indexing
726             # rootpage - Used internally by sqlite
727             # sql - The sql used to create the thing
728             sub _get_info_from_sqlite_master {
729 83     83   145 my($self, $name,$type) = @_;
730              
731 83         95 my($schema, @where, @exec_values);
732 83 100       186 if ($name) {
733 45         158 ($schema, $name) = $self->_resolve_owner_and_table_from_table_name($name);
734 45         84 push @where, 'name = ?';
735 45         57 push @exec_values, $name;
736             }
737 83 100       185 if ($type) {
738 81         109 push @where, 'type = ?';
739 81         98 push @exec_values, $type;
740             }
741              
742 83 100       170 my $sqlite_master_table = $schema
743             ? "${schema}.sqlite_master"
744             : 'sqlite_master';
745 83         160 my $sql = "select * from $sqlite_master_table";
746 83 50       172 if (@where) {
747 83         232 $sql .= ' where '.join(' and ', @where);
748             }
749              
750 83         281 my $dbh = $self->get_default_handle();
751 83         264 my $sth = $dbh->prepare($sql);
752 83 50       221 unless ($sth) {
753 127     127   785 no warnings;
  127         202  
  127         7156  
754 0         0 $self->error_message("Can't get table details for name $name and type $type: ".$dbh->errstr);
755 0         0 return;
756             }
757              
758 83 50       274 unless ($sth->execute(@exec_values)) {
759 127     127   544 no warnings;
  127         179  
  127         183737  
760 0         0 $self->error_message("Can't get table details for name $name and type $type: ".$dbh->errstr);
761 0         0 return;
762             }
763              
764 83         124 my @rows;
765 83         233 while (my $row = $sth->fetchrow_arrayref()) {
766 271         232 my $item;
767 271         909 @$item{'type','name','table_name','rootpage','sql'} = @$row;
768             # Force all names to lower case so we can find them later
769 271         565 push @rows, $item;
770             }
771              
772 83         261 return @rows;
773             }
774              
775              
776             # This is used if, for whatever reason, we can't sue the sqlite3 command-line
777             # program to load up the database. We'll make a good-faith effort to parse
778             # the SQL text, but it won't be fancy. This is intended to be used to initialize
779             # meta DB dumps, so we should have to worry about escaping quotes, multi-line
780             # statements, etc.
781             #
782             # The real DB file should be moved out of the way before this is called. The existing
783             # DB file will be removed.
784             sub _load_db_from_dump_internal {
785 39     39   72 my $self = shift;
786 39         78 my $file_name = shift;
787              
788 39         402 my $fh = IO::File->new($file_name);
789 39 50       4254 unless ($fh) {
790 0         0 Carp::croak("Can't open DB dump file $file_name: $!");
791             }
792              
793 39         141 my $db_file = $self->server;
794 39 50       444 if (-f $db_file) {
795 0 0       0 unless(unlink($db_file)) {
796 0         0 Carp::croak("Can't remove DB file $db_file: $!");
797             }
798             }
799              
800 39         519 my $dbh = DBI->connect("dbi:SQLite:dbname=$db_file",'','',{ AutoCommit => 0, RaiseError => 0 });
801 39 50       24486 unless($dbh) {
802 0         0 Carp::croak("Can't create DB handle for file $db_file: $DBI::errstr");
803             }
804              
805 39         63 my $dump_file_contents = do { local( $/ ) ; <$fh> };
  39         145  
  39         973  
806 39         565 my @sql = split(';',$dump_file_contents);
807              
808 39         191 for (my $i = 0; $i < @sql; $i++) {
809 827         52202 my $sql = $sql[$i];
810 827 100       2418 next unless ($sql =~ m/\S/); # Skip blank lines
811 788 100       5504 next if ($sql =~ m/BEGIN TRANSACTION|COMMIT/i); # We're probably already in a transaction
812              
813             # Is it restoring the foreign_keys setting?
814 710 100       1432 if ($sql =~ m/PRAGMA foreign_keys\s*=\s*(\w+)/) {
815 30         74 my $value = $1;
816 30         348 my $fk_setting = $self->_get_foreign_key_setting($dbh);
817 30 50       122 if (! defined($fk_setting)) {
818             # This version of SQLite cannot enforce foreign keys.
819             # Print a warning message if they're trying to turn it on.
820             # also, remember the setting so we can preserve its value
821             # in _dump_db_to_file_internal()
822 0         0 $self->_cache_foreign_key_setting_from_file($value);
823 0 0       0 if ($value ne 'OFF') {
824 0         0 $self->warning_message("Data source ".$self->id." does not support foreign key enforcement, but the dump file $db_file attempts to turn it on");
825             }
826 0         0 next;
827             }
828             }
829              
830 710 50       2357 unless ($dbh->do($sql)) {
831 0         0 Carp::croak("Error processing SQL statement $i from DB dump file:\n$sql\nDBI error was: $DBI::errstr\n");
832             }
833             }
834              
835 39         1837025 $dbh->commit();
836 39         6361 $dbh->disconnect();
837              
838 39         4090 return 1;
839             }
840              
841              
842             sub _cache_foreign_key_setting_from_file {
843 0     0   0 my $self = shift;
844              
845 0         0 our %foreign_key_setting_from_file;
846 0         0 my $id = $self->id;
847              
848 0 0       0 if (@_) {
849 0         0 $foreign_key_setting_from_file{$id} = shift;
850             }
851 0         0 return $foreign_key_setting_from_file{$id};
852             }
853              
854             # Is foreign key enforcement on or off?
855             # returns undef if this version of SQLite cannot enforce foreign keys
856             sub _get_foreign_key_setting {
857 32     32   62 my $self = shift;
858 32         67 my $dbh = shift;
859 32         377 my $id = $self->id;
860              
861 32         69 our %foreign_key_setting;
862 32 100       132 unless (exists $foreign_key_setting{$id}) {
863 30   66     126 $dbh ||= $self->get_default_handle;
864 30         383 my @row = $dbh->selectrow_array('PRAGMA foreign_keys');
865 30         15613 $foreign_key_setting{$id} = $row[0];
866             }
867 32         97 return $foreign_key_setting{$id};
868             }
869              
870             sub _resolve_order_by_clause_for_column {
871 2161     2161   3201 my($self, $column_name, $query_plan, $property_meta) = @_;
872              
873 2161         6302 my $is_optional = $property_meta->is_optional;
874              
875 2161         2937 my $column_clause = $column_name; # default, usual case
876 2161 100       7878 if ($is_optional) {
    100          
877 7 100       28 if ($query_plan->order_by_column_is_descending($column_name)) {
878 3         12 $column_clause = "CASE WHEN $column_name ISNULL THEN 0 ELSE 1 END, $column_name DESC";
879             } else {
880 4         14 $column_clause = "CASE WHEN $column_name ISNULL THEN 1 ELSE 0 END, $column_name";
881             }
882             } elsif ($query_plan->order_by_column_is_descending($column_name)) {
883 3         8 $column_clause = $column_name . ' DESC';
884             }
885 2161         6913 return $column_clause;
886             }
887              
888             sub _resolve_limit_value_from_query_plan {
889 1422     1422   1754 my($self, $query_plan) = @_;
890 1422         4687 my $limit = $query_plan->limit;
891 1422 100 100     6408 return (!defined($limit) and $query_plan->offset)
892             ? -1
893             : $limit;
894             }
895              
896              
897             sub _dump_db_to_file_internal {
898 1     1   3 my $self = shift;
899              
900 1         5 my $fk_setting = $self->_get_foreign_key_setting();
901              
902 1         5 my $file_name = $self->_data_dump_path();
903 1 50       21 unless (-w $file_name) {
904             # dump file isn't writable...
905 0         0 return 1;
906             }
907              
908 1         5 my $fh = IO::File->new($file_name, '>');
909 1 50       87 unless ($fh) {
910 0         0 Carp::croak("Can't open DB dump file $file_name for writing: $!");
911             }
912              
913 1         4 my $db_file = $self->server;
914 1         8 my $dbh = $self->get_default_handle;
915 1 50       4 unless ($dbh) {
916 0         0 Carp::croak("Can't create DB handle for file $db_file: $DBI::errstr");
917             }
918              
919 1 50       5 if (defined $fk_setting) {
920             # Save the value of the foreign_keys setting, if it's supported
921 1 50       8 $fh->print('PRAGMA foreign_keys = ' . ( $fk_setting ? 'ON' : 'OFF' ) .";\n");
922             } else {
923             # If not supported, but if _load_db_from_dump_internal came across the value, preserve it
924 0         0 $fk_setting = $self->_cache_foreign_key_setting_from_file;
925 0 0       0 if (defined $fk_setting) {
926 0         0 $fh->print("PRAGMA foreign_keys = $fk_setting;\n");
927             }
928             }
929              
930 1         11 $fh->print("BEGIN TRANSACTION;\n");
931              
932 1         14 my @tables = $self->_get_table_names_from_data_dictionary();
933 1         4 foreach my $qualified_table ( @tables ) {
934 2         14 my(undef, $table) = $self->_resolve_owner_and_table_from_table_name($qualified_table);
935 2         10 my($item_info) = $self->_get_info_from_sqlite_master($table);
936 2         5 my $creation_sql = $item_info->{'sql'};
937 2 50       7 $creation_sql .= ";" unless(substr($creation_sql, -1, 1) eq ";");
938 2 50       6 $creation_sql .= "\n" unless(substr($creation_sql, -1, 1) eq "\n");
939              
940 2         9 $fh->print($creation_sql);
941              
942 2 50       18 if ($item_info->{'type'} eq 'table') {
943 2         7 my $sth = $dbh->prepare("select * from $table");
944 2 50       7 unless ($sth) {
945 0         0 Carp::croak("Can't retrieve data from table $table: $DBI::errstr");
946             }
947 2 50       8 unless($sth->execute()) {
948 0         0 Carp::croak("execute() failed while retrieving data for table $table: $DBI::errstr");
949             }
950              
951 2         7 while(my @row = $sth->fetchrow_array) {
952 6         9 foreach my $col ( @row ) {
953 12 100 66     63 if (! defined $col) {
    100          
954 1         2 $col = 'null';
955             } elsif ($col =~ m/\D/ or length($col) == 0) {
956 2         7 $col = "'" . $col . "'"; # Put quotes around non-numeric stuff
957             }
958             }
959 6         32 $fh->printf("INSERT INTO %s VALUES(%s);\n",
960             $table,
961             join(',', @row));
962             }
963             }
964             }
965 1         4 $fh->print("COMMIT;\n");
966 1         7 $fh->close();
967              
968 1         77 $dbh->disconnect();
969              
970 1         14 return 1;
971             }
972            
973              
974             sub _create_dbh_for_alternate_db {
975 70     70   121 my($self, $connect_string) = @_;
976              
977 70         244 my $match_dbname = qr{dbname=([^;]+)}i;
978 70         392 my($db_file) = $connect_string =~ m/$match_dbname/;
979 70 50       205 $db_file
980             || Carp::croak("Cannot determine dbname for alternate DB from dbi connect string $connect_string");
981              
982 70 100       214 if ($self->_db_path_specifies_a_directory($db_file)) {
983 36         244 mkdir $db_file;
984 36         129 my $main_schema_file = join('', 'main', $self->_extension_for_db);
985 36         511 $db_file = File::Spec->catfile($db_file, $main_schema_file);
986              
987 36         311 $connect_string =~ s/$match_dbname/dbname=$db_file/;
988             }
989              
990 70         318 my $dbh = $self->SUPER::_create_dbh_for_alternate_db($connect_string);
991 70         25366 return $dbh;
992             }
993              
994             sub _db_path_specifies_a_directory {
995 245     245   2249 my($self, $pathname) = @_;
996 245   66     5072 return (-d $pathname) || ($pathname =~ m{/$});
997             }
998              
999             sub _assure_schema_exists_for_table {
1000 90     90   161 my($self, $table_name, $dbh) = @_;
1001 90   33     186 $dbh ||= $self->get_default_handle;
1002              
1003 90         272 my($schema_name, undef) = $self->_extract_schema_and_table_name($table_name);
1004 90 50 33     269 if ($schema_name
1005             and
1006             ! $self->is_schema_attached($schema_name, $dbh)
1007             ) {
1008             # pretend we have schemas
1009              
1010 0           my($main_filename) = $dbh->{Name} =~ m/(?:dbname=)*(.*)/;
1011 0           my $directory = File::Basename::dirname($main_filename);
1012 0           my $schema_filename = File::Spec->catfile($directory, "${schema_name}.sqlite3");
1013 0 0         unless (UR::Util::touch_file($schema_filename)) {
1014 0           Carp::carp("touch_file $schema_filename failed: $!");
1015 0           return;
1016             }
1017 0 0         unless ($dbh->do(qq(ATTACH DATABASE '$schema_filename' as $schema_name))) {
1018 0           Carp::carp("Cannot attach file $schema_filename as $schema_name: ".$dbh->errstr);
1019 0           return;
1020             }
1021             }
1022             }
1023              
1024             sub attached_schemas {
1025 0     0 0   my($self, $dbh) = @_;
1026 0   0       $dbh ||= $self->get_default_handle;
1027              
1028             # Statement returns id, schema, filename
1029 0   0       my $sth = $dbh->prepare('PRAGMA database_list') || Carp::croak("Cannot list attached databases: ".$dbh->errstr);
1030 0           $sth->execute();
1031 0           my %schemas = map { $_->[1] => $_->[2] }
1032 0           @{ $sth->fetchall_arrayref };
  0            
1033 0           return \%schemas;
1034             }
1035              
1036             sub is_schema_attached {
1037 0     0 0   my($self, $schema, $dbh) = @_;
1038 0   0       $dbh ||= $self->get_default_handle;
1039              
1040 0           my $schemas = $self->attached_schemas($dbh);
1041 0           return exists $schemas->{$schema};
1042             }
1043              
1044             1;