File Coverage

blib/lib/Games/Lacuna/Task/Storage.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Games::Lacuna::Task::Storage;
2              
3 1     1   1358 use 5.010;
  1         4  
  1         53  
4             our $VERSION = $Games::Lacuna::Task::VERSION;
5              
6 1     1   431 use Moose;
  0            
  0            
7             with qw(Games::Lacuna::Task::Role::Logger);
8              
9             use Games::Lacuna::Task;
10              
11             use DBI;
12             use Digest::MD5 qw(md5_hex);
13             use JSON qw();
14              
15             our %LOCAL_CACHE;
16             our $JSON = JSON->new->pretty(0)->utf8(1)->indent(0);
17              
18             has 'file' => (
19             is => 'ro',
20             isa => 'Path::Class::File',
21             required => 1,
22             coerce => 1,
23             );
24              
25             has 'current_version' => (
26             is => 'rw',
27             isa => 'Num',
28             lazy_build => 1,
29             required => 1,
30             );
31              
32             has 'latest_version' => (
33             is => 'ro',
34             isa => 'Num',
35             default => $Games::Lacuna::Task::VERSION,
36             required => 1,
37             );
38              
39             has 'dbh' => (
40             is => 'ro',
41             isa => 'DBI::db',
42             lazy_build => 1,
43             );
44              
45             sub _build_current_version {
46             my ($self) = @_;
47            
48             my ($current_version) = $self->dbh->selectrow_array('SELECT value FROM meta WHERE key = ?',{},'database_version');
49             $current_version ||= 2.00;
50             return $current_version;
51             }
52              
53             sub _build_dbh {
54             my ($self) = @_;
55            
56             my $dbh;
57             my $database_ok = 1;
58             my $file = $self->file;
59            
60             # Touch database file if it does not exist
61             unless (-e $file->stringify) {
62             $database_ok = 0;
63            
64             $self->log('info',"Initializing storage file %s",$file->stringify);
65             my $file_dir = $file->parent->stringify;
66             unless (-e $file_dir) {
67             mkdir($file_dir)
68             or $self->abort('Could not create storage directory %s: %s',$file_dir,$!);
69             }
70             $file->touch
71             or $self->abort('Could not create storage file %s: %s',$file->stringify,$!);
72             }
73            
74             # Connect database
75             {
76             no warnings 'once';
77             $dbh = DBI->connect("dbi:SQLite:dbname=$file","","",{ sqlite_unicode => 1 })
78             or $self->abort('Could not connect to database: %s',$DBI::errstr);
79             }
80            
81             # Set dbh
82             $self->meta->get_attribute('dbh')->set_raw_value($self,$dbh);
83            
84             # Check database for meta table
85             if ($database_ok) {
86             ($database_ok) = $dbh->selectrow_array('SELECT COUNT(1) FROM sqlite_master WHERE type=? AND name = ?',{},'table','meta');
87             }
88            
89             # Initialize database
90             unless ($database_ok) {
91             sleep 1;
92             $self->initialize();
93            
94             # Upgrade existing database
95             } else {
96             $self->upgrade();
97             }
98            
99             # Create distance function
100             $dbh->func( 'distance_func', 4, \&Games::Lacuna::Task::Utils::distance, "create_function" );
101            
102             return $dbh;
103             }
104              
105             sub initialize {
106             my ($self) = @_;
107            
108             $self->log('info',"Initializing storage tables in %s",$self->file->stringify);
109              
110             my $dbh = $self->dbh;
111             my $data_fh = *DATA;
112            
113             my $sql = '';
114             while (my $line = <$data_fh>) {
115             $sql .= $line;
116             if ($sql =~ m/;/) {
117             $dbh->do($sql)
118             or $self->abort('Could not excecute sql %s: %s',$sql,$dbh->errstr);
119             undef $sql;
120             }
121             }
122             close DATA;
123            
124             # Set version
125             $self->current_version($self->latest_version);
126             $dbh->do('INSERT INTO meta (key,value) VALUES (?,?)',{},'database_version',$self->current_version);
127             }
128              
129             sub upgrade {
130             my ($self) = @_;
131            
132             return
133             if $self->current_version == $self->latest_version;
134            
135             my $dbh = $self->dbh;
136            
137             $self->log('info',"Upgrading storage from version %.2f to %.2f",$self->current_version(),$self->latest_version);
138            
139             my @sql;
140            
141             if ($self->current_version() < 2.01) {
142             $self->log('debug','Upgrade for 2.00->2.01');
143            
144             push(@sql,'ALTER TABLE star RENAME TO star_old');
145            
146             push(@sql,'CREATE TABLE IF NOT EXISTS star (
147             id INTEGER NOT NULL PRIMARY KEY,
148             x INTEGER NOT NULL,
149             y INTEGER NOT NULL,
150             name TEXT NOT NULL,
151             zone TEXT NOT NULL,
152             last_checked INTEGER,
153             is_probed INTEGER,
154             is_known INTEGER
155             )');
156            
157             push(@sql,'INSERT INTO star (id,x,y,name,zone,last_checked,is_probed,is_known) SELECT id,x,y,name,zone,last_checked,probed,probed FROM star_old');
158            
159             push(@sql,'DROP TABLE star_old');
160            
161             push(@sql,'DELETE FROM cache');
162             }
163              
164             if ($self->current_version() < 2.02) {
165             $self->log('debug','Upgrade for 2.01->2.02');
166             push(@sql,'ALTER TABLE empire ADD COLUMN alliance INTEGER');
167             push(@sql,'ALTER TABLE empire ADD COLUMN colony_count INTEGER');
168             push(@sql,'ALTER TABLE empire ADD COLUMN level INTEGER');
169             push(@sql,'ALTER TABLE empire ADD COLUMN date_founded INTEGER');
170             push(@sql,'ALTER TABLE empire ADD COLUMN affinity TEXT');
171             push(@sql,'ALTER TABLE empire ADD COLUMN last_checked INTEGER');
172             }
173            
174             if ($self->current_version() < 2.03) {
175             $self->log('debug','Upgrade for 2.02->2.03');
176            
177             push(@sql,'ALTER TABLE body RENAME TO body_old');
178            
179             push(@sql,'CREATE TABLE IF NOT EXISTS body (
180             id INTEGER NOT NULL PRIMARY KEY,
181             star INTEGER NOT NULL,
182             x INTEGER NOT NULL,
183             y INTEGER NOT NULL,
184             orbit INTEGER NOT NULL,
185             size INTEGER NOT NULL,
186             name TEXT NOT NULL,
187             normalized_name TEXT NOT NULL,
188             type TEXT NOT NULL,
189             water INTEGER,
190             ore TEXT,
191             empire INTEGER,
192             is_excavated INTEGER
193             )');
194            
195             push(@sql,'INSERT INTO body (id,star,x,y,orbit,size,name,normalized_name,type,water,ore,empire) SELECT id,star,x,y,orbit,size,name,normalized_name,type,water,ore,empire FROM body_old');
196            
197             push(@sql,'DROP TABLE body_old');
198             }
199            
200             if (scalar @sql) {
201             foreach my $sql (@sql) {
202             $dbh->do($sql)
203             or $self->abort('Could not excecute sql %s: %s',$sql,$dbh->errstr);
204             }
205             }
206            
207             $self->current_version($self->latest_version);
208            
209             $dbh->do('INSERT OR REPLACE INTO meta (key,value) VALUES (?,?)',{},'database_version',$self->latest_version);
210            
211             return;
212             }
213              
214             sub selectrow_array {
215             my ($self,$sql,@bind) = @_;
216            
217             my $sth = $self->prepare($sql);
218             $sth->execute(@bind)
219             or return;
220            
221             my (@row) = $sth->fetchrow_array()
222             and $sth->finish;
223            
224             return @row;
225             }
226              
227             sub selectrow_hashref {
228             my ($self,$sql,@bind) = @_;
229            
230             my $sth = $self->prepare($sql);
231             $sth->execute(@bind)
232             or return;
233            
234             my $row = $sth->fetchrow_hashref()
235             and $sth->finish;
236            
237             return $row;
238             }
239              
240             sub do {
241             my ($self,$sql,@params) = @_;
242            
243             my $sql_log = $sql;
244             $sql_log =~ s/\n/ /g;
245              
246             foreach my $element (@params) {
247             if (ref $element) {
248             $element = $JSON->encode($element);
249             }
250             }
251            
252             return $self->dbh->do($sql,{},@params)
253             or $self->abort('Could not run SQL command "%s": %s',$sql_log,$self->dbh->errstr);
254             }
255              
256             sub prepare {
257             my ($self,$sql) = @_;
258            
259             my $sql_log = $sql;
260             $sql_log =~ s/\n/ /g;
261            
262             return $self->dbh->prepare($sql)
263             or $self->abort('Could not prepare SQL command "%s": %s',$sql_log,$self->dbh->errstr);
264             }
265              
266             sub get_cache {
267             my ($self,$key) = @_;
268            
269             return $LOCAL_CACHE{$key}->[0]
270             if defined $LOCAL_CACHE{$key};
271            
272             my ($value,$valid_until) = $self
273             ->selectrow_array(
274             'SELECT value, valid_until FROM cache WHERE key = ?',
275             $key
276             );
277            
278             return
279             if ! defined $value
280             || $valid_until < time();
281            
282             return $JSON->decode($value);
283             }
284              
285             sub set_cache {
286             my ($self,%params) = @_;
287            
288             $params{max_age} ||= 3600;
289              
290             my $valid_until = $params{valid_until} || ($params{max_age} + time());
291             my $key = $params{key};
292             my $value = $JSON->encode($params{value});
293             my $checksum = md5_hex($value);
294            
295             return
296             if defined $LOCAL_CACHE{$key}
297             && $LOCAL_CACHE{$key}->[1] eq $checksum;
298            
299             $LOCAL_CACHE{$key} = [ $params{value},$checksum ];
300            
301             # # Check local write cache
302             # my $checksum = $cache->checksum();
303             # if (defined $LOCAL_CACHE{$key}) {
304             # my $local_cache = $LOCAL_CACHE{$key};
305             # return $cache
306             # if $local_cache eq $checksum;
307             # }
308             #
309             # $LOCAL_CACHE{$key} = $checksum;
310            
311             $self->do(
312             'INSERT OR REPLACE INTO cache (key,value,valid_until,checksum) VALUES (?,?,?,?)',
313             $key,
314             $value,
315             $valid_until,
316             $checksum,
317             );
318            
319             return;
320             }
321              
322             sub clear_cache {
323             my ($self,$key) = @_;
324            
325             delete $LOCAL_CACHE{$key};
326            
327             $self->do(
328             'DELETE FROM cache WHERE key = ?',
329             $key,
330             );
331             }
332              
333              
334             __PACKAGE__->meta->make_immutable;
335             no Moose;
336             1;
337              
338             __DATA__
339             DROP TABLE IF EXISTS star;
340              
341             CREATE TABLE IF NOT EXISTS star (
342             id INTEGER NOT NULL PRIMARY KEY,
343             x INTEGER NOT NULL,
344             y INTEGER NOT NULL,
345             name TEXT NOT NULL,
346             zone TEXT NOT NULL,
347             last_checked INTEGER,
348             is_probed INTEGER,
349             is_known INTEGER
350             );
351              
352             DROP TABLE IF EXISTS body;
353              
354             CREATE TABLE IF NOT EXISTS body (
355             id INTEGER NOT NULL PRIMARY KEY,
356             star INTEGER NOT NULL,
357             x INTEGER NOT NULL,
358             y INTEGER NOT NULL,
359             orbit INTEGER NOT NULL,
360             size INTEGER NOT NULL,
361             name TEXT NOT NULL,
362             normalized_name TEXT NOT NULL,
363             type TEXT NOT NULL,
364             water INTEGER,
365             ore TEXT,
366             empire INTEGER,
367             is_excavated INTEGER
368             );
369              
370             CREATE INDEX IF NOT EXISTS body_star_index ON body(star);
371              
372             DROP TABLE IF EXISTS empire;
373              
374             CREATE TABLE IF NOT EXISTS empire (
375             id INTEGER NOT NULL PRIMARY KEY,
376             name TEXT NOT NULL,
377             normalized_name TEXT NOT NULL,
378             alignment TEXT NOT NULL,
379             is_isolationist TEXT NOT NULL,
380             alliance INTEGER,
381             colony_count INTEGER,
382             level INTEGER,
383             date_founded INTEGER,
384             affinity TEXT,
385             last_checked INTEGER
386             );
387              
388             DROP TABLE IF EXISTS cache;
389              
390             CREATE TABLE IF NOT EXISTS cache (
391             key TEXT NOT NULL PRIMARY KEY,
392             value TEXT NOT NULL,
393             valid_until INTEGER,
394             checksum TEXT NOT NULL
395             );
396              
397             CREATE TABLE IF NOT EXISTS meta (
398             key TEXT NOT NULL PRIMARY KEY,
399             value TEXT NOT NULL
400             );