From 54f35f93c9f42d81d684e4487d296759d2b16253 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Mon, 27 Jul 2026 00:11:59 +0200
Subject: [PATCH v1 10/12] POC: recovery: port TAP test 029_stats_restart to
 pytest
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The Perl helpers read one column per psql launch and collect the values in a
hash, which resulted in code like this:

    $results{records} =
      $node->safe_psql($connect_db, "SELECT wal_records FROM pg_stat_wal");
    $results{bytes} =
      $node->safe_psql($connect_db, "SELECT wal_bytes FROM pg_stat_wal");
    $results{reset} =
      $node->safe_psql($connect_db, "SELECT stats_reset FROM pg_stat_wal");

In Python the equivalent is this:

    return WalStats(
        *node.sql("SELECT wal_records, wal_bytes, stats_reset FROM pg_stat_wal")
    )

Besides being one query instead of three, the columns then come from a single
read of the view rather than three taken at different moments.

Test runtime changes in CI:

    platform    perl             pytest           diff
    ----------- ---------------- ---------------- ----------------
    windows       7.8s (±0.1)      3.8s (±0.1)      -4.0s (-52%)
    mingw         6.2s (±0.1)      3.5s (±0.1)      -2.7s (-43%)
    macos         6.0s (±0.3)      4.5s (±0.3)      -1.5s (-25%)
    linux-64      4.4s (±0.0)      3.2s (±0.0)      -1.2s (-28%)
    linux-32      2.7s (±0.0)      2.4s (±0.0)      -0.3s (-12%)

Timings are means of 5 runs of each form, interleaved on one CI runner with
nothing else running on it; ± is the standard deviation across the 5 runs.

LOC (no comments or blanks, with tokei): 264 -> 185 (-30%).
---
 src/test/recovery/meson.build                 |   2 +-
 .../recovery/pyt/test_029_stats_restart.py    | 305 ++++++++++++++
 src/test/recovery/t/029_stats_restart.pl      | 376 ------------------
 3 files changed, 306 insertions(+), 377 deletions(-)
 create mode 100644 src/test/recovery/pyt/test_029_stats_restart.py
 delete mode 100644 src/test/recovery/t/029_stats_restart.pl

diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index 6e4c5a6484b..955f5e6a316 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -7,6 +7,7 @@ tests += {
   'pytest': {
     'test_kwargs': {'priority': 40}, # recovery tests are slow, start early
     'tests': [
+      'pyt/test_029_stats_restart.py',
       'pyt/test_049_wait_for_lsn.py',
     ],
   },
@@ -43,7 +44,6 @@ tests += {
       't/026_overwrite_contrecord.pl',
       't/027_stream_regress.pl',
       't/028_pitr_timelines.pl',
-      't/029_stats_restart.pl',
       't/030_stats_cleanup_replica.pl',
       't/031_recovery_conflict.pl',
       't/032_relfilenode_reuse.pl',
diff --git a/src/test/recovery/pyt/test_029_stats_restart.py b/src/test/recovery/pyt/test_029_stats_restart.py
new file mode 100644
index 00000000000..a3d0204da7c
--- /dev/null
+++ b/src/test/recovery/pyt/test_029_stats_restart.py
@@ -0,0 +1,305 @@
+# Copyright (c) 2021-2026, PostgreSQL Global Development Group
+
+"""Port of src/test/recovery/t/029_stats_restart.pl.
+
+Tests statistics handling around restarts, including handling of crashes and
+invalid stats files, as well as restoring stats after "normal" restarts.
+"""
+
+import datetime
+import pathlib
+import shutil
+from typing import NamedTuple
+
+# The counters each group of checks compares before and after a restart or a
+# pg_stat_reset_shared(). Named rather than dicts so a mistyped field is an
+# AttributeError here instead of a KeyError inside a comparison.
+
+
+class IoStats(NamedTuple):
+    writes: int
+    reads: int
+
+
+class CheckpointStats(NamedTuple):
+    count: int
+    reset: datetime.datetime
+
+
+class WalStats(NamedTuple):
+    records: int
+    wal_bytes: int
+    reset: datetime.datetime
+
+
+def test_stats_restart(create_pg, tmp_path):
+    node = create_pg("primary", allows_streaming=True, conf={"track_functions": "all"})
+
+    db_under_test = "test"
+
+    # node.sql() runs on the default 'postgres' database, which is what the
+    # stats queries need; 'test' is only touched by the workload below, since
+    # connecting to it eagerly would create database stats and break the
+    # "stats were discarded" checks. node.sql() reconnects automatically after
+    # every (re)start, so no manual reconnect is needed.
+
+    def have_stats(kind, dboid, objid):
+        return node.sql("SELECT pg_stat_have_stats($1, $2, $3)", kind, dboid, objid)
+
+    def io_stats(context, obj, backend_type):
+        return IoStats(
+            *node.sql(
+                "SELECT writes, reads FROM pg_stat_io "
+                "WHERE context = $1 AND object = $2 AND backend_type = $3",
+                context,
+                obj,
+                backend_type,
+            )
+        )
+
+    def checkpoint_stats():
+        return CheckpointStats(
+            *node.sql(
+                "SELECT num_timed + num_requested, stats_reset FROM pg_stat_checkpointer"
+            )
+        )
+
+    def wal_stats():
+        return WalStats(
+            *node.sql("SELECT wal_records, wal_bytes, stats_reset FROM pg_stat_wal")
+        )
+
+    def trigger_funcrel_stat():
+        # A fresh connection each time: it must connect to 'test' (generating
+        # the stats under test), and the previous one is dead after a restart.
+        node.connect(dbname=db_under_test).sql_batch(
+            "SELECT * FROM tab_stats_crash_discard_test1",
+            "SELECT func_stats_crash_discard1()",
+            "SELECT pg_stat_force_next_flush()",
+        )
+
+    # Check some WAL statistics after a fresh startup.  The startup process
+    # should have done WAL reads, and initialization some WAL writes.
+    standalone = io_stats("init", "wal", "standalone backend")
+    startup = io_stats("normal", "wal", "startup")
+
+    assert standalone.writes > 0, "startup: increased standalone backend IO writes"
+    assert startup.reads > 0, "startup: increased startup IO reads"
+
+    # create test objects
+    node.sql(f"CREATE DATABASE {db_under_test}")
+    with node.connect(dbname=db_under_test) as dconn:
+        dconn.sql(
+            "CREATE TABLE tab_stats_crash_discard_test1 AS "
+            "SELECT generate_series(1,100) AS a"
+        )
+        dconn.sql(
+            "CREATE FUNCTION func_stats_crash_discard1() RETURNS VOID AS 'select 2;' "
+            "LANGUAGE SQL IMMUTABLE"
+        )
+
+        # collect object oids
+        dboid = dconn.sql(
+            "SELECT oid FROM pg_database WHERE datname = $1", db_under_test
+        )
+        funcoid = dconn.sql("SELECT 'func_stats_crash_discard1()'::regprocedure::oid")
+        tableoid = dconn.sql("SELECT 'tab_stats_crash_discard_test1'::regclass::oid")
+
+    def assert_stats_exist(exist, phase):
+        """Assert the database, function and relation stats are all (not) there.
+
+        Every phase below checks the same three kinds together, so the phase name
+        is an argument rather than a prefix repeated in each message.
+        """
+        for kind, objid in (
+            ("database", 0),
+            ("function", funcoid),
+            ("relation", tableoid),
+        ):
+            assert have_stats(kind, dboid, objid) == exist, f"{phase}: {kind} stats"
+
+    # generate stats and flush them
+    trigger_funcrel_stat()
+
+    # verify stats objects exist
+    assert_stats_exist(True, "initial")
+
+    # regular shutdown
+    node.stop()
+
+    # backup stats files
+    statsfile = tmp_path / "discard_stats1"
+
+    assert not statsfile.exists(), "backup statsfile cannot already exist"
+
+    og_stats = pathlib.Path(node.datadir) / "pg_stat" / "pgstat.stat"
+
+    assert og_stats.is_file(), "origin stats file must exist"
+
+    shutil.copy(og_stats, statsfile)
+
+    ## test discarding of stats file after crash etc
+
+    node.start()
+
+    assert_stats_exist(True, "copy")
+
+    node.stop("immediate")
+
+    assert not og_stats.exists(), "no stats file should exist after immediate shutdown"
+
+    # copy the old stats back to test we discard stats after crash restart
+    shutil.copy(statsfile, og_stats)
+    node.start()
+
+    # stats should have been discarded
+    assert_stats_exist(False, "post immediate")
+
+    # get rid of backup statsfile
+    statsfile.unlink()
+
+    # generate new stats and flush them
+    trigger_funcrel_stat()
+
+    assert_stats_exist(True, "post immediate, new")
+
+    # regular shutdown
+    node.stop()
+
+    ## check an invalid stats file is handled
+
+    # normal startup and no issues despite invalid stats file
+    og_stats.write_text("ZZZZZZZZZZZZZ")
+    node.start()
+
+    # no stats present due to invalid stats file
+    assert_stats_exist(False, "invalid_overwrite")
+
+    ## check invalid stats file starting with valid contents, but followed by
+    ## invalid content is handled.
+
+    trigger_funcrel_stat()
+    node.stop()
+    with open(og_stats, "a") as f:
+        f.write("XYZ")
+    node.start()
+
+    assert_stats_exist(False, "invalid_append")
+
+    ## checks related to stats persistency around restarts and resets
+
+    # Ensure enough checkpoints to protect against races for test after reset,
+    # even on very slow machines.
+    node.sql("CHECKPOINT")
+    node.sql("CHECKPOINT")
+
+    ## check checkpoint and wal stats are incremented due to restart
+
+    ckpt_start = checkpoint_stats()
+    wal_start = wal_stats()
+    node.pg_ctl("restart")
+
+    ckpt_restart = checkpoint_stats()
+    wal_restart = wal_stats()
+
+    assert ckpt_start.count < ckpt_restart.count, (
+        "post restart: increased checkpoint count"
+    )
+
+    assert wal_start.records < wal_restart.records, (
+        "post restart: increased wal record count"
+    )
+
+    assert wal_start.wal_bytes < wal_restart.wal_bytes, (
+        "post restart: increased wal bytes"
+    )
+
+    assert ckpt_start.reset == ckpt_restart.reset, (
+        "post restart: checkpoint stats_reset equal"
+    )
+
+    assert wal_start.reset == wal_restart.reset, "post restart: wal stats_reset equal"
+
+    ## Check that checkpoint stats are reset, WAL stats aren't affected
+
+    node.sql("SELECT pg_stat_reset_shared('checkpointer')")
+    ckpt_reset = checkpoint_stats()
+    wal_ckpt_reset = wal_stats()
+
+    assert ckpt_restart.count > ckpt_reset.count, (
+        "post ckpt reset: checkpoint count smaller"
+    )
+
+    assert ckpt_start.reset < ckpt_reset.reset, "post ckpt reset: stats_reset newer"
+
+    assert wal_restart.records <= wal_ckpt_reset.records, (
+        "post ckpt reset: wal record count not affected by reset"
+    )
+
+    assert wal_start.reset == wal_ckpt_reset.reset, (
+        "post ckpt reset: wal stats_reset equal"
+    )
+
+    ## check that checkpoint stats stay reset after restart
+
+    node.pg_ctl("restart")
+    ckpt_restart_reset = checkpoint_stats()
+    wal_restart2 = wal_stats()
+
+    assert ckpt_restart_reset.count < ckpt_restart.count, (
+        "post ckpt reset & restart: checkpoint still reset"
+    )
+
+    assert ckpt_restart_reset.reset == ckpt_reset.reset, (
+        "post ckpt reset & restart: stats_reset same"
+    )
+
+    assert wal_ckpt_reset.records < wal_restart2.records, (
+        "post ckpt reset & restart: increased wal record count"
+    )
+
+    assert wal_ckpt_reset.wal_bytes < wal_restart2.wal_bytes, (
+        "post ckpt reset & restart: increased wal bytes"
+    )
+
+    assert wal_start.reset == wal_restart2.reset, (
+        "post ckpt reset & restart: wal stats_reset equal"
+    )
+
+    ## check WAL stats stay reset
+
+    node.sql("SELECT pg_stat_reset_shared('wal')")
+    wal_reset = wal_stats()
+
+    assert wal_reset.records < wal_restart2.records, (
+        "post wal reset: smaller record count"
+    )
+
+    assert wal_reset.wal_bytes < wal_restart2.wal_bytes, "post wal reset: smaller bytes"
+    assert wal_reset.reset > wal_restart2.reset, "post wal reset: newer stats_reset"
+
+    node.pg_ctl("restart")
+    wal_reset_restart = wal_stats()
+
+    assert wal_reset_restart.records < wal_restart2.records, (
+        "post wal reset & restart: smaller record count"
+    )
+
+    assert wal_reset.wal_bytes < wal_restart2.wal_bytes, (
+        "post wal reset & restart: smaller bytes"
+    )
+
+    assert wal_reset.reset > wal_restart2.reset, (
+        "post wal reset & restart: newer stats_reset"
+    )
+
+    # An immediate restart bumps the WAL stats_reset timestamp.
+    node.stop("immediate")
+    node.start()
+    wal_restart_immediate = wal_stats()
+
+    assert wal_reset_restart.reset < wal_restart_immediate.reset, (
+        "post immediate restart: reset timestamp is new"
+    )
+
+    node.stop()
diff --git a/src/test/recovery/t/029_stats_restart.pl b/src/test/recovery/t/029_stats_restart.pl
deleted file mode 100644
index cdc427dbc78..00000000000
--- a/src/test/recovery/t/029_stats_restart.pl
+++ /dev/null
@@ -1,376 +0,0 @@
-# Copyright (c) 2021-2026, PostgreSQL Global Development Group
-
-# Tests statistics handling around restarts, including handling of crashes and
-# invalid stats files, as well as restoring stats after "normal" restarts.
-
-use strict;
-use warnings FATAL => 'all';
-use PostgreSQL::Test::Cluster;
-use PostgreSQL::Test::Utils;
-use Test::More;
-use File::Copy;
-
-my $node = PostgreSQL::Test::Cluster->new('primary');
-$node->init(allows_streaming => 1);
-$node->append_conf('postgresql.conf', "track_functions = 'all'");
-$node->start;
-
-my $connect_db = 'postgres';
-my $db_under_test = 'test';
-
-my $sect = "startup";
-
-# Check some WAL statistics after a fresh startup.  The startup process
-# should have done WAL reads, and initialization some WAL writes.
-my $standalone_io_stats = io_stats('init', 'wal', 'standalone backend');
-my $startup_io_stats = io_stats('normal', 'wal', 'startup');
-cmp_ok(
-	'0', '<',
-	$standalone_io_stats->{writes},
-	"$sect: increased standalone backend IO writes");
-cmp_ok(
-	'0', '<',
-	$startup_io_stats->{reads},
-	"$sect: increased startup IO reads");
-
-# create test objects
-$node->safe_psql($connect_db, "CREATE DATABASE $db_under_test");
-$node->safe_psql($db_under_test,
-	"CREATE TABLE tab_stats_crash_discard_test1 AS SELECT generate_series(1,100) AS a"
-);
-$node->safe_psql($db_under_test,
-	"CREATE FUNCTION func_stats_crash_discard1() RETURNS VOID AS 'select 2;' LANGUAGE SQL IMMUTABLE"
-);
-
-# collect object oids
-my $dboid = $node->safe_psql($db_under_test,
-	"SELECT oid FROM pg_database WHERE datname = '$db_under_test'");
-my $funcoid = $node->safe_psql($db_under_test,
-	"SELECT 'func_stats_crash_discard1()'::regprocedure::oid");
-my $tableoid = $node->safe_psql($db_under_test,
-	"SELECT 'tab_stats_crash_discard_test1'::regclass::oid");
-
-# generate stats and flush them
-trigger_funcrel_stat();
-
-# verify stats objects exist
-$sect = "initial";
-is(have_stats('database', $dboid, 0), 't', "$sect: db stats do exist");
-is(have_stats('function', $dboid, $funcoid),
-	't', "$sect: function stats do exist");
-is(have_stats('relation', $dboid, $tableoid),
-	't', "$sect: relation stats do exist");
-
-# regular shutdown
-$node->stop();
-
-# backup stats files
-my $statsfile = $PostgreSQL::Test::Utils::tmp_check . '/' . "discard_stats1";
-ok(!-f "$statsfile", "backup statsfile cannot already exist");
-
-my $datadir = $node->data_dir();
-my $og_stats = "$datadir/pg_stat/pgstat.stat";
-ok(-f "$og_stats", "origin stats file must exist");
-copy($og_stats, $statsfile) or die "Copy failed: $!";
-
-
-## test discarding of stats file after crash etc
-
-$node->start;
-
-$sect = "copy";
-is(have_stats('database', $dboid, 0), 't', "$sect: db stats do exist");
-is(have_stats('function', $dboid, $funcoid),
-	't', "$sect: function stats do exist");
-is(have_stats('relation', $dboid, $tableoid),
-	't', "$sect: relation stats do exist");
-
-$node->stop('immediate');
-
-ok(!-f "$og_stats", "no stats file should exist after immediate shutdown");
-
-# copy the old stats back to test we discard stats after crash restart
-copy($statsfile, $og_stats) or die "Copy failed: $!";
-
-$node->start;
-
-# stats should have been discarded
-$sect = "post immediate";
-is(have_stats('database', $dboid, 0), 'f', "$sect: db stats do not exist");
-is(have_stats('function', $dboid, $funcoid),
-	'f', "$sect: function stats do exist");
-is(have_stats('relation', $dboid, $tableoid),
-	'f', "$sect: relation stats do not exist");
-
-# get rid of backup statsfile
-unlink $statsfile or die "cannot unlink $statsfile $!";
-
-
-# generate new stats and flush them
-trigger_funcrel_stat();
-
-$sect = "post immediate, new";
-is(have_stats('database', $dboid, 0), 't', "$sect: db stats do exist");
-is(have_stats('function', $dboid, $funcoid),
-	't', "$sect: function stats do exist");
-is(have_stats('relation', $dboid, $tableoid),
-	't', "$sect: relation stats do exist");
-
-# regular shutdown
-$node->stop();
-
-
-## check an invalid stats file is handled
-
-overwrite_file($og_stats, "ZZZZZZZZZZZZZ");
-
-# normal startup and no issues despite invalid stats file
-$node->start;
-
-# no stats present due to invalid stats file
-$sect = "invalid_overwrite";
-is(have_stats('database', $dboid, 0), 'f', "$sect: db stats do not exist");
-is(have_stats('function', $dboid, $funcoid),
-	'f', "$sect: function stats do not exist");
-is(have_stats('relation', $dboid, $tableoid),
-	'f', "$sect: relation stats do not exist");
-
-
-## check invalid stats file starting with valid contents, but followed by
-## invalid content is handled.
-
-trigger_funcrel_stat();
-$node->stop;
-append_file($og_stats, "XYZ");
-$node->start;
-
-$sect = "invalid_append";
-is(have_stats('database', $dboid, 0), 'f', "$sect: db stats do not exist");
-is(have_stats('function', $dboid, $funcoid),
-	'f', "$sect: function stats do not exist");
-is(have_stats('relation', $dboid, $tableoid),
-	'f', "$sect: relation stats do not exist");
-
-
-## checks related to stats persistency around restarts and resets
-
-# Ensure enough checkpoints to protect against races for test after reset,
-# even on very slow machines.
-$node->safe_psql($connect_db, "CHECKPOINT; CHECKPOINT;");
-
-
-## check checkpoint and wal stats are incremented due to restart
-
-my $ckpt_start = checkpoint_stats();
-my $wal_start = wal_stats();
-$node->restart;
-
-$sect = "post restart";
-my $ckpt_restart = checkpoint_stats();
-my $wal_restart = wal_stats();
-
-cmp_ok(
-	$ckpt_start->{count}, '<',
-	$ckpt_restart->{count},
-	"$sect: increased checkpoint count");
-cmp_ok(
-	$wal_start->{records}, '<',
-	$wal_restart->{records},
-	"$sect: increased wal record count");
-cmp_ok($wal_start->{bytes}, '<', $wal_restart->{bytes},
-	"$sect: increased wal bytes");
-is( $ckpt_start->{reset},
-	$ckpt_restart->{reset},
-	"$sect: checkpoint stats_reset equal");
-is($wal_start->{reset}, $wal_restart->{reset},
-	"$sect: wal stats_reset equal");
-
-
-## Check that checkpoint stats are reset, WAL stats aren't affected
-
-$node->safe_psql($connect_db, "SELECT pg_stat_reset_shared('checkpointer')");
-
-$sect = "post ckpt reset";
-my $ckpt_reset = checkpoint_stats();
-my $wal_ckpt_reset = wal_stats();
-
-cmp_ok($ckpt_restart->{count},
-	'>', $ckpt_reset->{count}, "$sect: checkpoint count smaller");
-cmp_ok($ckpt_start->{reset}, 'lt', $ckpt_reset->{reset},
-	"$sect: stats_reset newer");
-
-cmp_ok(
-	$wal_restart->{records},
-	'<=',
-	$wal_ckpt_reset->{records},
-	"$sect: wal record count not affected by reset");
-is( $wal_start->{reset},
-	$wal_ckpt_reset->{reset},
-	"$sect: wal stats_reset equal");
-
-
-## check that checkpoint stats stay reset after restart
-
-$node->restart;
-
-$sect = "post ckpt reset & restart";
-my $ckpt_restart_reset = checkpoint_stats();
-my $wal_restart2 = wal_stats();
-
-# made sure above there's enough checkpoints that this will be stable even on slow machines
-cmp_ok(
-	$ckpt_restart_reset->{count},
-	'<',
-	$ckpt_restart->{count},
-	"$sect: checkpoint still reset");
-is($ckpt_restart_reset->{reset},
-	$ckpt_reset->{reset}, "$sect: stats_reset same");
-
-cmp_ok(
-	$wal_ckpt_reset->{records},
-	'<',
-	$wal_restart2->{records},
-	"$sect: increased wal record count");
-cmp_ok(
-	$wal_ckpt_reset->{bytes},
-	'<',
-	$wal_restart2->{bytes},
-	"$sect: increased wal bytes");
-is( $wal_start->{reset},
-	$wal_restart2->{reset},
-	"$sect: wal stats_reset equal");
-
-
-## check WAL stats stay reset
-
-$node->safe_psql($connect_db, "SELECT pg_stat_reset_shared('wal')");
-
-$sect = "post wal reset";
-my $wal_reset = wal_stats();
-
-cmp_ok(
-	$wal_reset->{records}, '<',
-	$wal_restart2->{records},
-	"$sect: smaller record count");
-cmp_ok(
-	$wal_reset->{bytes}, '<',
-	$wal_restart2->{bytes},
-	"$sect: smaller bytes");
-cmp_ok(
-	$wal_reset->{reset}, 'gt',
-	$wal_restart2->{reset},
-	"$sect: newer stats_reset");
-
-$node->restart;
-
-$sect = "post wal reset & restart";
-my $wal_reset_restart = wal_stats();
-
-# enough WAL generated during prior tests and initdb to make this not racy
-cmp_ok(
-	$wal_reset_restart->{records},
-	'<',
-	$wal_restart2->{records},
-	"$sect: smaller record count");
-cmp_ok(
-	$wal_reset->{bytes}, '<',
-	$wal_restart2->{bytes},
-	"$sect: smaller bytes");
-cmp_ok(
-	$wal_reset->{reset}, 'gt',
-	$wal_restart2->{reset},
-	"$sect: newer stats_reset");
-
-$node->stop('immediate');
-$node->start;
-
-$sect = "post immediate restart";
-my $wal_restart_immediate = wal_stats();
-
-cmp_ok(
-	$wal_reset_restart->{reset},
-	'lt',
-	$wal_restart_immediate->{reset},
-	"$sect: reset timestamp is new");
-
-$node->stop;
-done_testing();
-
-sub trigger_funcrel_stat
-{
-	$node->safe_psql(
-		$db_under_test, q[
-	SELECT * FROM tab_stats_crash_discard_test1;
-	SELECT func_stats_crash_discard1();
-    SELECT pg_stat_force_next_flush();]);
-}
-
-sub have_stats
-{
-	my ($kind, $dboid, $objid) = @_;
-
-	return $node->safe_psql($connect_db,
-		"SELECT pg_stat_have_stats('$kind', $dboid, $objid)");
-}
-
-sub overwrite_file
-{
-	my ($filename, $str) = @_;
-	open my $fh, ">", $filename
-	  or die "could not overwrite \"$filename\": $!";
-	print $fh $str;
-	close $fh;
-	return;
-}
-
-sub append_file
-{
-	my ($filename, $str) = @_;
-	open my $fh, ">>", $filename
-	  or die "could not append to \"$filename\": $!";
-	print $fh $str;
-	close $fh;
-	return;
-}
-
-sub checkpoint_stats
-{
-	my %results;
-
-	$results{count} = $node->safe_psql($connect_db,
-		"SELECT num_timed + num_requested FROM pg_stat_checkpointer");
-	$results{reset} = $node->safe_psql($connect_db,
-		"SELECT stats_reset FROM pg_stat_checkpointer");
-
-	return \%results;
-}
-
-sub wal_stats
-{
-	my %results;
-	$results{records} =
-	  $node->safe_psql($connect_db, "SELECT wal_records FROM pg_stat_wal");
-	$results{bytes} =
-	  $node->safe_psql($connect_db, "SELECT wal_bytes FROM pg_stat_wal");
-	$results{reset} =
-	  $node->safe_psql($connect_db, "SELECT stats_reset FROM pg_stat_wal");
-
-	return \%results;
-}
-
-sub io_stats
-{
-	my ($context, $object, $backend_type) = @_;
-	my %results;
-
-	$results{writes} = $node->safe_psql(
-		$connect_db, qq{SELECT writes FROM pg_stat_io
-  WHERE context = '$context' AND object = '$object' AND
-    backend_type = '$backend_type'});
-	$results{reads} = $node->safe_psql(
-		$connect_db, qq{SELECT reads FROM pg_stat_io
-  WHERE context = '$context' AND object = '$object' AND
-    backend_type = '$backend_type'});
-
-	return \%results;
-}
-- 
2.54.0

