TagIndex.pm 5.5 KB

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