TagIndex.pm 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. package Trog::SQLite::TagIndex;
  2. use strict;
  3. use warnings;
  4. no warnings 'experimental';
  5. use feature qw{signatures};
  6. use List::Util qw{uniq};
  7. use Trog::SQLite;
  8. =head1 Trog::SQLite::TagIndex
  9. An SQLite3 index of posts by tag and date.
  10. Used to speed up the flat-file data model.
  11. Also used to retrieve cached routes from posts.
  12. =head1 FUNCTIONS
  13. =cut
  14. sub posts_for_tags (@tags) {
  15. my $dbh = _dbh();
  16. my $clause = @tags ? "WHERE tag IN (".join(',' ,(map {'?'} @tags)).")" : '';
  17. my $rows = $dbh->selectall_arrayref("SELECT DISTINCT id FROM posts $clause ORDER BY created DESC",{ Slice => {} }, @tags);
  18. return () unless ref $rows eq 'ARRAY' && @$rows;
  19. return map { $_->{id} } @$rows;
  20. }
  21. sub routes {
  22. my $dbh = _dbh();
  23. my $rows = $dbh->selectall_arrayref("SELECT id, route, method, callback FROM all_routes",{ Slice => {} });
  24. return () unless ref $rows eq 'ARRAY' && @$rows;
  25. my %routes = map { $_->{route} => $_ } @$rows;
  26. return %routes;
  27. }
  28. sub aliases {
  29. my $dbh = _dbh();
  30. my $rows = $dbh->selectall_arrayref("SELECT actual,alias FROM aliases", { Slice => {} });
  31. return () unless ref $rows eq 'ARRAY' && @$rows;
  32. my %aliases = map { $_->{alias} => $_->{actual} } @$rows;
  33. return %aliases;
  34. }
  35. sub tags {
  36. my $dbh = _dbh();
  37. my $rows = $dbh->selectall_arrayref("SELECT name FROM tag", { Slice => {} });
  38. return () unless ref $rows eq 'ARRAY' && @$rows;
  39. return map { $_->{name} } @$rows;
  40. }
  41. sub add_post ($post,$data_obj) {
  42. my $dbh = _dbh();
  43. build_index($data_obj,[$post]);
  44. build_routes($data_obj,[$post]);
  45. return 1;
  46. }
  47. sub remove_post ($post) {
  48. my $dbh = _dbh();
  49. # Deleting the post will cascade to the post index & primary route, which cascades to the aliases
  50. $dbh->do("DELETE FROM post WHERE uuid=?", undef, $post->{id});
  51. # Now that we've wasted the routes and post, let's reap any dangling tags or callbacks.
  52. # We won't ever reap methods, because they're just HTTP methods in an enum table.
  53. $dbh->do("DELETE from callbacks WHERE id NOT IN (SELECT DISTINCT callback_id FROM routes)");
  54. $dbh->do("DELETE from tag WHERE id NOT IN (SELECT DISTINCT tag_id FROM posts_index)");
  55. return 1;
  56. }
  57. sub build_index($data_obj,$posts=[]) {
  58. my $dbh = _dbh();
  59. $posts = $data_obj->read({ limit => 0, acls => ['admin'] }) unless @$posts;
  60. # First, slap in the UUIDs
  61. my @uuids = map { $_->{id} } @$posts;
  62. Trog::SQLite::bulk_insert($dbh,'post',['uuid'],'IGNORE', @uuids);
  63. my $pids = _id_for_uuid($dbh,@uuids);
  64. foreach my $post (@$posts) {
  65. $post->{post_id} = $pids->{$post->{id}}{id};
  66. }
  67. # Slap in the tags
  68. my @tags = uniq map { @{$_->{tags}} } @$posts;
  69. Trog::SQLite::bulk_insert($dbh,'tag', ['name'], 'IGNORE', @tags);
  70. #TODO restrict query to only the specific tags we care about
  71. my $t = $dbh->selectall_hashref("SELECT id,name FROM tag", 'name');
  72. foreach my $k (keys(%$t)) { $t->{$k} = $t->{$k}->{id} };
  73. # Finally, index the posts
  74. Trog::SQLite::bulk_insert($dbh,'posts_index',[qw{post_id post_time tag_id}], 'IGNORE', map {
  75. my $subj = $_;
  76. map { ( $subj->{post_id}, $subj->{created}, $t->{$_} ) } @{$subj->{tags}}
  77. } @$posts );
  78. }
  79. sub _id_for_uuid($dbh,@uuids) {
  80. my $bind = join(',', (map { '?' } @uuids));
  81. Trog::SQLite::bulk_insert($dbh,'post',['uuid'],'IGNORE', @uuids);
  82. return $dbh->selectall_hashref("SELECT id,uuid FROM post WHERE uuid IN ($bind)", 'uuid', {}, @uuids);
  83. }
  84. # It is important we use get() instead of read() because of incomplete data.
  85. sub build_routes($data_obj,$posts=[]) {
  86. my $dbh = _dbh();
  87. @$posts = $data_obj->get( limit => 0, acls => ['admin'] ) unless @$posts;
  88. my @uuids = map { $_->{id} } @$posts;
  89. my $pids = _id_for_uuid($dbh,@uuids);
  90. foreach my $post (@$posts) {
  91. $post->{post_id} = $pids->{$post->{id}}{id};
  92. }
  93. # Ensure the callbacks we need are installed
  94. Trog::SQLite::bulk_insert($dbh,'callbacks', [qw{callback}], 'IGNORE', (uniq map { $_->{callback} } @$posts) );
  95. my $m = $dbh->selectall_hashref("SELECT id, method FROM methods", 'method');
  96. foreach my $k (keys(%$m)) { $m->{$k} = $m->{$k}->{id} };
  97. my $c = $dbh->selectall_hashref("SELECT id, callback FROM callbacks", 'callback');
  98. foreach my $k (keys(%$c)) { $c->{$k} = $c->{$k}->{id} };
  99. @$posts = map {
  100. $_->{method_id} = $m->{$_->{method}};
  101. $_->{callback_id} = $c->{$_->{callback}};
  102. $_
  103. } @$posts;
  104. my @routes = map { ($_->{post_id}, $_->{local_href}, $_->{method_id}, $_->{callback_id} ) } @$posts;
  105. Trog::SQLite::bulk_insert($dbh,'routes', [qw{post_id route method_id callback_id}], 'IGNORE', @routes);
  106. # Now, compile the post aliases
  107. my %routes_actual = routes();
  108. foreach my $post (@$posts) {
  109. next unless (ref $post->{aliases} eq 'ARRAY') && @{$post->{aliases}};
  110. my $route = $post->{local_href};
  111. Trog::SQLite::bulk_insert($dbh, 'post_aliases', [qw{route_id alias}], 'IGNORE', map { ($routes_actual{$route}{id}, $_) } @{$post->{aliases}} );
  112. }
  113. }
  114. # Ensure the db schema is OK, and give us a handle
  115. sub _dbh {
  116. my $file = 'schema/flatfile.schema';
  117. my $dbname = "data/posts.db";
  118. return Trog::SQLite::dbh($file,$dbname);
  119. }
  120. 1;