File Coverage

blib/lib/PICA/SQLiteStore.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package PICA::SQLiteStore;
2             {
3             $PICA::SQLiteStore::VERSION = '0.585';
4             }
5             #ABSTRACT: Store Ls in a SQLite database with versioning
6 1     1   1415 use strict;
  1         3  
  1         49  
7              
8 1     1   7 use PICA::Record;
  1         3  
  1         57  
9 1     1   684 use PICA::Store;
  0            
  0            
10             use Carp qw(croak);
11             use DBD::SQLite;
12             use DBI;
13              
14             our @ISA=qw(PICA::Store);
15              
16              
17             sub new {
18             my $class = shift;
19             my ($filename, %params) = (@_ % 2) ? (@_) : (undef, @_);
20              
21             PICA::Store::readconfigfile( \%params, $ENV{PICASTORE} )
22             if exists $params{config} or exists $params{conf} ;
23              
24             $filename = $params{SQLite} unless defined $filename;
25              
26             croak("filename for SQLite database not specified") unless defined $filename;
27              
28             my $rebuild = $params{rebuild};
29             # TODO: option to use PPN as ID !
30              
31             my $dbh = DBI->connect( "dbi:SQLite:dbname=$filename","","",
32             { AutoCommit => 0, RaiseError => 1 } );
33             $dbh->{sqlite_unicode} = 1;
34              
35             croak("SQLite database connection failed: $filename: " . DBD->errstr) unless $dbh;
36              
37             #$dbh::DESCTROY = DESTROY {
38             # my $sth = shift;
39             # $sth->finish if $sth->FETCH('Active');
40             #}
41              
42             # tables and triggers
43             my %tables = (
44             record => [
45             'record_ppn INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT',
46             'record_first INTEGER NOT NULL DEFAULT 0', # first revision
47             'record_latest INTEGER NOT NULL DEFAULT 0', # current revision
48             ],
49             revision => [
50             'rev_id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT', # key (version)
51             'rev_ppn INTEGER DEFAULT 0', # foreign key to record.record_ppn
52             'rev_data TEXT NOT NULL', # PICA+ data
53             'rev_timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP',
54             'rev_user TEXT DEFAULT 0',
55             'rev_deleted BOOLEAN NOT NULL DEFAULT 0', # delete action
56             'rev_is_new BOOLEAN NOT NULL DEFAULT 0'
57             ],
58             archive => [
59             'arc_ppn INTEGER PRIMARY KEY',
60             'arc_latest INTEGER NOT NULL DEFAULT 0'
61             ],
62             );
63             my %triggers = (
64             record_insert => q{CREATE TRIGGER record_insert AFTER INSERT ON revision WHEN new.rev_ppn = 0
65             BEGIN
66             INSERT INTO record (record_first,record_latest) VALUES (new.rev_id,new.rev_id);
67             UPDATE revision SET rev_ppn=last_insert_rowid(), rev_is_new=1 WHERE rev_id=new.rev_id;
68             END;},
69             record_update => q{CREATE TRIGGER record_update AFTER INSERT ON revision WHEN new.rev_ppn != 0
70             BEGIN
71             UPDATE record SET record_latest=new.rev_id WHERE record_ppn=new.rev_ppn;
72             END;},
73             record_delete => q{CREATE TRIGGER record_delete DELETE ON record
74             BEGIN
75             INSERT INTO archive (arc_ppn, arc_latest) VALUES (old.record_ppn, old.record_latest);
76             UPDATE revision SET rev_deleted=1 WHERE rev_id=old.record_latest;
77             END;},
78             );
79             # TODO: where is timestamp and user of deletion logged??
80             # INSERT INTO revision (rev_ppn,rev_deleted,rev_user) VALUES (old.record_ppn,1,''); -- TODO
81              
82             my @tb;
83             my $std_tab = $dbh->table_info('', '', '%', '');
84             while( my $tbl = $std_tab->fetchrow_hashref ) {
85             push @tb, $tbl->{TABLE_NAME} if $tables{$tbl->{TABLE_NAME}};
86             # TODO: check whether there is any difference in table definitions
87             }
88             $rebuild = 1 if (@tb != keys %tables);
89              
90             if ($rebuild) {
91             eval {
92             foreach my $name (@tb) {
93             $dbh->do("DROP TABLE $name");
94             }
95             foreach my $name (keys %tables) {
96             my $sql = "CREATE TABLE $name (".join(",",@{$tables{$name}}).")";
97             $dbh->do($sql);
98             };
99             foreach my $name (keys %triggers) {
100             $dbh->do($triggers{$name});
101             }
102             $dbh->commit;
103             };
104             croak("Failed to create database structure: $@") if $@;
105             }
106              
107             my $self = bless {
108             dbh => $dbh,
109             user => 0, # current user id
110             }, $class;
111              
112             # init prepared statements
113             $self->{get_record} = $dbh->prepare(q{SELECT
114             rev_user AS user, rev_ppn AS id, rev_data AS record, rev_timestamp AS timestamp, rev_id AS version, rev_id AS latest
115             FROM revision, record WHERE revision.rev_id=record.record_latest AND revision.rev_ppn=record.record_ppn AND record_ppn=?;});
116             $self->{get_revision} = $dbh->prepare(q{SELECT
117             rev_user AS user, rev_ppn AS id, rev_data AS record, rev_timestamp AS timestamp, rev_id AS version, record_latest AS latest
118             FROM revision, record WHERE rev_ppn=record_ppn AND revision.rev_id=?;});
119             $self->{insert_record} = $dbh->prepare('INSERT INTO revision (rev_ppn,rev_data,rev_user) VALUES (0,?,?)');
120             $self->{update_record} = $dbh->prepare('INSERT INTO revision (rev_ppn,rev_data,rev_user) VALUES (?,?,?)');
121             $self->{delete_record} = $dbh->prepare('DELETE FROM record WHERE record_ppn=?');
122             $self->{recent_changes} = $dbh->prepare(q{SELECT
123             rev_id AS version, rev_ppn AS ppn, rev_user AS user, rev_timestamp AS timestamp, rev_is_new AS is_new, rev_deleted AS deleted FROM revision
124             ORDER BY version DESC LIMIT ? OFFSET ?});
125             $self->{record_history} = $dbh->prepare(q{SELECT
126             rev_ppn AS ppn, rev_id AS version, rev_user AS user, rev_timestamp AS timestamp, rev_is_new AS is_new, rev_deleted AS deleted FROM revision
127             WHERE rev_ppn=?
128             ORDER BY version DESC LIMIT ? OFFSET ?
129             });
130             $self->{next_rev} = $dbh->prepare(q{SELECT
131             rev_id AS version, rev_user AS user, rev_timestamp AS timestamp, rev_is_new AS is_new, rev_deleted AS deleted FROM revision
132             WHERE rev_ppn = ? AND rev_id > ?
133             ORDER BY version ASC LIMIT ?
134             });
135             $self->{prev_rev} = $dbh->prepare(q{SELECT
136             rev_id AS version, rev_user AS user, rev_timestamp AS timestamp, rev_is_new AS is_new, rev_deleted AS deleted FROM revision
137             WHERE rev_ppn = ? AND rev_id < ?
138             ORDER BY version DESC LIMIT ?
139             });
140             $self->{deleted} = $dbh->prepare(q{SELECT rev_timestamp AS timestamp, rev_user AS user, arc_ppn AS ppn, arc_latest AS version FROM archive, revision
141             WHERE rev_id=arc_latest ORDER BY arc_latest DESC LIMIT ? OFFSET ?
142             });
143             $self->{contributions} = $dbh->prepare(q{SELECT
144             rev_id AS version, rev_ppn AS ppn, rev_user AS user, rev_timestamp AS timestamp, rev_is_new AS is_new, rev_deleted AS s FROM revision
145             WHERE rev_user=? ORDER BY version DESC LIMIT ? OFFSET ?
146             });
147              
148             return $self;
149             }
150              
151              
152             sub get {
153             my ($self, $id, $version) = @_;
154              
155             my %result;
156             eval {
157             my $stm;
158             if ($version) {
159             $stm = $self->{get_revision};
160             $stm->execute( $version );
161             } else {
162             $stm = $self->{get_record};
163             $stm->execute( $id );
164             }
165             my $hashref = $stm->fetchrow_hashref;
166             croak( $version ? "version $version" : $id) unless $hashref;
167             $hashref->{record} = PICA::Record->new( $hashref->{record} );
168             if ($version && $id) {
169             %result = $hashref->{id} == $id ? %$hashref : (
170             errorcode => 2, errormessage => "record id does not match version"
171             );
172             } else {
173             %result = %$hashref;
174             }
175             $stm->finish;
176             };
177             if ($@) {
178             # TODO: remove line number
179             %result = ( errorcode => 1, errormessage => "get failed: $@" );
180             }
181             return %result;
182             }
183              
184              
185             sub create {
186             my ($self, $record) = @_;
187              
188             croak('create needs a PICA::Record object')
189             unless UNIVERSAL::isa($record,'PICA::Record');
190              
191             my %result = eval {
192             my $recorddata = $record->string;
193             $self->{insert_record}->execute( $recorddata, $self->{user} );
194             my $version = $self->{dbh}->func('last_insert_rowid');
195             $self->get( undef, $version );
196             };
197             if ($@) {
198             %result = ( errorcode => 1, errormessage => "create failed: $@" );
199             $self->{dbh}->rollback;
200             } else {
201             $self->{dbh}->commit;
202             }
203             return %result;
204             }
205              
206              
207             sub update {
208             my ($self, $id, $record, $version) = @_;
209              
210             croak('update needs a PICA::Record object')
211             unless UNIVERSAL::isa($record,'PICA::Record');
212              
213             my %result = eval {
214             if ($version) {
215             # TODO (version is ignored so far)
216             }
217             $self->{update_record}->execute( $id, $record->string, $self->{user} );
218             $self->get( $id );
219             };
220             if ($@) {
221             %result = ( errorcode => 1, errormessage => "update failed: $@" );
222             $self->{dbh}->rollback;
223             } else {
224             $self->{dbh}->commit;
225             }
226             return %result;
227             }
228              
229              
230             sub delete {
231             my ($self, $id) = @_;
232              
233             my %result = eval {
234             # TODO: create a new version
235             $self->{update_record}->execute( $id, "", $self->{user} );
236             $self->{delete_record}->execute( $id );
237             ( 'id' => $id );
238             };
239             if ($@) {
240             %result = ( errorcode => 1, errormessage => "delete failed: $@" );
241             $self->{dbh}->rollback;
242             } else {
243             $self->{dbh}->commit;
244             }
245             return %result;
246             }
247              
248              
249             sub access {
250             my ($self, %params) = @_;
251              
252             for my $key (qw(userkey password dbsid language)) {
253             # ...check whether access can be granted or not...
254             }
255              
256             $self->{user} = $params{userkey};
257              
258             return $self;
259             }
260              
261              
262             sub history {
263             my ($self, $id, $offset, $limit) = @_;
264              
265             $offset = 0 unless $offset;
266             $limit = 30 unless $limit;
267              
268             eval {
269             $self->{record_history}->execute( $id, $limit, $offset );
270             my $result = $self->{record_history}->fetchall_arrayref({});
271             $self->{record_history}->finish();
272             return $result;
273             };
274             }
275              
276              
277             sub prevnext {
278             my ($self, $id, $version, $limit) = @_;
279             $limit = 1 unless $limit;
280              
281             my $revisions = {};
282              
283             eval {
284             $self->{prev_rev}->execute( $id, $version, $limit );
285             $revisions = $self->{prev_rev}->fetchall_hashref('version');
286             $self->{prev_rev}->finish();
287             $self->{next_rev}->execute( $id, $version, $limit );
288             my $result = $self->{next_rev}->fetchall_hashref('version');
289             $self->{next_rev}->finish();
290             while (my ($k,$v) = each %$result) {
291             $revisions->{$k} = $v;
292             }
293             };
294              
295             return $revisions;
296             }
297              
298              
299             sub recentchanges {
300             my ($self, $offset, $limit) = @_;
301              
302             $offset = 0 unless $offset;
303             $limit = 30 unless $limit;
304              
305             eval {
306             $self->{recent_changes}->execute( $limit, $offset );
307             my $result = $self->{recent_changes}->fetchall_arrayref({});
308             $self->{recent_changes}->finish();
309             return $result;
310             };
311             }
312              
313              
314             sub contributions {
315             my ($self, $user, $offset, $limit) = @_;
316              
317             $offset = 0 unless $offset;
318             $limit = 30 unless $limit;
319              
320             eval {
321             $self->{contributions}->execute( $user, $limit, $offset );
322             my $result = $self->{contributions}->fetchall_arrayref({});
323             $self->{contributions}->finish();
324             return $result;
325             };
326             }
327              
328              
329             sub deletions {
330             my ($self, $offset, $limit) = @_;
331              
332             $offset = 0 unless $offset;
333             $limit = 30 unless $limit;
334              
335             eval {
336             $self->{deleted}->execute( $limit, $offset );
337             my $result = $self->{deleted}->fetchall_arrayref({});
338             $self->{deleted}->finish();
339             return $result;
340             };
341             }
342              
343              
344             sub DESTROY {
345             my $self = shift;
346             $self->{dbh}->disconnect;
347             }
348              
349              
350             1;
351              
352             __END__