فهرست منبع

Merge pull request #356 from Troglodyne-Internet-Widgets/rebuild-index-from-scratch

Rebuild the tag index from scratch, as build_index.pl says it does
George S. Baugh 1 هفته پیش
والد
کامیت
4de4af1965
6فایلهای تغییر یافته به همراه160 افزوده شده و 31 حذف شده
  1. 1 1
      Makefile.PL
  2. 16 10
      bin/build_index.pl
  3. 1 18
      lib/Trog/Data/SQLite.pm
  4. 36 2
      lib/Trog/SQLite/TagIndex.pm
  5. 4 0
      t/Trog-Data-SQLite.t
  6. 102 0
      t/Trog-SQLite-TagIndex.t

+ 1 - 1
Makefile.PL

@@ -21,7 +21,7 @@ WriteMakefile(
     },
     },
     PREREQ_PM => {
     PREREQ_PM => {
         'Trog::TOTP'                          => '1.007',
         'Trog::TOTP'                          => '1.007',
-        'Trog::SQLite'                        => '0.001',
+        'Trog::SQLite'                        => '0.004',
         'CGI::Cookie'                         => '0',
         'CGI::Cookie'                         => '0',
         'Capture::Tiny'                       => '0',
         'Capture::Tiny'                       => '0',
         'Carp'                                => '0',
         'Carp'                                => '0',

+ 16 - 10
bin/build_index.pl

@@ -10,12 +10,15 @@ use Trog::SQLite::TagIndex;
 
 
 =head1 SYNOPSIS
 =head1 SYNOPSIS
 
 
-Rebuild the post index and the route table from the posts on disk.
+Rebuild the FlatFile model's tag index and route table, data/posts.db, from the
+posts in data/files.
 
 
-Use this to build the post index after you import data, otherwise it's not
-needed -- the index is maintained as posts are written, so the only time it
-gets out of step with the flat files is when something other than tCMS put them
-there.
+Use this to build the index after you import posts, otherwise it is not needed:
+tCMS keeps the index up to date as it writes posts, so the only time it gets out
+of step with the flat files is when something other than tCMS put them there.
+
+The SQLite model keeps its tags and routes with the posts, and has no separate
+index.  On a site served from it, this says so and changes nothing.
 
 
 =head2 USAGE
 =head2 USAGE
 
 
@@ -24,12 +27,15 @@ there.
 Takes no arguments, and must be run from the tCMS root, as the data paths in
 Takes no arguments, and must be run from the tCMS root, as the data paths in
 the configuration are relative to it.
 the configuration are relative to it.
 
 
-Safe to re-run; it rebuilds from scratch rather than adding to what's there.
+Safe to re-run.  It empties the index first and builds it again, in one
+transaction, so tags and routes that no post has any more are gone afterwards.
 
 
 =cut
 =cut
 
 
-my $conf   = Trog::Config::get();
-my $search = Trog::Data->new($conf);
+my $data = Trog::Data->new( Trog::Config::get() );
+if ( !$data->isa('Trog::Data::FlatFile') ) {
+    say "This site serves from " . ref($data) . ", which has no separate index.  Nothing to do.";
+    exit 0;
+}
 
 
-Trog::SQLite::TagIndex::build_index($search);
-Trog::SQLite::TagIndex::build_routes($search);
+Trog::SQLite::TagIndex::rebuild($data);

+ 1 - 18
lib/Trog/Data/SQLite.pm

@@ -321,24 +321,7 @@ and taking minutes, since SQLite otherwise commits once per row.
 sub write ( $self, $data ) {
 sub write ( $self, $data ) {
     my $dbh = _dbh();
     my $dbh = _dbh();
 
 
-    # Only if we are not already inside someone else's transaction: begin_work
-    # dies rather than nesting.
-    my $mine = $dbh->{AutoCommit} ? $dbh->begin_work : 0;
-
-    local $@;
-    eval {
-        _insert( $dbh, $data );
-        $dbh->commit if $mine;
-        1;
-    } or do {
-        my $error = $@;
-        if ($mine) {
-            local $@;
-            eval { $dbh->rollback; 1 } or WARN("Could not roll back a failed write: $@");
-        }
-        confess $error;
-    };
-
+    Trog::SQLite::transaction( $dbh, sub { _insert( $dbh, $data ) } );
     return 1;
     return 1;
 }
 }
 
 

+ 36 - 2
lib/Trog/SQLite/TagIndex.pm

@@ -54,8 +54,42 @@ sub tags {
 
 
 sub add_post ( $post, $data_obj ) {
 sub add_post ( $post, $data_obj ) {
     my $dbh = _dbh();
     my $dbh = _dbh();
-    build_index( $data_obj, [$post] );
-    build_routes( $data_obj, [$post] );
+    Trog::SQLite::transaction(
+        $dbh,
+        sub {
+            # posts_index has no key for INSERT OR IGNORE to skip a duplicate on,
+            # so a save would add every tag again and keep the ones it removed.
+            $dbh->do( "DELETE FROM posts_index WHERE post_id IN (SELECT id FROM post WHERE uuid=?)", undef, $post->{id} );
+            build_index( $data_obj, [$post] );
+            build_routes( $data_obj, [$post] );
+        }
+    );
+    return 1;
+}
+
+=head2 rebuild($data_obj)
+
+Empty the index, the route table and the aliases, then fill them again from
+every post $data_obj holds.  It is one transaction, so a reader sees the old
+index or the new one, and never an empty one.
+
+The HTTP methods are kept, as the schema fills those rather than any post.
+
+=cut
+
+sub rebuild ($data_obj) {
+    my $dbh = _dbh();
+    Trog::SQLite::transaction(
+        $dbh,
+        sub {
+            # Cascades to posts_index, routes and post_aliases.
+            $dbh->do("DELETE FROM post");
+            $dbh->do("DELETE FROM tag");
+            $dbh->do("DELETE FROM callbacks");
+            build_index($data_obj);
+            build_routes($data_obj);
+        }
+    );
     return 1;
     return 1;
 }
 }
 
 

+ 4 - 0
t/Trog-Data-SQLite.t

@@ -352,6 +352,10 @@ subtest 'writing' => sub {
 
 
     like( exception { $sqlite->write( [ { title => 'No Id' } ] ) }, qr/no id/i, 'a post with no id is refused' );
     like( exception { $sqlite->write( [ { title => 'No Id' } ] ) }, qr/no id/i, 'a post with no id is refused' );
 
 
+    my $count = $sqlite->count();
+    ok( exception { $sqlite->write( [ post( id => 'p-half', title => 'Half' ), { title => 'No Id' } ] ) }, 'a batch with a post it refuses is refused' );
+    is( $sqlite->count(), $count, 'and none of the batch lands' );
+
     # These are computed per query out of the version history; storing them
     # These are computed per query out of the version history; storing them
     # would bake one query's answer into the post forever.
     # would bake one query's answer into the post forever.
     my $dbh = Trog::SQLite::dbh( undef, 'data/posts.sqlite' );
     my $dbh = Trog::SQLite::dbh( undef, 'data/posts.sqlite' );

+ 102 - 0
t/Trog-SQLite-TagIndex.t

@@ -0,0 +1,102 @@
+use strict;
+use warnings;
+
+use Test::More;
+use Cwd        ();
+use File::Copy ();
+use File::Path ();
+use File::Temp ();
+use FindBin;
+
+use lib "$FindBin::Bin/../lib";
+
+use_ok('Trog::SQLite::TagIndex') or BAIL_OUT("Can't find SUT");
+
+# The index opens schema/ and data/ relative to the cwd, so it gets a root of
+# its own with the real schema in it.
+my $oldcwd = Cwd::getcwd();
+my $root   = File::Temp::tempdir( CLEANUP => 1 );
+File::Path::make_path( "$root/schema", "$root/data" );
+File::Copy::copy( "$FindBin::Bin/../schema/flatfile.schema", "$root/schema/flatfile.schema" ) or die "Could not copy the schema: $!";
+chdir $root                                                                                   or die "Could not chdir to $root: $!";
+
+# A data model holding whatever posts it is given, answering read() and get()
+# the way build_index() and build_routes() ask.
+package Fake::Data {
+    sub new  { my ( $class, @posts ) = @_; return bless( { posts => \@posts }, $class ) }
+    sub read { my ($self) = @_; return [ @{ $self->{posts} } ] }
+    sub get  { my ($self) = @_; return @{ $self->{posts} } }
+}
+
+# One that gets as far as the index, then falls over before the routes.
+package Broken::Data {
+    our @ISA = ('Fake::Data');
+    sub get { die "the data model fell over\n" }
+}
+
+sub post {
+    my (%over) = @_;
+    return {
+        id         => 'p-one',
+        tags       => [qw{blog public}],
+        created    => 1700000000,
+        local_href => '/posts/p-one',
+        method     => 'GET',
+        callback   => 'Trog::Routes::HTML::posts',
+        aliases    => [],
+        %over,
+    };
+}
+
+sub index_rows {
+    my $rows = Trog::SQLite::TagIndex::_dbh()->selectall_arrayref( 'SELECT id, tag FROM posts ORDER BY id, tag', { Slice => {} } );
+    return [ map { "$_->{id}:$_->{tag}" } @$rows ];
+}
+
+subtest 'add_post' => sub {
+    my $data = Fake::Data->new();
+
+    Trog::SQLite::TagIndex::add_post( post(), $data );
+    Trog::SQLite::TagIndex::add_post( post(), $data );
+    is_deeply( index_rows(), [qw{p-one:blog p-one:public}], 'saving a post twice indexes its tags once' );
+
+    Trog::SQLite::TagIndex::add_post( post( tags => ['public'] ), $data );
+    is_deeply( index_rows(),                                       ['p-one:public'], 'and a tag the edit removed is gone' );
+    is_deeply( [ Trog::SQLite::TagIndex::posts_for_tags('blog') ], [],               'so the post no longer turns up under it' );
+};
+
+subtest 'rebuild' => sub {
+    my $dbh = Trog::SQLite::TagIndex::_dbh();
+
+    # What an index looks like after something other than tCMS changed the
+    # posts: a post that is gone, and a route to it.
+    Trog::SQLite::TagIndex::add_post( post( id => 'p-gone', local_href => '/posts/p-gone', aliases => ['/gone'] ), Fake::Data->new() );
+    $dbh->do('INSERT INTO posts_index (post_id, post_time, tag_id) SELECT post_id, post_time, tag_id FROM posts_index');
+
+    my $data = Fake::Data->new( post( tags => [qw{blog public}] ), post( id => 'p-two', tags => ['public'], local_href => '/posts/p-two' ) );
+    Trog::SQLite::TagIndex::rebuild($data);
+
+    is_deeply( index_rows(), [qw{p-one:blog p-one:public p-two:public}], 'the index holds exactly the posts there are, once each' );
+
+    my %routes = Trog::SQLite::TagIndex::routes();
+    is_deeply( [ sort keys %routes ],                 [qw{/posts/p-one /posts/p-two}], 'and the routes of those posts only' );
+    is_deeply( { Trog::SQLite::TagIndex::aliases() }, {},                              'with the gone post\'s alias gone too' );
+
+    my ($methods) = $dbh->selectrow_array('SELECT COUNT(*) FROM methods');
+    ok( $methods, 'the HTTP methods the schema fills are kept' );
+
+    Trog::SQLite::TagIndex::rebuild($data);
+    is_deeply( index_rows(), [qw{p-one:blog p-one:public p-two:public}], 'running it again changes nothing' );
+};
+
+subtest 'rebuild is one transaction' => sub {
+    my $before = index_rows();
+
+    my $broken = Broken::Data->new( post() );
+    ok( !eval { Trog::SQLite::TagIndex::rebuild($broken); 1 }, 'a rebuild that fails part way dies' );
+    like( $@, qr/fell over/a, 'with the error that stopped it' );
+    is_deeply( index_rows(), $before, 'and leaves the index as it was' );
+};
+
+chdir $oldcwd;
+done_testing();