+ my $sth = $self->{dbh}->prepare_cached(<<"");
+INSERT OR IGNORE INTO msgmap (num,mid) VALUES (?,?)
+
+ my $result = $sth->execute($num, $mid);
+ $self->num_highwater($num) if (defined($result) && $result == 1);
+ $result;
+}
+
+sub DESTROY {
+ my ($self) = @_;
+ my $dbh = $self->{dbh} or return;
+ if (($self->{pid} // 0) == $$) {
+ my $f = $dbh->sqlite_db_filename;
+ unlink $f or warn "failed to unlink $f: $!\n";
+ }
+}
+
+sub atfork_parent {
+ my ($self) = @_;
+ $self->{pid} or die 'BUG: not a temporary clone';
+ $self->{dbh} and die 'BUG: tmp_clone dbh not prepared for parent';
+ defined($self->{filename}) or die 'BUG: {filename} not defined';
+ $self->{dbh} = PublicInbox::Over::dbh_new($self, 2);
+ $self->{dbh}->do('PRAGMA journal_mode = MEMORY');
+}
+
+sub atfork_prepare {
+ my ($self) = @_;
+ my $pid = $self->{pid} or die 'BUG: not a temporary clone';
+ $pid == $$ or die "BUG: atfork_prepare not called by $pid";
+ my $dbh = $self->{dbh} or die 'BUG: temporary clone not open';
+
+ # must clobber prepared statements
+ %$self = (filename => $dbh->sqlite_db_filename, pid => $pid);
+}
+
+sub skip_artnum {
+ my ($self, $skip_artnum) = @_;
+ return meta_accessor($self, 'skip_artnum') if !defined($skip_artnum);
+
+ my $cur = num_highwater($self) // 0;
+ if ($skip_artnum < $cur) {
+ die "E: current article number $cur ",
+ "exceeds --skip-artnum=$skip_artnum\n";
+ } else {
+ my $ok;
+ for (1..10) {
+ my $mid = 'skip'.rand.'@'.rand.'.example.com';
+ $ok = mid_set($self, $skip_artnum, $mid);
+ if ($ok) {
+ mid_delete($self, $mid);
+ last;
+ }
+ }
+ $ok or die '--skip-artnum failed';
+
+ # in the future, the indexer may use this value for
+ # new messages in old epochs
+ meta_accessor($self, 'skip_artnum', $skip_artnum);
+ }
+}
+
+sub check_inodes {
+ my ($self) = @_;
+ $self->{dbh} // return;
+ my $rw = !$self->{dbh}->{ReadOnly};
+ PublicInbox::Over::check_inodes($self);
+ $self->{dbh} //= PublicInbox::Over::dbh_new($self, !$rw);