support postgres as well as sqlite (for testing)
[id3fs.git] / lib / ID3FS / DB.pm
1 package ID3FS::DB;
2
3 use strict;
4 use warnings;
5 use DBI;
6 use ID3FS::File;
7
8 our $SCHEMA_VERSION=1;
9 my $dbfile=".id3fs";
10
11 sub new
12 {
13     my $proto=shift;
14     my $class=ref($proto) || $proto;
15     my $self={};
16     bless($self,$class);
17
18     my($dir, $init, $me)=@_;
19     $self->{dbpath}="$dir/$dbfile";
20     $self->{me}=$me;
21
22     my $exists=-f $self->{dbpath};
23
24     $self->{postgres}=0;
25
26     unless($self->{postgres})
27     {
28         die("$me: $self->{dbpath}: not found. use --init to create.\n") if(!$exists && !$init);
29         die("$me: --init used but $self->{dbpath} exists.\n")           if($exists && $init);
30     }
31
32     my $connectstr="dbi:SQLite:dbname=$self->{dbpath}";
33     my ($user, $pass)=("", "");
34     if($self->{postgres})
35     {
36         $connectstr="dbi:Pg:dbname=id3fs";
37         $user="ianb";
38         $pass="foo";
39     }
40     $self->{dbh}=DBI->connect($connectstr, $user, $pass,
41                               { AutoCommit=>1 } );
42     unless(defined($self->{dbh}))
43     {
44         die("$me: DB Error: " . $DBI::errstr . "\n");
45     }
46
47     if($init)
48     {
49         $self->create();
50     }
51     else
52     {
53         $self->checkschema();
54     }
55
56     return $self;
57 }
58
59 sub create
60 {
61     my($self,$name)=@_;
62     my @schema=split(/\n\n/,join("", <DATA>));
63     close(DATA);
64     for my $cmd (@schema)
65     {
66         $self->{dbh}->do($cmd);
67     }
68     if($self->{postgres})
69     {
70         $self->cmd("CREATE SEQUENCE seq");
71     }
72     $self->cmd("INSERT INTO id3fs (schema_version) VALUES (?)", $SCHEMA_VERSION);
73 }
74
75 sub checkschema
76 {
77     my $self=shift;
78     my ($version)=$self->cmd_onerow("SELECT schema_version from id3fs");
79     if(!defined($version) || $version != $SCHEMA_VERSION)
80     {
81         die("$self->{me}: id3fs database version " .
82             defined($version) ? $version : '""' .
83             "not known, current version is $SCHEMA_VERSION.\n");
84     }
85 }
86
87 sub cmd_sth
88 {
89     my($self, $sql, @params)=@_;
90     my $sth=$self->{dbh}->prepare($sql);
91     my $idx=1;
92     for my $param (@params)
93     {
94         $param="" unless(defined($param));
95         $sth->bind_param($idx++, $param);
96     }
97     $sth->execute();
98     return $sth;
99 }
100
101 sub tags
102 {
103     my($self, @constraints)=@_;
104     if(!@constraints) # /
105     {
106         my $sql="SELECT DISTINCT name FROM tags;";
107         my $tags=$self->cmd_rows($sql);
108         return(map { $_->[0]; } @$tags);
109     }
110     my @file_ids=();
111     my @tag_ids=();
112     my $main_sql_start=("SELECT DISTINCT tags.name FROM files\n" .
113                         "INNER JOIN files_x_tags ON files.id=files_x_tags.files_id\n" .
114                         "INNER JOIN tags ON tags.id=files_x_tags.tags_id\n" .
115                         "WHERE files.id in (" .
116                         ("\tSELECT DISTINCT files.id FROM files\n" .
117                          "\tINNER JOIN files_x_tags ON files.id=files_x_tags.files_id\n" .
118                          "\tINNER JOIN tags ON tags.id=files_x_tags.tags_id\n" .
119                          "\tWHERE tags.id in ("));
120     my $main_sql_mid=")\n) AND tags.id NOT IN (";
121     my $main_sql_end=")\n";
122     while(my $constraint=shift @constraints)
123     {
124         print "CONSTRAINT: $constraint->{name}\n";
125         my $cid=$constraint->{id};
126         push(@tag_ids, $cid);
127     }
128     my $sql = ($main_sql_start . join(", ", @tag_ids) .
129                $main_sql_mid   . join(", ", @tag_ids) .
130                $main_sql_end);
131     print "SQL: $sql\n";
132     my $result=$self->cmd_rows($sql);
133     my @tagnames=map { $_->[0]; } @$result;
134     print "SUBNAMES: ", join(', ', @tagnames), "\n";
135     return(@tagnames);
136 }
137
138 sub tag_values
139 {
140     my($self, $tag)=@_;
141     my $sql=("SELECT DISTINCT tagvals.name FROM tags\n" .
142              "INNER JOIN tags_x_tagvals ON tags.id=tags_x_tagvals.tags_id\n" .
143              "INNER JOIN tagvals ON tagvals.id=tags_x_tagvals.tagvals_id\n" .
144              "WHERE tags.name=?");
145     my $tags=$self->cmd_rows($sql, $tag);
146     return(map { $_->[0]; } @$tags);
147 }
148
149 sub tag_id
150 {
151     my($self, $tag)=@_;
152     my $sql='SELECT id FROM tags WHERE name=?';
153     my ($id)=$self->cmd_onerow($sql, $tag);
154     return($id);
155 }
156
157 sub add
158 {
159     my($self,$path)=@_;
160     my $file=ID3FS::File->new($path);
161     return unless(defined($file));
162     my $artist=$file->artist();
163     my $album=$file->album();
164     my $v1genre=$file->v1genre();
165     my $year=$file->year();
166     my $audiotype=$file->album();
167     my $tags=$file->tags();
168     my $haspic=$file->haspic();
169
170     my $file_id=$self->add_to_table("files", $path);
171     my $artists_id=$self->add_to_table("artists",  $artist);
172     my $albums_id=$self->add_to_table("albums",  $album);
173     for my $tag (keys %$tags)
174     {
175         $self->add_tag($file_id, $tag, $tags->{$tag});
176     }
177
178     if($self->ok($year))
179     {
180         $self->add_tag($file_id, "year", $year);
181         if($year=~/^(\d\d\d)\d$/)
182         {
183             $self->add_tag($file_id, "decade", "${1}0s");
184         }
185     }
186     if($self->ok($v1genre))
187     {
188         $self->add_tag($file_id, "v1genre", $v1genre);
189     }
190
191     if($haspic)
192     {
193         $self->add_tag($file_id, "haspic", undef);
194     }
195
196     $self->add_relation("files_x_artists",
197                         { "files_id" => $file_id,
198                           "artists_id" => $artists_id });
199
200     $self->add_relation("artists_x_albums",
201                       { "artists_id" => $artists_id,
202                         "albums_id" => $albums_id});
203 }
204
205 sub add_tag
206 {
207     my($self, $file_id, $tag, $val)=@_;
208     my $tag_id=$self->add_to_table("tags",  $tag);
209     $self->add_relation("files_x_tags",
210                         { "files_id" => $file_id,
211                           "tags_id"  => $tag_id });
212     if(defined($val))
213     {
214         my $val_id=$self->add_to_table("tagvals", $val);
215         $self->add_relation("tags_x_tagvals",
216                             { "tags_id"     => $tag_id,
217                               "tagvals_id"  => $val_id });
218     }
219 }
220
221 sub add_to_table
222 {
223     my($self, $table, $name, $extradata)=@_;
224     my $id=$self->lookup_id($table, $name);
225     unless(defined($id))
226     {
227         my $sql="INSERT INTO $table (";
228         $sql .= "id, " if($self->{postgres});
229         my @fields=qw(name);
230         if(defined($extradata))
231         {
232             push(@fields, sort keys(%$extradata));
233         }
234         $sql .= join(", ", @fields);
235         $sql .=") VALUES (";
236         $sql .=") nextval('seq'), " if($self->{postgres});
237         $sql .= join(", ", map { "?"; } @fields);
238         $sql .= ");";
239         $id=$self->cmd_id($sql, $name, map { $extradata->{$_} || ""; } sort keys %$extradata);
240     }
241     return $id;
242 }
243
244 sub add_relation
245 {
246     my ($self, $relname, $fields)=@_;
247     return if($self->relation_exists($relname, $fields));
248     my $sql="INSERT INTO $relname (";
249     $sql .= join(", ", sort keys(%$fields));
250     $sql .= ") VALUES (";
251     $sql .= join(", ", map { "?"; } sort keys(%$fields));
252     $sql .= ");";
253     $self->cmd($sql, map { $fields->{$_}; } sort keys(%$fields));
254 }
255
256 sub lookup_id
257 {
258     my($self, $table, $name)=@_;
259     my($id)=$self->cmd_onerow("SELECT id FROM $table where name=?", $name);
260     return $id;
261 }
262
263 sub relation_exists
264 {
265     my ($self, $relname, $fields)=@_;
266     my $sql="SELECT count(1) FROM $relname WHERE ";
267     my @exprs=();
268     my @vals=();
269     for my $field (keys %$fields)
270     {
271         push(@exprs,$field);
272         push(@vals,$fields->{$field});
273     }
274     $sql .= join(' AND ', map { "$_=?"; } @exprs);
275     my ($ret)=$self->cmd_onerow($sql, @vals);
276     return $ret;
277 }
278
279 sub ok
280 {
281     my($self, $thing)=@_;
282     return(defined($thing) && length($thing));
283 }
284
285 sub cmd
286 {
287     my ($self, @args)=@_;
288     # don't care about retcode
289     $self->cmd_sth(@args);
290 }
291
292 sub cmd_onerow
293 {
294     my ($self, @args)=@_;
295     my $sth=$self->cmd_sth(@args);
296     return($sth->fetchrow_array());
297 }
298
299 sub cmd_rows
300 {
301     my ($self, @args)=@_;
302     my $sth=$self->cmd_sth(@args);
303     return $sth->fetchall_arrayref();
304 }
305
306 sub cmd_id
307 {
308     my ($self, @args)=@_;
309     $self->cmd_sth(@args);
310     return($self->last_insert_id());
311 }
312
313 sub last_insert_id
314 {
315     my $self=shift;
316     if($self->{postgres})
317     {
318         return $self->{dbh}->last_insert_id(undef, undef, undef, undef,
319                                             { sequence => "seq" });
320     }
321     else
322     {
323         return $self->{dbh}->last_insert_id("","","","");
324     }
325 }
326
327 __DATA__
328
329 CREATE TABLE id3fs (
330     schema_version INTEGER
331 );
332
333 CREATE TABLE files (
334     id INTEGER PRIMARY KEY,
335     name text
336 );
337
338 CREATE TABLE artists (
339     id INTEGER PRIMARY KEY,
340     name text
341 );
342
343 CREATE TABLE albums (
344     id INTEGER PRIMARY KEY,
345     name text
346 );
347
348 CREATE TABLE tags (
349     id INTEGER PRIMARY KEY,
350     name text
351 );
352
353 CREATE TABLE tagvals (
354     id INTEGER PRIMARY KEY,
355     name text
356 );
357
358 CREATE TABLE files_x_tags (
359     files_id INTEGER,
360     tags_id INTEGER
361 );
362
363 CREATE TABLE tags_x_tagvals (
364     tags_id INTEGER,
365     tagvals_id INTEGER
366 );
367
368 CREATE TABLE files_x_artists (
369     files_id INTEGER,
370     artists_id INTEGER
371 );
372
373 CREATE TABLE artists_x_albums (
374     artists_id INTEGER,
375     albums_id INTEGER
376 );