File Coverage

lib/Data/TagDB.pm
Criterion Covered Total %
statement 41 465 8.8
branch 0 174 0.0
condition 0 99 0.0
subroutine 14 58 24.1
pod 21 23 91.3
total 76 819 9.2


line stmt bran cond sub pod time code
1             # Copyright (c) 2024-2025 Philipp Schafft
2              
3             # licensed under Artistic License 2.0 (see LICENSE file)
4              
5             # ABSTRACT: Work with Tag databases
6              
7             package Data::TagDB;
8              
9 1     1   314494 use v5.10;
  1         3  
10 1     1   5 use strict;
  1         2  
  1         23  
11 1     1   3 use warnings;
  1         2  
  1         48  
12              
13 1     1   3 use Scalar::Util qw(weaken blessed);
  1         1  
  1         58  
14              
15 1     1   4 use Carp;
  1         1  
  1         55  
16 1     1   2113 use DBI;
  1         15166  
  1         65  
17              
18 1     1   550 use Data::TagDB::Tag;
  1         7  
  1         57  
19 1     1   647 use Data::TagDB::Relation;
  1         3  
  1         46  
20 1     1   540 use Data::TagDB::Metadata;
  1         2  
  1         76  
21 1     1   529 use Data::TagDB::LinkIterator;
  1         4  
  1         44  
22 1     1   620 use Data::TagDB::MultiIterator;
  1         4  
  1         126  
23 1     1   669 use Data::TagDB::WellKnown;
  1         4  
  1         52  
24 1     1   637 use Data::TagDB::Cloudlet;
  1         3  
  1         43  
25 1     1   882 use Data::URIID::Colour;
  1         3540  
  1         8742  
26              
27             our $VERSION = v0.12;
28              
29             my %_queries = (
30             _default => {
31             tag_by_hint => 'SELECT tag FROM hint WHERE name = ?',
32             _tag_simple_identifier => 'SELECT data FROM metadata WHERE relation = (SELECT tag FROM hint WHERE name = \'also-shares-identifier\') AND type = (SELECT tag FROM hint WHERE name = ?) AND context = 0 AND encoding = 0 AND tag = ? ORDER BY data DESC',
33             _tag_by_dbid_type_and_data => 'SELECT tag FROM metadata WHERE relation = (SELECT tag FROM hint WHERE name = \'also-shares-identifier\') AND type = ? AND context = 0 AND encoding = 0 AND data = ?',
34             _create_tag => 'INSERT INTO tag DEFAULT VALUES',
35             _create_metadata => 'INSERT OR IGNORE INTO metadata (tag,relation,context,type,encoding,data) VALUES (?,?,?,?,?,?)',
36             _create_relation => 'INSERT OR IGNORE INTO relation (tag,relation,related,context,filter) VALUES (?,?,?,?,?)',
37             },
38             Pg => {
39             _create_tag => 'INSERT INTO tag DEFAULT VALUES RETURNING id',
40             _create_metadata => 'INSERT INTO metadata (tag,relation,context,type,encoding,data) VALUES (?,?,?,?,?,?) ON CONFLICT DO NOTHING',
41             _create_relation => 'INSERT INTO relation (tag,relation,related,context,filter) VALUES (?,?,?,?,?) ON CONFLICT DO NOTHING',
42             },
43             );
44              
45              
46              
47             sub new {
48 0     0 1   my ($pkg, $first, @rest) = @_;
49 0           my $DBI_name;
50             my $dbh;
51 0           my %query;
52              
53 0 0         croak 'No dsn or dbh given to new' unless defined $first;
54              
55 0 0 0       if (scalar(@rest) == 0 && eval { $first->can('prepare'); }) {
  0            
56 0           $dbh = $first;
57             } else {
58 0 0         $dbh = DBI->connect($first, @rest) or croak 'Cannot connect to database';
59             }
60              
61 0           $DBI_name = $dbh->{Driver}{Name};
62 0           foreach my $name (keys %{$_queries{_default}}) {
  0            
63 0   0       $query{$name} = $dbh->prepare($_queries{$DBI_name}{$name} // $_queries{_default}{$name});
64             }
65              
66 0           return bless {
67             dbh => $dbh,
68             _DBI_name => $DBI_name,
69             cache_tag => {},
70             cache_ise => {},
71             cache_default_type => {},
72             cache_default_encoding => {},
73             backup_type => {},
74             query => \%query,
75             }, $pkg;
76             }
77              
78              
79             sub dbh {
80 0     0 1   my ($self) = @_;
81 0           return $self->{dbh};
82             }
83              
84              
85             sub disconnect {
86 0     0 1   my ($self) = @_;
87 0           $self->assert_connected->disconnect;
88 0           $self->{dbh} = undef;
89 0           $self->_cache_clear;
90             }
91              
92              
93             sub tag_by_id {
94 0     0 1   my ($self, $type, $id, $autocreate) = @_;
95              
96             # Shift arguments into correct order as needed:
97 0 0 0       if (blessed($type) && $type->isa('Data::Identifier')) {
98 0           ($self, $id, $autocreate) = @_;
99              
100 0           $type = $id->type;
101              
102             # Is $type === UUID?
103             # TODO: Make this a better check.
104 0 0         if ($type->eq('uuid')) {
105 0           $type = $self->tag_by_hint('uuid');
106             } else {
107 0           $type = $self->tag_by_id($type);
108             }
109              
110 0           $id = $id->id;
111             }
112              
113 0 0         $type = $self->tag_by_hint($type) unless eval { $type->isa('Data::TagDB::Tag') };
  0            
114              
115 0 0         if ($autocreate) {
116 0           return $self->create_tag([$type => $id]);
117             } else {
118 0           return $self->tag_by_dbid($self->_get_data(_tag_by_dbid_type_and_data => ($type->dbid, $id)));
119             }
120             }
121              
122              
123             sub tag_by_specification {
124 0     0 1   my ($self, $specification, %opts) = @_;
125 0           my $wk = $self->wk;
126 0           my $style = $opts{style};
127 0           my $important = $opts{important};
128 0           my $role = $opts{role};
129 0           my @candidates;
130              
131 0 0 0       croak 'No style given' unless defined($style) && length($style);
132              
133 0 0         if ($style eq 'ise') {
    0          
    0          
134 0   0       @candidates = (eval { $self->tag_by_id(uuid => $specification) } // eval { $self->tag_by_id(oid => $specification) } // eval { $self->tag_by_id(uri => $specification) });
  0   0        
  0            
  0            
135             } elsif ($style eq 'tagpool') {
136 0 0         unless ($opts{as_is}) {
137 0   0       $important ||= $specification =~ s/\!$//;
138              
139 0 0 0       if (!defined($role) && $specification =~ s/^(.+)\@([^@]+)$/$2/) {
140 0           $role = $self->tag_by_specification($1, %opts);
141             }
142             }
143              
144 0 0         if ($specification =~ /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/) {
145 0           return $self->tag_by_id(uuid => $specification);
146             }
147              
148 0           @candidates = $self->metadata(
149             relation => $wk->also_shares_identifier,
150             type => $wk->tagname,
151             encoding => undef,
152             data_raw => $specification,
153             )->collect('tag');
154             } elsif ($style eq 'sirtx') {
155 0           my ($type, $id);
156 0           my $backup_type;
157              
158 0           $specification =~ s/^\[(.+)\]$/$1/;
159              
160 0 0         if ($specification =~ /^\/([0-9]+)$/) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
161 0           $id = $1;
162 0           $type = $wk->sirtx_function_number;
163             } elsif ($specification =~ /^\/([a-z_]+)$/) {
164 0           $id = $1;
165 0           $type = $wk->sirtx_function_name;
166             } elsif ($specification =~ /^\*([0-9]+)$/) {
167 0           @candidates = ($opts{sirtx_local_ids}{int $1});
168             } elsif ($specification eq '*') {
169 0           @candidates = ($opts{sirtx_local_ids}{0});
170             } elsif ($specification =~ /^\'([0-9]+)$/) {
171 0           my $num = int($1);
172 0           require Data::Identifier::Generate;
173 0           my $id = Data::Identifier::Generate->integer($num);
174 0           @candidates = ($self->tag_by_id($id));
175             } elsif ($specification eq '\'') {
176 0           @candidates = ($wk->zero);
177             } elsif ($specification =~ /^[\&\%]([0-9a-zA-Z_]+)$/) {
178 0           my $port_tag = $self->tag_by_specification($1, %opts);
179 0           my $ports = $opts{sirtx_ports};
180 0           my $len = scalar(@{$ports});
  0            
181              
182 0   0       for (my $i = 0; !scalar(@candidates) && $i < $len; $i += 2) {
183 0           my $p = $ports->[$i];
184 0 0 0       if ($port_tag == $p || $port_tag->dbid eq $p->dbid) {
185 0           @candidates = ($ports->[$i+1]);
186             }
187             }
188             } elsif ($specification =~ /^(.+):(.+)$/) {
189 0           ($type, $id) = ($1, $2);
190             } else {
191 0           $type = $wk->sirtx_logical;
192 0           $backup_type = $wk->sirtx_function_name;
193 0           $id = $specification;
194             }
195              
196 0 0         if (defined $type) {
197 0 0         unless (ref $type) {
198 0           $type = $self->tag_by_specification($type, %opts);
199             }
200              
201 0           @candidates = $self->metadata(
202             relation => $wk->also_shares_identifier,
203             type => $type,
204             encoding => undef,
205             data_raw => $id,
206             )->collect('tag');
207              
208 0 0 0       if (scalar(@candidates) == 0 && defined($backup_type)) {
209 0           @candidates = $self->metadata(
210             relation => $wk->also_shares_identifier,
211             type => $backup_type,
212             encoding => undef,
213             data_raw => $id,
214             )->collect('tag');
215             }
216             }
217             } else {
218 0           croak 'Invalid/unsupported style: '.$style;
219             }
220              
221 0 0         if ($important) {
222 0           @candidates = $self->relation(
223             tag => \@candidates,
224             relation => $wk->flagged_as,
225             related => $wk->important,
226             )->collect('tag');
227             }
228              
229 0 0         if (defined $role) {
230             @candidates = grep {
231 0           $_->cloudlet('roles')->is_entry($role)
  0            
232             } @candidates;
233             }
234              
235 0 0         if (scalar(@candidates) == 1) {
    0          
236 0           return $candidates[0];
237             } elsif (scalar(@candidates) > 1) {
238 0           croak 'Nore than one match found';
239             } else {
240 0           croak 'Tag not found';
241             }
242              
243 0           die 'BUG';
244             }
245              
246              
247             sub relation {
248 0     0 1   my ($self, %opts) = @_;
249 0           return $self->_link_iterator(%opts, package => 'Data::TagDB::Relation');
250             }
251              
252              
253             sub metadata {
254 0     0 1   my ($self, %opts) = @_;
255 0           return $self->_link_iterator(%opts, package => 'Data::TagDB::Metadata');
256             }
257              
258              
259             sub link {
260 0     0 1   my ($self, %opts) = @_;
261 0           return Data::TagDB::MultiIterator->new(db => $self, iterators => [
262             $self->metadata(%opts),
263             $self->relation(%opts),
264             ]);
265             }
266              
267              
268             sub wk {
269 0     0 1   my ($self) = @_;
270 0   0       return $self->{wk} //= Data::TagDB::WellKnown->_new(db => $self);
271             }
272              
273              
274             sub register_decoder {
275 0     0 1   my ($self, $type, $encoding, $decoder) = @_;
276 0   0       my $decoders = $self->{decoders} //= $self->_register_basic_decoders;
277 0   0       $decoders->{$type->dbid} //= {};
278 0           $decoders->{$type->dbid}{$encoding->dbid} = $decoder;
279             }
280              
281              
282             sub create_tag {
283 0     0 1   my ($self, $ids, $addional_ids) = @_;
284 0           my $asi = $self->wk->also_shares_identifier;
285 0           my $asi_dbid = $asi->dbid;
286 0           my $query_tpl = 'SELECT tag FROM metadata WHERE relation = '.$asi_dbid.' AND type = ? AND context = 0 AND encoding = 0 AND data = ?';
287 0           my $query = '';
288 0           my @bind;
289             my $row;
290 0           my $tag;
291              
292 0 0 0       if (blessed($ids) && $ids->isa('Data::Identifier')) {
293 0           $ids = [$self->tag_by_id($ids->type) => $ids->id];
294             }
295              
296 0 0 0       if (blessed($addional_ids) && $addional_ids->isa('Data::Identifier')) {
297 0           $addional_ids = [$self->tag_by_id($addional_ids->type) => $addional_ids->id];
298             }
299              
300 0           for (my $i = 0; $i < scalar(@{$ids}); $i += 2) {
  0            
301 0           my $type = $ids->[$i + 0];
302 0           my $value = $ids->[$i + 1];
303              
304 0 0         next unless defined $value;
305              
306 0 0         $query .= ' UNION ' if length $query;
307              
308 0           $query .= $query_tpl;
309 0           push(@bind, $type->dbid, $value);
310             }
311              
312 0           $query = $self->dbh->prepare($query);
313 0           $query->execute(@bind);
314 0           $row = $query->fetchrow_arrayref;
315 0           $query->finish;
316              
317 0 0 0       if (defined($row) && defined($row->[0]) && $row->[0] > 0) {
      0        
318 0           $tag = $self->tag_by_dbid($row->[0]);
319             } else {
320 0           $query = $self->_query('_create_tag');
321 0           $query->execute;
322 0 0         if ($self->_DBI_name eq 'Pg') {
323 0           my $row = $query->fetchrow_arrayref;
324 0           $tag = $self->tag_by_dbid($row->[0]);
325             } else {
326 0           $tag = $self->tag_by_dbid($query->last_insert_id);
327             }
328 0           $query->finish;
329             }
330              
331 0           for (my $i = 0; $i < scalar(@{$ids}); $i += 2) {
  0            
332 0           my $type = $ids->[$i + 0];
333 0           my $value = $ids->[$i + 1];
334              
335 0           $self->create_metadata(tag => $tag, relation => $asi, type => $type, data_raw => $value);
336             }
337              
338 0 0         if (defined $addional_ids) {
339 0           for (my $i = 0; $i < scalar(@{$addional_ids}); $i += 2) {
  0            
340 0           my $type = $addional_ids->[$i + 0];
341 0           my $value = $addional_ids->[$i + 1];
342              
343 0 0         next unless defined $value;
344              
345 0           $self->create_metadata(tag => $tag, relation => $asi, type => $type, data_raw => $value);
346             }
347             }
348              
349 0           return $tag;
350             }
351              
352              
353             sub create_metadata {
354 0     0 1   my ($self, %opts) = @_;
355 0           my $query = $self->_query('_create_metadata');
356             my @bind = (
357             $self->_as_tag($opts{tag}, 1)->dbid,
358             $self->_as_tag($opts{relation}, 1)->dbid,
359             Data::TagDB::Tag::dbid($self->_as_tag($opts{context}, 1)),
360             Data::TagDB::Tag::dbid($self->_as_tag($opts{type}, 1)),
361             Data::TagDB::Tag::dbid($self->_as_tag($opts{encoding}, 1)),
362             $opts{data_raw},
363 0           );
364              
365 0           $query->execute(@bind);
366 0           $query->finish;
367              
368 0           return Data::TagDB::Metadata->_new(%opts, db => $self);
369             }
370              
371              
372             sub create_relation {
373 0     0 1   my ($self, %opts) = @_;
374 0           my $query = $self->_query('_create_relation');
375             my @bind = (
376             $self->_as_tag($opts{tag}, 1)->dbid,
377             $self->_as_tag($opts{relation}, 1)->dbid,
378             $self->_as_tag($opts{related}, 1)->dbid,
379             Data::TagDB::Tag::dbid($self->_as_tag($opts{context}), 1),
380 0           Data::TagDB::Tag::dbid($self->_as_tag($opts{filter}), 1),
381             );
382              
383 0           $query->execute(@bind);
384 0           $query->finish;
385              
386 0           return Data::TagDB::Relation->_new(%opts, db => $self);
387             }
388              
389              
390             sub create_cache {
391 0     0 1   my ($self) = @_;
392 0           require Data::TagDB::Cache;
393 0           return Data::TagDB::Cache->_new(db => $self);
394             }
395              
396              
397             sub migration {
398 0     0 1   my ($self) = @_;
399 0           require Data::TagDB::Migration;
400 0   0       return $self->{migration} //= Data::TagDB::Migration->_new(db => $self);
401             }
402              
403              
404             sub factory {
405 0     0 1   my ($self) = @_;
406 0           require Data::TagDB::Factory;
407 0   0       return $self->{factory} //= Data::TagDB::Factory->_new(db => $self);
408             }
409              
410              
411             sub exporter {
412 0     0 1   my ($self, $target, %opts) = @_;
413 0           require Data::TagDB::Exporter;
414 0           return Data::TagDB::Exporter->_new(db => $self, target => $target, %opts);
415             }
416              
417              
418             sub begin_work {
419 0     0 1   my ($self, @args) = @_;
420 0 0 0       croak 'Transaction already in process' if $self->{transaction_refc} || defined($self->{transaction_type});
421 0           $self->{transaction_refc} = 1;
422 0           return $self->dbh->begin_work(@args);
423             }
424              
425             sub commit {
426 0     0 1   my ($self, @args) = @_;
427 0 0         croak 'No transaction in process' unless $self->{transaction_refc};
428 0           $self->{transaction_refc}--;
429 0 0         return if $self->{transaction_refc};
430 0           return $self->dbh->commit(@args);
431             }
432              
433             sub rollback {
434 0     0 1   my ($self, @args) = @_;
435 0 0         croak 'No transaction in process' unless $self->{transaction_refc};
436 0           $self->{transaction_refc}--;
437 0 0         return if $self->{transaction_refc};
438 0           return $self->dbh->rollback(@args);
439             }
440              
441              
442             sub in_transaction {
443 0     0 1   my ($self, $type, $code) = @_;
444 0           my $error;
445              
446 0 0 0       croak 'Bad transaction type' unless $type eq 'ro' || $type eq 'rw';
447 0 0         croak 'Transaction already in process' if $self->{transaction_refc};
448              
449 0 0         unless (defined($self->{transaction_type})) {
450 0           $self->{transaction_type} = $type;
451 0           $self->{transaction_open} = 0;
452 0           $self->dbh->begin_work;
453             }
454              
455 0 0 0       if ($self->{transaction_type} eq $type || $self->{transaction_type} eq 'rw') {
    0 0        
456             # no-op
457             } elsif ($self->{transaction_type} eq 'ro' && $type eq 'rw') {
458 0           $self->{transaction_type} = $type;
459             } else {
460 0           $error = 'Transaction type missmatch';
461             }
462              
463 0 0         unless (defined $error) {
464 0           $self->{transaction_open}++;
465 0           eval { $code->() };
  0            
466 0           $self->{transaction_open}--;
467             }
468              
469 0 0         unless ($self->{transaction_open}) {
470 0           delete $self->{transaction_type};
471 0           $self->dbh->commit;
472             }
473              
474 0 0         croak $error if defined $error;
475             }
476              
477             # ---- Virtual methods ----
478              
479             # ---- Private helpers ----
480              
481             sub DESTROY {
482 0     0     my ($self) = @_;
483 0           eval { $self->disconnect };
  0            
484             }
485              
486             sub assert_connected {
487 0     0 0   my ($self) = @_;
488 0           my $dbh = $self->{dbh};
489 0 0         confess 'Not connected to any database' unless defined $dbh;
490 0           return $dbh;
491             }
492              
493             sub tag_by_dbid {
494 0     0 0   my ($self, $dbid) = @_;
495 0           my $cache = $self->{cache_tag};
496 0 0         if (defined $cache->{$dbid}) {
497 0           return $cache->{$dbid};
498             } else {
499 0           state $done = 0;
500 0           my $tag = Data::TagDB::Tag->_new(db => $self, dbid => $dbid);
501              
502 0 0         if ($done++ > 1024) {
503 0           $self->_cache_maintain;
504 0           $done = 0;
505             }
506              
507 0           $cache->{$dbid} = $tag;
508 0           weaken($cache->{$dbid});
509              
510 0           return $tag;
511             }
512             }
513              
514             sub _tag_by_ise_cached {
515 0     0     my ($self, $ise, $autocreate) = @_;
516 0 0         if (defined $self->{cache_ise}{$ise}) {
517 0           return $self->tag_by_dbid($self->{cache_ise}{$ise});
518             } else {
519 0           my $tag = $self->tag_by_id(uuid => $ise, $autocreate); # TODO: Allow all ISE here.
520 0           $self->{cache_ise}{$ise} = $tag->dbid;
521 0           return $tag;
522             }
523             }
524              
525             sub _cache_maintain {
526 0     0     my ($self) = @_;
527 0           my $cache = $self->{cache_tag};
528              
529 0           foreach my $key (keys %{$cache}) {
  0            
530 0 0         delete $cache->{$key} unless defined $cache->{$key};
531             }
532             }
533              
534             sub _cache_clear {
535 0     0     my ($self) = @_;
536 0           $self->_cache_maintain;
537 0           %{$self->{cache_ise}} = ();
  0            
538             }
539              
540             sub _as_tag {
541 0     0     my ($self, $id, $autocreate) = @_;
542 0 0         return undef unless defined $id;
543 0 0         return $id if eval {$id->isa('Data::TagDB::Tag')};
  0            
544 0           return $self->tag_by_id(Data::Identifier->new(from => $id, db => $self), $autocreate);
545             }
546              
547             sub _default_type {
548 0     0     my ($self, $relation) = @_;
549 0           my $relation_dbid = $relation->dbid;
550 0 0         if (defined $self->{cache_default_type}{$relation_dbid}) {
551 0           return $self->tag_by_dbid($self->{cache_default_type}{$relation_dbid});
552             } else {
553 0           my $type = eval {$self->relation(tag => $relation, relation => $self->wk->default_type)->one->related};
  0            
554 0 0         if (defined $type) {
    0          
555 0           $self->{cache_default_type}{$relation_dbid} = $type->dbid;
556             } elsif (defined $self->{backup_type}{$relation_dbid}) {
557 0           return $self->tag_by_dbid($self->{cache_default_type}{$relation_dbid} = $self->{backup_type}{$relation_dbid});
558             } else {
559 0           die 'No default type known';
560             }
561 0           return $type;
562             }
563             }
564              
565             sub _default_encoding {
566 0     0     my ($self, $type) = @_;
567 0           my $type_dbid = $type->dbid;
568 0 0         if (defined $self->{cache_default_encoding}{$type_dbid}) {
569 0           return $self->tag_by_dbid($self->{cache_default_encoding}{$type_dbid});
570             } else {
571 0           my $encoding = $self->relation(tag => $type, relation => $self->wk->default_encoding)->one->related;
572 0           $self->{cache_default_encoding}{$type_dbid} = $encoding->dbid;
573 0           return $encoding;
574             }
575             }
576              
577             sub _register_backup_type {
578 0     0     my ($self, $relation, $type) = @_;
579 0           $self->{backup_type}{$relation->dbid} = $type->dbid;
580             }
581              
582             sub _register_basic_decoders {
583 0     0     my ($self) = @_;
584 0   0       my $decoders = $self->{decoders} //= {};
585 0           my $wk = $self->wk;
586 0     0     my $decode_string = sub { $_[0]->data_raw };
  0            
587 0     0     my $decode_uri = sub { URI->new($_[0]->data_raw) };
  0            
588 0 0   0     my $decode_int = sub { my $v = $_[0]->data_raw; croak 'Bad data' unless $v =~ /^[0-9]+$/; int($v) };
  0            
  0            
  0            
589 0     0     my $decode_colour = sub { Data::URIID::Colour->new(rgb => $_[0]->data_raw) };
  0            
590              
591 0           eval { $self->register_decoder($wk->uuid, $wk->string_ise_uuid_encoding, $decode_string) };
  0            
592 0           eval { $self->register_decoder($wk->oid, $wk->string_ise_oid_encoding, $decode_string) };
  0            
593 0           eval { $self->register_decoder($wk->uri, $wk->ascii_uri_encoding, $decode_uri) };
  0            
594 0           eval { $self->register_decoder($wk->tagname, $wk->utf_8_string_encoding, $decode_string) };
  0            
595 0           eval { $self->register_decoder($wk->x11_colour_name, $wk->utf_8_string_encoding, $decode_string) };
  0            
596 0           eval { $self->register_decoder($wk->wikidata_identifier, $wk->utf_8_string_encoding, $decode_string) };
  0            
597 0           eval { $self->register_decoder($wk->small_identifier, $wk->ascii_decimal_integer_encoding, $decode_int) };
  0            
598 0           eval { $self->register_decoder($wk->unsigned_integer, $wk->ascii_decimal_integer_encoding, $decode_int) };
  0            
599 0           eval { $self->register_decoder($wk->unicode_string, $wk->utf_8_string_encoding, $decode_string) };
  0            
600 0           eval { $self->register_decoder($wk->colour_value, $wk->hex_rgb_encoding, $decode_colour) };
  0            
601              
602 0           eval { $self->_register_backup_type($wk->wd_unicode_character, $wk->unicode_string) };
  0            
603 0           eval { $self->_register_backup_type($wk->tagpool_tag_icontext, $wk->unicode_string) };
  0            
604 0           eval { $self->_register_backup_type($wk->also_has_description, $wk->unicode_string) };
  0            
605 0           eval { $self->_register_backup_type($wk->final_file_size, $wk->unsigned_integer) };
  0            
606              
607 0           return $decoders;
608             }
609              
610             sub _get_decoder {
611 0     0     my ($self, $metadata) = @_;
612 0   0       my $decoders = $self->{decoders} //= $self->_register_basic_decoders;
613 0   0       my $for_type = $decoders->{$metadata->type_evaluated->dbid} //= {};
614 0   0       return $for_type->{$metadata->encoding_evaluated->dbid} // croak 'No matching decoder found';
615             }
616              
617             sub _DBI_name {
618 0     0     my ($self) = @_;
619 0   0       return $self->{_DBI_name} //= $self->dbh->{Driver}{Name};
620             }
621              
622             sub _query {
623 0     0     my ($self, $name) = @_;
624 0           $self->assert_connected;
625 0   0       return $self->{query}{$name} // confess 'No such query: '.$name;
626             }
627              
628             sub _get_data {
629 0     0     my ($self, $name, @args) = @_;
630 0           my $query = $self->_query($name);
631 0           my $row;
632              
633 0           $query->execute(@args);
634 0           $row = $query->fetchrow_arrayref;
635 0           $query->finish;
636              
637 0 0         croak 'No such entry' unless defined $row;
638              
639 0           return $row->[0];
640             }
641              
642             sub _build_query {
643 0     0     my ($self, %opts) = @_;
644 0           my %parts;
645             my @where;
646 0           my @binds;
647              
648 0 0         if ($opts{package} eq 'Data::TagDB::Metadata') {
649 0           $parts{FROM} = 'metadata';
650 0           $parts{SELECT} = '*'; # TODO
651             } else {
652 0           $parts{FROM} = 'relation';
653 0           $parts{SELECT} = '*'; # TODO
654             }
655              
656 0 0         if (defined $opts{limit}) {
657 0           $parts{LIMIT} = $opts{limit};
658             }
659              
660 0           foreach my $key (qw(tag relation context filter related type encoding)) {
661 0           foreach my $neg (0, 1) {
662 0 0         my $curkey = ($neg ? 'no_' : '').$key;
663 0 0         if (defined $opts{$curkey}) {
664 0 0         my @list = ref($opts{$curkey}) eq 'ARRAY' ? @{$opts{$curkey}} : ($opts{$curkey});
  0            
665              
666 0           foreach my $ent (@list) {
667 0 0         croak 'Something not a Data::TagDB::Tag used as Tag filter' unless $ent->isa('Data::TagDB::Tag');
668             }
669              
670 0 0         push(@where, sprintf('%s %sIN (%s)', $key, $neg ? 'NOT ' : '', join(',', map {$_->dbid} @list)));
  0            
671             }
672             }
673             }
674              
675 0 0         if (defined $opts{data_raw}) {
676 0           push(@where, 'data = ?');
677 0           push(@binds, $opts{data_raw});
678             }
679              
680 0 0         if (scalar(@where)) {
681 0           $parts{WHERE} = join(' AND ', @where);
682             }
683              
684 0 0         if (defined $opts{order_by}) {
685 0 0         my @list = ref($opts{order_by}) eq 'ARRAY' ? @{$opts{order_by}} : ($opts{order_by});
  0            
686 0 0         if (scalar @list) {
687             $parts{ORDER} = 'BY '.join(', ',
688 0           map {sprintf('%s ASC', $_)} @list
  0            
689             );
690             }
691             }
692              
693             {
694 0           my $q = '';
  0            
695 0           my $sth;
696              
697 0           foreach my $key (qw(SELECT FROM WHERE ORDER LIMIT)) {
698 0 0         if (defined $parts{$key}) {
699 0 0         $q .= ' ' if length $q;
700 0           $q .= $key.' '.$parts{$key};
701             }
702             }
703              
704 0           $sth = $self->dbh->prepare($q);
705 0           $sth->execute(@binds);
706 0           return $sth;
707             }
708             }
709              
710             sub _link_iterator {
711 0     0     my ($self, %opts) = @_;
712 0           my $query = $self->_build_query(%opts);
713 0           my %args;
714              
715 0 0         if ($opts{package} eq 'Data::TagDB::Metadata') {
716 0           $args{tag_keys} = {map{$_ => $_} qw(type encoding)};
  0            
717 0           $args{raw_keys} = {data_raw => 'data'};
718             } else {
719 0           $args{tag_keys} = {map{$_ => $_} qw(filter related)};
  0            
720 0           $args{raw_keys} = {}; # empty
721             }
722              
723             # Add common keys:
724 0           $args{tag_keys}{$_} = $_ foreach qw(tag relation context);
725              
726 0           return Data::TagDB::LinkIterator->new(%args, db => $self, query => $query, package => $opts{package});
727             }
728              
729             sub _load_cloudlet {
730 0     0     my ($self, %opts) = @_;
731 0           my $direct = $opts{direct};
732 0           my $indirect = $opts{indirect};
733              
734 0 0         $direct = [$direct] unless ref($direct) eq 'ARRAY';
735 0 0 0       $indirect = [$indirect] unless ref($indirect) eq 'ARRAY' || !defined($indirect);
736              
737 0 0         return Data::TagDB::Cloudlet->new(db => $self, root => []) unless scalar(@{$direct});
  0            
738              
739 0 0 0       if (defined($indirect) && !scalar(@{$indirect})) {
  0            
740 0           $indirect = undef;
741             }
742              
743 0 0         if (defined $opts{indirect}) {
744 0           my $query = 'WITH RECURSIVE X(related,root) AS (SELECT related,true FROM relation WHERE tag = ? AND relation IN ('.join(',', map{'?'} @{$direct}).') UNION SELECT relation.related,false FROM relation, X WHERE relation.relation IN ('.join(',', map{'?'} @{$indirect}).') AND relation.tag = X.related) SELECT related,root FROM X';
  0            
  0            
  0            
  0            
745 0           my @bind = ($opts{tag}->dbid, map {$_->dbid} @{$direct}, @{$indirect});
  0            
  0            
  0            
746 0           my $sth = $self->dbh->prepare($query);
747 0           my @root;
748             my @entry;
749              
750 0           $sth->execute(@bind);
751 0           while (my $row = $sth->fetchrow_arrayref) {
752 0           my $ent = $self->tag_by_dbid($row->[0]);
753 0 0         if ($row->[1]) {
754 0           push(@root, $ent);
755             } else {
756 0           push(@entry, $ent);
757             }
758             }
759 0           $sth->finish;
760 0           return Data::TagDB::Cloudlet->new(db => $self, root => \@root, entry => \@entry);
761             } else {
762             return Data::TagDB::Cloudlet->new(db => $self, root => [
763 0           $self->relation(tag => $opts{tag}, relation => $opts{direct})->collect('related')
764             ]);
765             }
766              
767             # WITH RECURSIVE X(related,root) AS (SELECT related,true FROM relation WHERE tag = 597 AND relation IN (7, 201) UNION SELECT relation.related,false FROM relation, X WHERE relation.relation = 140 AND relation.tag = X.related) SELECT *,(SELECT data FROM metadata WHERE tag = X.related AND relation = 1 AND type = 5 LIMIT 1) FROM X
768             }
769              
770             # ---- AUTOLOAD ----
771              
772             sub AUTOLOAD {
773 0     0     my ($self, @args) = @_;
774 0           our $AUTOLOAD;
775 0           my $function = $AUTOLOAD =~ s/^.*:://r;
776 0   0       my $query = $self->{query}{$function} // confess 'Bad function: '.$function;
777              
778 0 0         if ($function =~ /^tag_by_/) {
779 0           my $row;
780              
781 0           $query->execute(@args);
782 0           $row = $query->fetchrow_hashref;
783 0           $query->finish;
784              
785 0 0 0       croak 'No such tag' unless defined($row->{tag}) && $row->{tag} > 0;
786              
787 0           return $self->tag_by_dbid($row->{tag});
788             } else {
789 0           confess 'Unsupported function with know query: '.$function;
790             }
791             }
792              
793             1;
794              
795             __END__
796              
797             =pod
798              
799             =encoding UTF-8
800              
801             =head1 NAME
802              
803             Data::TagDB - Work with Tag databases
804              
805             =head1 VERSION
806              
807             version v0.12
808              
809             =head1 SYNOPSIS
810              
811             use Data::TagDB;
812              
813             my $db = Data::TagDB->new($dsn, ...);
814             # or:
815             my $db = Data::TagDB->new($dbh);
816              
817             # Create new database:
818             use Data::TagDB::Migration;
819             my Data::TagDB $db = Data::TagDB::Migration->create(...);
820              
821             This module implements SQL based universal tag databases. Such databases can be used to store any kind of (semantic) data.
822              
823             This module and it's submodule implement creation of databases, migration (to most current scheme),
824             adding data and reading data from the database.
825              
826             For an introduction see L<Data::TagDB::Tutorial>.
827              
828             The instances of L<Data::TagDB::Tag> repesent any kind of object (may it be file, user account or a real life object like a tree).
829             It provides some convenience functions such as to query objects for their name.
830              
831             L<Data::TagDB::Factory> (via L</factory>) is provided for easy creation of new tags.
832              
833             B<Note:>
834             Correct transaction management can improve performance I<significantly>. Sometimes the improvement can be by a factor of a few thousand.
835             Applications should therefore consider to group requests into transactions. This is also true for read only requests.
836              
837             B<Note:>
838             Future versions of this module will depend on L<Data::Identifier>.
839              
840             B<Note:>
841             This module supports SQLite and PostgreSQL (experimental).
842              
843             =head1 METHODS
844              
845             =head2 new
846              
847             my $db = Data::TagDB->new($dsn, ...);
848             # or:
849             my $db = Data::TagDB->new($dbh);
850              
851             Returns a new object that can be used for lookups.
852             Either an already connected L<DBI> handle can be passed or
853             data source that is then passed to L<DBI/connect> internally.
854              
855             If a open handle is passed, the same restrictions apply as for L</dbh>.
856              
857             =head2 dbh
858              
859             my $dbh = $db->dbh;
860              
861             Returns the current L<DBI> connection.
862              
863             This connection can be used to call any transaction independent method on the handle.
864             It can for example be used to call L<DBI/ping> to keep the connection alive.
865              
866             If methods are called that depend on the state of the transaction logic
867             (such as performing an SELECT or UPDATE) the state of the transaction B<must> be managed via
868             this module. See L</begin_work>.
869              
870             The same holds true for any open handle passed to L</new>. When passed the handle must
871             not be in any active transaction and must not be used outside this module to change the transaction
872             state of the handle.
873              
874             It is also wise to avoid interacting with the tables managed by this module. This may result in the
875             internal states being in a wrong state. It is however generally safe (but for the restrictions given above)
876             to interact with tables outside of the use of this module.
877              
878             As table names that are in use by this module depend on the version of the schema that is currently active
879             (and may change in future) it is most wise to have any custom tables in a seperate namespace of some kind
880             (the exact ways to do this may depend on the type of database used).
881              
882             =head2 disconnect
883              
884             $db->disconnect
885              
886             This disconnects from the database backend. It also renders this object useless.
887              
888             =head2 tag_by_id
889              
890             my Data::TagDB::Tag $tag = $db->tag_by_id($type => $id);
891             # or:
892             my Data::TagDB::Tag $tag = $db->tag_by_id($hint => $id);
893             # or:
894             my Data::Identifier $id = ...;
895             my Data::TagDB::Tag $tag = $db->tag_by_id($id);
896             # e.g:
897             my Data::TagDB::Tag $tag = $db->tag_by_id(uuid => 'abc...');
898              
899             Gets a tag by an an identifier of the provided type. The type must be a C<Data::TagDB::Tag> or a
900             a string that is a valid hint.
901              
902             If only argument is provided the argument must be an instance of L<Data::Identifier>.
903              
904             =head2 tag_by_specification
905              
906             my Data::TagDB::Tag $tag = $db->tag_by_specification($specification, style => $style [, %opts ]);
907              
908             Gets a tag by specification according to a style.
909             This method is mostly useful to parse user input and find the corresponding tag.
910              
911             B<Note:>
912             This method is experimental. It may change prototype, and behaviour or may be removed in future versions without warning.
913             Role matching depends on L<Data::TagDB::Tag/cloudlet> and is subject to its status.
914              
915             The following styles are supported:
916              
917             =over
918              
919             =item C<ise>
920              
921             The given specification is an UUID, OID, or URI.
922              
923             =item C<tagpool>
924              
925             The given specification is in tagpool format.
926             Both C<type@tag> and C<tag!> notation is supported (can also be mixed freely).
927              
928             Parsing interacts with options the same way as tagpool does.
929              
930             =item C<sirtx>
931              
932             The given specification is in SIRTX format.
933             Currently only I<*local>, I<'number>, I<logical>, and I<type:id> formats are supported.
934             There is very limited support for I<%port>, and I<&port>.
935             Bracket-escape is only supported for top level.
936              
937             Supports the options C<sirtx_local_ids>, and C<sirtx_ports>.
938              
939             =back
940              
941             The following (all optional) options are supported:
942              
943             =over
944              
945             =item C<as_is>
946              
947             If true, this disables special parsing rules.
948             For style C<tagpool> it disables all parsing but the check for UUIDs.
949              
950             =item C<important>
951              
952             Requires the tag to be marked important.
953              
954             =item C<role>
955              
956             A role the tag is required to have.
957              
958             =item C<sirtx_local_ids>
959              
960             An hashref with the local id (without the C<*>) as key and L<Data::TagDB::Tag> as value.
961              
962             =item C<sirtx_ports>
963              
964             An arrayref with an even number of elements (key-value pairs).
965             Elements with an even index are considered the key (port).
966             They are followed by the corresponding (port) value.
967              
968             All elements must be an instance of L<Data::TagDB::Tag>.
969              
970             =back
971              
972             =head2 relation
973              
974             my Data::TagDB::Iterator $iter = $db->relation(...);
975              
976             Returns an iterator for relations.
977             The following keys can be used to filter the list. All must be L<Data::TagDB::Tag> or an array ref of them objects:
978             C<tag>,
979             C<relation>,
980             C<context>,
981             C<filter>, and
982             C<related>.
983             Each may be prefixed with C<no_> for negative filtering.
984              
985             =head2 metadata
986              
987             my Data::TagDB::Iterator $iter = $db->metadata(...);
988              
989             Returns an iterator for relations.
990             The following keys can be used to filter the list. All must be L<Data::TagDB::Tag> or an array ref of them objects:
991             C<tag>,
992             C<relation>,
993             C<context>,
994             C<type>, and
995             C<encoding>.
996             Each may be prefixed with C<no_> for negative filtering.
997              
998             Additionally C<data_raw> can be used to filter for a data value.
999              
1000             =head2 link
1001              
1002             my Data::TagDB::Iterator $iter = $db->link(...);
1003              
1004             This combines L</relation>, and L</metadata>. An iterator is returned that lists both metadata, and relations (in any order).
1005             The common subset of filters can be used. Namely:
1006             C<tag>,
1007             C<relation>, and
1008             C<context>.
1009              
1010             =head2 wk
1011              
1012             my Data::TagDB::WellKnown $tag = $db->wk;
1013             my Data::TagDB::Tag $tag = $wk->...;
1014             # e.g.:
1015             my Data::TagDB::Tag $asi = $db->wk->also_shares_identifier;
1016              
1017             Returns a dictionary of well known tags.
1018              
1019             =head2 register_decoder
1020              
1021             $db->register_decoder($type, $encoding, sub { ... });
1022              
1023             Registers a decoder for a given type and encoding. Both C<$type>, and C<$encoding>
1024             must be L<Data::TagDB::Tag>.
1025              
1026             =head2 create_tag
1027              
1028             my Data::TagDB::Tag $tag = $db->create_tag([$type => $value, ...], [$type => $value, ...]);
1029             # or:
1030             my Data::Identifier $id = ...;
1031             my Data::Identifier $extra = ...;
1032             my Data::TagDB::Tag $tag = $db->create_tag($id, [ $extra ]);
1033              
1034             Create a tag (or return it if it already exists). Takes two lists if type-identifier pairs.
1035             The first list is the list of identifiers that uniquely identify the tag (e.g. an UUID).
1036             The second list contains additional, non unique identifiers (e.g. tagnames) and is optional.
1037              
1038             If the tag does not exist it is created. Once it exists all identifiers added (for already existing tags missing identifiers are added).
1039              
1040             Each list can be replaced by a single instance of L<Data::Identifier>.
1041              
1042             =head2 create_metadata
1043              
1044             my Data::TagDB::Metadata $metadata = $db->create_metadata(
1045             tag => $tag, # required
1046             relation => $relation, # required
1047             context => $context,
1048             type => $type,
1049             encoding => $encoding,
1050             data_raw => $raw, # required
1051             );
1052              
1053             Create a metadata entry if it does not yet exist. Returns it once created.
1054              
1055             =head2 create_relation
1056              
1057             my Data::TagDB::Relation $relation = $db->create_relation(
1058             tag => $tag, # required
1059             relation => $relation, # required
1060             related => $related, # required
1061             context => $context,
1062             filter => $filter,
1063             );
1064              
1065             Creates a relation (if it does not yet exist) and returns it.
1066              
1067             =head2 create_cache
1068              
1069             my Data::TagDB::Cache $cache = $db->create_cache;
1070              
1071             Create a new cache object every time this is called.
1072             Cache objects can be used to speed up processing.
1073             See L<Data::TagDB::Cache> for details.
1074              
1075             =head2 migration
1076              
1077             $db->migration->upgrade;
1078              
1079             Get a migration object. This is mostly used for upgrading the database schema to the
1080             current one. It is recommended to perform upgrades for long running processes.
1081             For short running processes this can increase the startup time.
1082              
1083             See also L<Data::TagDB::Migration>.
1084              
1085             =head2 factory
1086              
1087             my Data::TagDB::Factory $factory = $db->factory;
1088              
1089             Get a factory object used to create tags.
1090             See also L<Data::TagDB::Factory> for details.
1091              
1092             =head2 exporter
1093              
1094             my Data::TagDB::Exporter $exporter = $db->exporter($target, %opts);
1095              
1096             Create a new exporter. C<$target> must be a open file handle (that supports seeking)
1097             or a filename.
1098              
1099             See also L<Data::TagDB::Exporter>.
1100              
1101             The following options (all optional) are defined:
1102              
1103             =over
1104              
1105             =item C<format>
1106              
1107             The format to use. This can be L<Data::TagDB::Tag>, a L<Data::Identfier>, or a raw ISE string.
1108              
1109             The default is I<tagpool-source-format> (C<e5da6a39-46d5-48a9-b174-5c26008e208e>).
1110              
1111             =back
1112              
1113             =head2 begin_work, commit, rollback
1114              
1115             $db->begin_work;
1116             # ...
1117             $db->commit;
1118             # or:
1119             $db->rollback;
1120              
1121             Those methods are provided as proxy to L<DBI>'s.
1122             The correct use of transactions can improve the speed (both for reading and writing)
1123             significantly. Specifically tag databases are subject to many improvements of correct transaction
1124             mangement.
1125              
1126             B<Note:>
1127             For each call to C<begin_work> there must be a matching call to C<commit> or C<rollback>.
1128             This is important as this API will keep track of transactions internally.
1129              
1130             B<Note:>
1131             A call to C<begin_work> may or may not fail if another transaction is already in process.
1132             This may depend on the type of database used.
1133              
1134             B<Note:>
1135             The return value of those methods is undefined. On error they will C<die>.
1136              
1137             B<Note:>
1138             These methods are mutually exclusive with the use of L</in_transaction> at this time.
1139             However, the use of L</in_transaction> is recommended.
1140              
1141             For details see also: L<DBI/begin_work>, L<DBI/commit>, L<DBI/rollback>.
1142              
1143             =head2 in_transaction
1144              
1145             $db->in_transaction(ro => sub { ....});
1146             # or:
1147             $db->in_transaction(rw => sub { ....});
1148              
1149             Runs a block of code (a subref) inside a transaction.
1150              
1151             The passed block is run in a transaction. The transaction is commited after the code finishes.
1152              
1153             The type of the transaction can be C<ro> (read only) or C<rw> (read-write).
1154             The module may optimise based on this information.
1155             If a write operation is performed in a transaction that is marked C<ro> the behaviour is unspecified.
1156              
1157             In contrast to L</begin_work> and L</commit> calls to this method can be stacked freely.
1158             For example the following is valid:
1159              
1160             $db->in_transaction(ro => sub {
1161             # do some read...
1162             $db->in_transaction(rw => sub {
1163             # do some write...
1164             });
1165             # do more reading, writing is invalid here
1166             });
1167              
1168             B<Note:>
1169             If the code C<die>s the error is ignored. The transaction is still commited.
1170             If the code wants to perform rollback in case it fails this function might not be the one to use.
1171              
1172             B<Note:>
1173             Data written might only be visible to other handles of the same database once I<all>
1174             transactions have been finished.
1175              
1176             B<Note:>
1177             This method is mutually exclusive with the use of L</begin_work> at this time.
1178              
1179             =head2 tag_by_hint
1180              
1181             my Data::TagDB::Tag $tag = $db->tag_by_hint($hint);
1182              
1183             Get a tag by hint. What hints are supported depends on what is stored in the database's hint table.
1184              
1185             =head1 AUTHOR
1186              
1187             Philipp Schafft <lion@cpan.org>
1188              
1189             =head1 COPYRIGHT AND LICENSE
1190              
1191             This software is Copyright (c) 2024-2025 by Philipp Schafft <lion@cpan.org>.
1192              
1193             This is free software, licensed under:
1194              
1195             The Artistic License 2.0 (GPL Compatible)
1196              
1197             =cut