id3fs-index: removed unused entries when reindexing
[id3fs.git] / lib / ID3FS / DB.pm
1 package ID3FS::DB;
2
3 use strict;
4 use warnings;
5 use DBI;
6 use ID3FS::AudioFile;
7 use Cwd;
8
9 our $SCHEMA_VERSION=1;
10 my $dbfile=".id3fs";
11
12 sub new
13 {
14     my $proto=shift;
15     my $class=ref($proto) || $proto;
16     my $self={};
17     bless($self,$class);
18
19     $self->{me}=shift;
20     $self->{dbpath}=shift;
21     $self->{base}=shift;
22     $self->{fallbackdir}=shift;
23
24     if(!defined($self->{base}) &&
25        defined($self->{fallbackdir}) &&
26        -d $self->{fallbackdir})
27     {
28         $self->{base}=$self->{fallbackdir};
29     }
30     $self->{dbpath}="$self->{base}/$dbfile" unless(defined($self->{dbpath}));
31     $self->{absbase}=Cwd::abs_path($self->{base});
32
33     my $connectstr="dbi:SQLite:dbname=$self->{dbpath}";
34     my ($user, $pass)=("", "");
35     if($self->{postgres})
36     {
37         $connectstr="dbi:Pg:dbname=id3fs";
38         $user="ianb";
39         $pass="foo";
40     }
41     my $exists=-f $self->{dbpath};
42     $self->{dbh}=DBI->connect($connectstr, $user, $pass,
43                               { AutoCommit=>1 } );
44     unless(defined($self->{dbh}))
45     {
46         die("$self->{me}: DB Error: " . $DBI::errstr . "\n");
47     }
48
49     if($exists)
50     {
51         $self->checkschema();
52     }
53     else
54     {
55         $self->create();
56     }
57     $self->enable_foreign_keys();
58     return $self;
59 }
60
61 sub create
62 {
63     my($self,$name)=@_;
64     my @schema=split(/\n\n/,join("", <DATA>));
65     close(DATA);
66     for my $cmd (@schema)
67     {
68         $self->{dbh}->do($cmd);
69     }
70     if($self->{postgres})
71     {
72         $self->cmd("CREATE SEQUENCE seq");
73     }
74     else
75     {
76         my %indexes=( "idx_files_id"  => "files (id)",
77                       "idx_fxt_both"  => "files_x_tags (files_id, tags_id)",
78                       "idx_fxt_files" => "files_x_tags (files_id)",
79                       "idx_fxt_tags"  => "files_x_tags (tags_id)",
80                       "idx_tags_id"   => "tags (id)",
81                       "idx_tags_name" => "tags (name)");
82         for my $index (keys %indexes)
83         {
84             $self->{dbh}->do("CREATE INDEX $index ON " . $indexes{$index});
85         }
86     }
87     $self->cmd("INSERT INTO id3fs (schema_version, last_update) VALUES (?, ?)",
88                $SCHEMA_VERSION, time());
89 }
90
91 sub checkschema
92 {
93     my $self=shift;
94     my ($version)=$self->cmd_onerow("SELECT schema_version from id3fs");
95     if(!defined($version) || $version != $SCHEMA_VERSION)
96     {
97         die("$self->{me}: id3fs database version " .
98             defined($version) ? $version : '""' .
99             "not known, current version is $SCHEMA_VERSION.\n");
100     }
101 }
102
103 sub enable_foreign_keys
104 {
105     my $self=shift;
106     $self->cmd("PRAGMA foreign_keys = ON");
107 }
108
109 sub last_update
110 {
111     my($self, $newval)=@_;
112     if(defined($newval))
113     {
114         $self->cmd("UPDATE id3fs SET last_update=?", $newval);
115     }
116     else
117     {
118         ($newval)=$self->cmd_onerow("SELECT last_update from id3fs");
119     }
120     return $newval;
121 }
122
123 sub cmd_sth
124 {
125     my($self, $sql, @params)=@_;
126     my $sth=$self->{dbh}->prepare($sql);
127     my $idx=1;
128     for my $param (@params)
129     {
130         $param="" unless(defined($param));
131         $sth->bind_param($idx++, $param);
132     }
133     $sth->execute();
134     return $sth;
135 }
136
137 sub tags
138 {
139     my($self, @constraints)=@_;
140     if(!@constraints) # /
141     {
142         # FIXME: add ALL?
143         my $sql="SELECT DISTINCT name FROM tags;";
144         my $tags=$self->cmd_rows($sql);
145         return(map { $_->[0]; } @$tags);
146     }
147     my @ids=();
148
149     my $main_sql_start=("SELECT t2.name\n" .
150                         "\tFROM (SELECT files_id FROM tags t1\n" .
151                         "\t\tINNER JOIN files_x_tags ON t1.id=files_x_tags.tags_id\n" .
152                         "\t\tWHERE t1.id in\n\t\t\t(");
153     my $main_sql_mid=(")\n\t\t) AS subselect\n" .
154                       "\tINNER JOIN files_x_tags ON subselect.files_id=files_x_tags.files_id\n" .
155                       "\tINNER JOIN tags t2 ON files_x_tags.tags_id=t2.id\n" .
156                       "\tWHERE t2.id NOT IN (");
157     my $main_sql_end=")\n\tGROUP BY t2.name;";
158     while(my $constraint=shift @constraints)
159     {
160         my $cid=$constraint->{id};
161         push(@ids, $cid);
162     }
163     @ids = map( { "\"$_\""; } grep { defined; } @ids) unless($self->{postgres});
164     my $tagstr=join(", ", @ids);
165     my $sql = ($main_sql_start . $tagstr .
166                $main_sql_mid   . $tagstr .
167                $main_sql_end);
168     print "SQL: $sql\n";
169     my $result=$self->cmd_rows($sql);
170     my @tagnames=map { $_->[0]; } @$result;
171     print "SUBNAMES: ", join(', ', @tagnames), "\n";
172     return(@tagnames);
173 }
174
175 sub tag_values
176 {
177     my($self, $tagid)=@_;
178     my $sql=("SELECT DISTINCT tagvals.name FROM tagvals\n" .
179              "INNER JOIN tags_x_tagvals ON tagvals.id=tags_x_tagvals.tagvals_id\n" .
180              "WHERE tags_x_tagvals.tags_id=?");
181     my $tags=$self->cmd_rows($sql, $tagid);
182     my @tags=map { $_->[0]; } @$tags;
183     @tags=map { length($_) ? $_ : "NOVALUE"; } @tags;
184     return @tags;
185 }
186
187 sub artists
188 {
189     my($self, @constraints)=@_;
190     if(!@constraints) # /ALL
191     {
192         my $sql="SELECT DISTINCT name FROM artists;";
193         my $tags=$self->cmd_rows($sql);
194         return(map { $_->[0]; } @$tags);
195     }
196     my @ids=();
197     my $main_sql_start=("SELECT artists.name\n" .
198                         "\tFROM (SELECT files_id FROM tags\n" .
199                         "\t\tINNER JOIN files_x_tags ON tags.id=files_x_tags.tags_id\n" .
200                         "\t\tWHERE tags.id in\n\t\t\t(");
201     my $main_sql_end=(")\n\t\t) AS subselect\n" .
202                       "\tINNER JOIN files ON subselect.files_id=files.id\n" .
203                       "\tINNER JOIN artists ON files.artists_id=artists.id\n" .
204                       "\n\tGROUP BY artists.name;");
205     while(my $constraint=shift @constraints)
206     {
207         my $cid=$constraint->{id};
208         push(@ids, $cid);
209     }
210     @ids = map( { "\"$_\""; } grep { defined; } @ids) unless($self->{postgres});
211     my $tagstr=join(", ", @ids);
212     my $sql = ($main_sql_start . $tagstr .
213                $main_sql_end);
214     print "SQL: $sql\n";
215     my $result=$self->cmd_rows($sql);
216     my @tagnames=map { $_->[0]; } @$result;
217     print "ARTISTS: ", join(', ', @tagnames), "\n";
218     return(@tagnames);
219 }
220
221 sub albums
222 {
223     my($self, @constraints)=@_;
224     my @ids=();
225     # FIXME: rework PathElements
226     if(ref($constraints[$#constraints]) eq "ID3FS::PathElement::Artist")
227     {
228         return $self->artist_albums($constraints[$#constraints]->{id});
229     }
230     my $main_sql_start=("SELECT albums.name\n" .
231                         "\tFROM (SELECT files_id FROM tags\n" .
232                         "\t\tINNER JOIN files_x_tags ON tags.id=files_x_tags.tags_id\n" .
233                         "\t\tWHERE tags.id in\n\t\t\t(");
234     my $main_sql_end=(")\n\t\t) AS subselect\n" .
235                       "\tINNER JOIN files ON subselect.files_id=files.id\n" .
236                       "\tINNER JOIN albums ON files.albums_id=albums.id\n" .
237                       "\n\tGROUP BY albums.name;");
238     while(my $constraint=shift @constraints)
239     {
240         my $cid=$constraint->{id};
241         push(@ids, $cid);
242     }
243     @ids = map( { "\"$_\""; } grep { defined; } @ids) unless($self->{postgres});
244     my $str=join(", ", @ids);
245     my $sql = ($main_sql_start . $str .
246                $main_sql_end);
247     my $result=$self->cmd_rows($sql);
248     my @names=map { $_->[0]; } @$result;
249     print "ALBUMS: ", join(', ', @names), "\n";
250     return(@names);
251 }
252
253 sub artist_albums
254 {
255     my($self, $artist_id)=@_;
256     my $sql=("SELECT albums.name FROM files\n\t" .
257              "INNER JOIN albums ON albums.id=files.albums_id\n\t" .
258              "INNER JOIN artists ON artists.id=files.artists_id\n\t" .
259              "WHERE artists.id=? and albums.name <> ''\n\t" .
260              "GROUP BY albums.name\n");
261     print "ARTIST_ALBUMS SQL: $sql\n";
262     my $result=$self->cmd_rows($sql, $artist_id);
263     my @albums=map { $_->[0]; } @$result;
264     print "ALBUMS: ", join(', ', @albums), "\n";
265     return(@albums);
266 }
267
268 sub artist_tracks
269 {
270     my($self, $artist_id)=@_;
271     my $sql=("SELECT files.name FROM files\n\t" .
272              "INNER JOIN artists ON artists.id=files.artists_id\n\t" .
273              "INNER JOIN albums  ON albums.id=files.albums_id\n\t" .
274              "WHERE artists.id=? AND albums.name=''\n\t" .
275              "GROUP BY files.name\n");
276     print "ARTIST_TRACKS SQL: $sql\n";
277     my $result=$self->cmd_rows($sql, $artist_id);
278     my @names=map { $_->[0]; } @$result;
279     print "ARTISTTRACKS: ", join(', ', @names), "\n";
280     return(@names);
281 }
282
283 sub album_tracks
284 {
285     my($self, $artist_id, $album_id)=@_;
286     my $sql=("SELECT files.name FROM files\n\t" .
287              "INNER JOIN albums  ON albums.id=files.albums_id\n\t" .
288              "INNER JOIN artists ON artists.id=files.artists_id\n\t" .
289              "WHERE artists.id=? AND albums.id=?\n\t" .
290              "GROUP BY files.name\n");
291     print "ALBUM_TRACKS SQL($artist_id, $album_id): $sql\n";
292     my $result=$self->cmd_rows($sql, $artist_id, $album_id);
293     my @names=map { $_->[0]; } @$result;
294     print "TRACKS: ", join(', ', @names), "\n";
295     return(@names);
296 }
297
298 sub tracks
299 {
300     my($self, @constraints)=@_;
301     # FIXME: rework PathElements
302     if(ref($constraints[$#constraints]) eq "ID3FS::PathElement::Artist")
303     {
304         return $self->artist_tracks($constraints[$#constraints]->{id});
305     }
306     elsif(ref($constraints[$#constraints]) eq "ID3FS::PathElement::Album")
307     {
308         my $artist_id=0;
309         my $artist=$constraints[($#constraints)-1];
310         if(defined($artist) && (ref($artist) eq "ID3FS::PathElement::Artist"))
311         {
312             # should always happen
313             $artist_id=$artist->{id};
314         }
315         return $self->album_tracks($artist_id, $constraints[$#constraints]->{id});
316     }
317
318     my $main_sql_start=("SELECT files.name\n" .
319                         "\tFROM (SELECT files_id FROM tags\n" .
320                         "\t\tINNER JOIN files_x_tags ON tags.id=files_x_tags.tags_id\n" .
321                         "\t\tWHERE tags.id in\n\t\t\t(");
322     my $main_sql_end=(")\n\t\t) AS subselect\n" .
323                       "\tINNER JOIN files ON files.id=subselect.files_id" .
324                       "\tGROUP BY files.name;");
325     my @ids;
326     while(my $constraint=shift @constraints)
327     {
328         my $cid=$constraint->{id};
329         push(@ids, $cid);
330     }
331     @ids = map( { "\"$_\""; } grep { defined; } @ids) unless($self->{postgres});
332     my $str=join(", ", @ids);
333     my $sql = ($main_sql_start . $str .
334                $main_sql_end);
335     print "SQL: $sql\n";
336     my $result=$self->cmd_rows($sql);
337     my @names=map { $_->[0]; } @$result;
338     print "TRACKS: ", join(', ', @names), "\n";
339     return(@names);
340 }
341
342 sub filename
343 {
344     my($self, @constraints)=@_;
345     if(ref($constraints[$#constraints]) eq "ID3FS::PathElement::File")
346     {
347         my $id=$constraints[$#constraints]->{id};
348         my $sql=("SELECT paths.name, files.name FROM files\n" .
349                  "INNER JOIN paths ON files.paths_id=paths.id\n" .
350                  "WHERE files.id=?\n" .
351                  "GROUP BY paths.name, files.name");
352         print "FILENAME SQL: $sql\n";
353         my ($path, $name)=$self->cmd_onerow($sql, $id);
354         return($self->{absbase} . "/$path/$name");
355     }
356     die("DB::filename: unhandled case\n"); #FIXME
357 }
358
359 sub bare_tags
360 {
361     my($self)=@_;
362     my $sql=("SELECT tags.name FROM tags\n" .
363              "LEFT JOIN tags_x_tagvals ON tags.id=tags_x_tagvals.tags_id\n" .
364              "WHERE tags_x_tagvals.tags_id IS NULL\n" .
365              "GROUP BY tags.name\n");
366     my $result=$self->cmd_rows($sql);
367     my @names=map { $_->[0]; } @$result;
368     return (@names);
369 }
370
371 sub tags_with_values
372 {
373     my($self)=@_;
374     my $sql=("SELECT tags.name, tagvals.name FROM tags\n" .
375              "INNER JOIN tags_x_tagvals ON tags.id=tags_x_tagvals.tags_id\n" .
376              "INNER JOIN tagvals ON tagvals.id=tags_x_tagvals.tagvals_id\n" .
377              "GROUP BY tags.name, tagvals.name\n");
378     my $result=$self->cmd_rows($sql);
379     my $tags={};
380     for my $pair (@$result)
381     {
382         push(@{$tags->{$pair->[0]}}, $pair->[1]);
383     }
384     return $tags;
385 }
386
387 sub id
388 {
389     my($self, $type, $val)=@_;
390     my $sql="SELECT id FROM $type WHERE name=?";
391     my ($id)=$self->cmd_onerow($sql, $val);
392     return($id);
393 }
394
395 sub add
396 {
397     my($self,$path)=@_;
398     my $relpath=$path;
399     $relpath =~ s/^\Q$self->{base}\E\/?//;
400     my($filepart,$pathpart);
401     if($path !~ /\//)
402     {
403         $pathpart='';
404         $filepart=$relpath;
405     }
406     else
407     {
408         ($pathpart, $filepart) = ($relpath =~ /(.*)\/(.*)/);
409     }
410     my $file=ID3FS::AudioFile->new($path);
411     return unless(defined($file));
412     my $artist=$file->artist();
413     my $album=$file->album();
414     my $v1genre=$file->v1genre();
415     my $year=$file->year();
416     my $audiotype=$file->audiotype();
417     my $tags=$file->tags();
418     my $haspic=$file->haspic();
419
420     $artist=undef unless($self->ok($artist));
421     my $artist_id=$self->add_to_table("artists",  $artist);
422     my $path_id=$self->add_to_table("paths", $pathpart);
423     $album=undef unless($self->ok($album));
424     my $albums_id=$self->add_to_table("albums", $album);
425     my $file_id=$self->add_to_table("files", $filepart,
426                                     { "artists_id" => $artist_id,
427                                       "albums_id"  => $albums_id,
428                                       "paths_id"   => $path_id });
429     for my $tag (keys %$tags)
430     {
431         $self->add_tag($file_id, $tag, $tags->{$tag});
432     }
433
434     if($self->ok($year))
435     {
436         $self->add_tag($file_id, "year", $year);
437         if($year=~/^(\d\d\d)\d$/)
438         {
439             $self->add_tag($file_id, "decade", "${1}0s");
440         }
441     }
442
443     if($self->ok($v1genre))
444     {
445         $self->add_tag($file_id, "v1genre", $v1genre);
446     }
447
448     if($haspic)
449     {
450         $self->add_tag($file_id, "haspic", undef);
451     }
452 }
453
454 sub add_tag
455 {
456     my($self, $file_id, $tag, $val)=@_;
457     my $tag_id=$self->add_to_table("tags",  $tag);
458     $self->add_relation("files_x_tags",
459                         { "files_id" => $file_id,
460                           "tags_id"  => $tag_id });
461     if(defined($val))
462     {
463         my $val_id=$self->add_to_table("tagvals", $val);
464         $self->add_relation("tags_x_tagvals",
465                             { "tags_id"     => $tag_id,
466                               "tagvals_id"  => $val_id });
467     }
468 }
469
470 sub add_to_table
471 {
472     my($self, $table, $name, $extradata)=@_;
473     my $id=$self->lookup_id($table, $name);
474     unless(defined($id))
475     {
476         my $sql="INSERT INTO $table (";
477         $sql .= "id, " if($self->{postgres});
478         my @fields=qw(name);
479         if(defined($extradata))
480         {
481             push(@fields, sort keys(%$extradata));
482         }
483         $sql .= join(", ", @fields);
484         $sql .=") VALUES (";
485         $sql .=") nextval('seq'), " if($self->{postgres});
486         $sql .= join(", ", map { "?"; } @fields);
487         $sql .= ");";
488         $id=$self->cmd_id($sql, $name, map { $extradata->{$_} || ""; } sort keys %$extradata);
489     }
490     return $id;
491 }
492
493 sub add_relation
494 {
495     my ($self, $relname, $fields)=@_;
496     return if($self->relation_exists($relname, $fields));
497     my $sql="INSERT INTO $relname (";
498     $sql .= join(", ", sort keys(%$fields));
499     $sql .= ") VALUES (";
500     $sql .= join(", ", map { "?"; } sort keys(%$fields));
501     $sql .= ");";
502     $self->cmd($sql, map { $fields->{$_}; } sort keys(%$fields));
503 }
504
505 sub lookup_id
506 {
507     my($self, $table, $name)=@_;
508     my($id)=$self->cmd_onerow("SELECT id FROM $table where name=?", $name);
509     return $id;
510 }
511
512 sub tag_has_values
513 {
514     my($self, $id)=@_;
515     my $sql=("SELECT COUNT(*) FROM tags\n\t" .
516              "INNER JOIN tags_x_tagvals ON tags.id=tags_x_tagvals.tags_id\n\t" .
517              "INNER JOIN tagvals ON tagvals.id=tags_x_tagvals.tagvals_id\n\t" .
518              "WHERE tags.id=?\n");
519     my ($rows)=$self->cmd_onerow($sql, $id);
520     return $rows;
521 }
522
523 sub files_in
524 {
525     my ($self, $dir)=@_;
526     $dir=~s/^$self->{base}\/?//;
527     print "Munged dir: $dir\n";
528     my $sql=("SELECT files.name FROM files\n" .
529              "INNER JOIN paths ON files.paths_id=paths.id\n" .
530              "WHERE paths.name=?\n");
531     my $files=$self->cmd_rows($sql, $dir);
532     return(map { $_->[0]; } @$files);
533 }
534
535 sub prune_directories
536 {
537     my($self)=@_;
538     my $sql=("SELECT name, id FROM paths ORDER BY name\n");
539     my $pathsref=$self->cmd_rows($sql);
540     my @ids=();
541     for my $pathpair (@$pathsref)
542     {
543         my($path, $id)=@$pathpair;
544         my $fullpath="$self->{absbase}/$path";
545         print "PRUNING PATH $fullpath: ";
546         unless(-d $fullpath)
547         {
548             push(@ids, $id)
549         }
550     }
551     $self->prune_paths(@ids);
552     return scalar(@ids);
553 }
554
555 sub prune_paths
556 {
557     my($self, @ids)=@_;
558     my $sql=("DELETE FROM files WHERE paths_id IN (\n\t" .
559              join(', ', map { "\"$_\""; } @ids). "\n\t)");
560     print "SQL: \n", $sql, "\n";
561     $self->cmd($sql);
562 }
563
564 sub remove_unused
565 {
566     my($self)=@_;
567     my $sql=<<'EOT';
568    DELETE FROM artists WHERE id IN (
569        SELECT artists.id FROM artists
570        LEFT JOIN files ON files.artists_id=artists.id
571        WHERE files.id IS NULL);
572
573    DELETE FROM albums WHERE id IN (
574        SELECT albums.id FROM albums
575        LEFT JOIN files ON files.albums_id=albums.id
576        WHERE files.id IS NULL);
577
578    DELETE FROM paths WHERE id IN (
579        SELECT paths.id FROM paths
580        LEFT JOIN files ON files.paths_id=paths.id
581        WHERE files.id IS NULL);
582
583    DELETE FROM files_x_tags WHERE files_id IN (
584        SELECT files_x_tags.files_id FROM files_x_tags
585        LEFT JOIN files ON files.id=files_x_tags.files_id
586        WHERE files.id IS NULL);
587
588    DELETE FROM tags WHERE id IN (
589        SELECT tags.id FROM tags
590        LEFT JOIN files_x_tags ON files_x_tags.tags_id=tags.id
591        WHERE files_x_tags.files_id IS NULL);
592
593    DELETE FROM tags_x_tagvals WHERE tags_id IN (
594        SELECT tags_x_tagvals.tags_id FROM tags_x_tagvals
595        LEFT JOIN tags ON tags.id=tags_x_tagvals.tags_id
596        WHERE tags.id IS NULL);
597
598    DELETE FROM tagvals WHERE id IN (
599        SELECT tagvals.id FROM tagvals
600        LEFT JOIN tags_x_tagvals ON tags_x_tagvals.tagvals_id=tagvals.id
601        WHERE tags_x_tagvals.tagvals_id IS NULL);
602 EOT
603     print "SQL: $sql\n";
604     my @sql=split(/\n\n/, $sql);
605     $self->cmd($_) for (@sql);
606 }
607
608 sub relation_exists
609 {
610     my ($self, $relname, $fields)=@_;
611     my $sql="SELECT count(1) FROM $relname WHERE ";
612     my @exprs=();
613     my @vals=();
614     for my $field (keys %$fields)
615     {
616         push(@exprs,$field);
617         push(@vals,$fields->{$field});
618     }
619     $sql .= join(' AND ', map { "$_=?"; } @exprs);
620     my ($ret)=$self->cmd_onerow($sql, @vals);
621     return $ret;
622 }
623
624 sub ok
625 {
626     my($self, $thing)=@_;
627     return(defined($thing) && length($thing) && $thing =~ /\S+/);
628 }
629
630 sub cmd
631 {
632     my ($self, @args)=@_;
633     # don't care about retcode
634     $self->cmd_sth(@args);
635 }
636
637 sub cmd_onerow
638 {
639     my ($self, @args)=@_;
640     my $sth=$self->cmd_sth(@args);
641     return($sth->fetchrow_array());
642 }
643
644 sub cmd_rows
645 {
646     my ($self, @args)=@_;
647     my $sth=$self->cmd_sth(@args);
648     return $sth->fetchall_arrayref();
649 }
650
651 sub cmd_id
652 {
653     my ($self, @args)=@_;
654     $self->cmd_sth(@args);
655     return($self->last_insert_id());
656 }
657
658 sub last_insert_id
659 {
660     my $self=shift;
661     if($self->{postgres})
662     {
663         return $self->{dbh}->last_insert_id(undef, undef, undef, undef,
664                                             { sequence => "seq" });
665     }
666     else
667     {
668         return $self->{dbh}->last_insert_id("","","","");
669     }
670 }
671
672 __DATA__
673
674 CREATE TABLE id3fs (
675     schema_version INTEGER,
676     last_update
677 );
678
679 CREATE TABLE paths (
680     id INTEGER PRIMARY KEY,
681     name text
682 );
683
684 CREATE TABLE artists (
685     id INTEGER PRIMARY KEY,
686     name text
687 );
688
689 CREATE TABLE albums (
690     id INTEGER PRIMARY KEY,
691     name text
692 );
693
694 CREATE TABLE files (
695     id INTEGER PRIMARY KEY,
696     name text,
697     artists_id,
698     albums_id,
699     paths_id,
700     FOREIGN KEY(artists_id) REFERENCES artists(id) ON DELETE CASCADE ON UPDATE CASCADE,
701     FOREIGN KEY(albums_id)  REFERENCES albums(id)  ON DELETE CASCADE ON UPDATE CASCADE,
702     FOREIGN KEY(paths_id)   REFERENCES paths(id)   ON DELETE CASCADE ON UPDATE CASCADE
703 );
704
705 CREATE TABLE tags (
706     id INTEGER PRIMARY KEY,
707     name text
708 );
709
710 CREATE TABLE tagvals (
711     id INTEGER PRIMARY KEY,
712     name text
713 );
714
715 CREATE TABLE files_x_tags (
716     files_id INTEGER,
717     tags_id INTEGER,
718     FOREIGN KEY(files_id) REFERENCES files(id) ON DELETE CASCADE ON UPDATE CASCADE,
719     FOREIGN KEY(tags_id)  REFERENCES tags(id)  ON DELETE CASCADE ON UPDATE CASCADE
720 );
721
722 CREATE TABLE tags_x_tagvals (
723     tags_id INTEGER,
724     tagvals_id INTEGER,
725     FOREIGN KEY(tags_id) REFERENCES tags(id) ON DELETE CASCADE ON UPDATE CASCADE,
726     FOREIGN KEY(tagvals_id) REFERENCES tagvals(id) ON DELETE CASCADE ON UPDATE CASCADE
727 );
728