From 77a162f968c36e18890d0f845a8ebbb501769c10 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Fri, 14 Feb 2025 15:11:23 +0100
Subject: [PATCH v7] psql: Make default \watch interval configurable

The default interval for \watch to wait between executing queries,
when executed without a specified interval, was hardcoded to two
seconds.  This adds the new variable WATCH_INTERVAL which is used
to set the default interval, making it configurable for the user.

Author: Daniel Gustafsson <daniel@yesql.se>
Reviewed-by: Heikki Linnakangas <hlinnaka@iki.fi>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Kirill Reshke <reshkekirill@gmail.com>
Reviewed-by: Masahiro Ikeda <ikedamsh@oss.nttdata.com>
Reviewed-by: Laurenz Albe <laurenz.albe@cybertec.at>
Reviewed-by: Greg Sabino Mullane <htamfids@gmail.com>
Reviewed-by: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Discussion: https://postgr.es/m/B2FD26B4-8F64-4552-A603-5CC3DF1C7103@yesql.se
---
 doc/src/sgml/ref/psql-ref.sgml | 16 +++++++-
 src/bin/psql/command.c         |  6 ++-
 src/bin/psql/help.c            |  2 +
 src/bin/psql/settings.h        |  7 ++++
 src/bin/psql/startup.c         | 18 +++++++++
 src/bin/psql/t/001_basic.pl    | 24 ++++++++++++
 src/bin/psql/variables.c       | 70 ++++++++++++++++++++++++++++++++++
 src/bin/psql/variables.h       |  3 ++
 8 files changed, 142 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index f083dba49a9..5ead1c784a0 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -3851,8 +3851,9 @@ SELECT 1 \bind \sendpipeline
         Repeatedly execute the current query buffer (as <literal>\g</literal> does)
         until interrupted, or the query fails, or the execution count limit
         (if given) is reached, or the query no longer returns the minimum number
-        of rows. Wait the specified number of seconds (default 2) between executions.
-        For backwards compatibility,
+        of rows.  Wait the specified number of seconds (defaults to 2 if omitted, which can be
+        changed with the variable <xref linkend="app-psql-variables-watch-interval"/>)
+        between executions.  For backwards compatibility,
         <replaceable class="parameter">seconds</replaceable> can be specified
         with or without an <literal>interval=</literal> prefix.
         Each query result is
@@ -4746,6 +4747,17 @@ bar
         </listitem>
       </varlistentry>
 
+      <varlistentry id="app-psql-variables-watch-interval">
+        <term><varname>WATCH_INTERVAL</varname></term>
+        <listitem>
+        <para>
+        This variable sets the default interval which <command>\watch</command>
+        waits between executing the query.  Specifying an interval in the
+        command overrides this variable.
+        </para>
+        </listitem>
+      </varlistentry>
+
     </variablelist>
 
    </refsect3>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index bbe337780ff..015ece77aec 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -3278,7 +3278,7 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 		bool		have_sleep = false;
 		bool		have_iter = false;
 		bool		have_min_rows = false;
-		double		sleep = 2;
+		double		sleep = pset.watch_interval;
 		int			iter = 0;
 		int			min_rows = 0;
 
@@ -3292,7 +3292,9 @@ exec_command_watch(PsqlScanState scan_state, bool active_branch,
 		/*
 		 * Parse arguments.  We allow either an unlabeled interval or
 		 * "name=value", where name is from the set ('i', 'interval', 'c',
-		 * 'count', 'm', 'min_rows').
+		 * 'count', 'm', 'min_rows').  The parsing of interval value should
+		 * be kept in sync with ParseVariableDouble which is used for setting
+		 * the default interval value.
 		 */
 		while (success)
 		{
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index e47cad24de9..30e3792d263 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -460,6 +460,8 @@ helpVariables(unsigned short int pager)
 		  "  VERSION_NAME\n"
 		  "  VERSION_NUM\n"
 		  "    psql's version (in verbose string, short string, or numeric format)\n");
+	HELP0("  WATCH_INTERVAL\n"
+		  "    number of seconds \\watch waits between executing the query buffer\n");
 
 	HELP0("\nDisplay settings:\n");
 	HELP0("Usage:\n");
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 71f553c22ad..fd82303f776 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -27,6 +27,12 @@
 #define DEFAULT_PROMPT2 "%/%R%x%# "
 #define DEFAULT_PROMPT3 ">> "
 
+#define DEFAULT_WATCH_INTERVAL "2"
+/*
+ * Limit the max default setting to a value which should be safe for the
+ * itimer call, yet large enough to cover all realistic usecases.
+ */
+#define DEFAULT_WATCH_INTERVAL_MAX (1000*1000)
 /*
  * Note: these enums should generally be chosen so that zero corresponds
  * to the default behavior.
@@ -166,6 +172,7 @@ typedef struct _psqlSettings
 	int			fetch_count;
 	int			histsize;
 	int			ignoreeof;
+	double		watch_interval;
 	PSQL_ECHO	echo;
 	PSQL_ECHO_HIDDEN echo_hidden;
 	PSQL_ERROR_ROLLBACK on_error_rollback;
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 5018eedf1e5..249b6aa5169 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -944,6 +944,21 @@ histsize_hook(const char *newval)
 	return ParseVariableNum(newval, "HISTSIZE", &pset.histsize);
 }
 
+static char *
+watch_interval_substitute_hook(char *newval)
+{
+	if (newval == NULL)
+		newval = pg_strdup(DEFAULT_WATCH_INTERVAL);
+	return newval;
+}
+
+static bool
+watch_interval_hook(const char *newval)
+{
+	return ParseVariableDouble(newval, "WATCH_INTERVAL", &pset.watch_interval,
+							   0, DEFAULT_WATCH_INTERVAL_MAX);
+}
+
 static char *
 ignoreeof_substitute_hook(char *newval)
 {
@@ -1270,4 +1285,7 @@ EstablishVariableSpace(void)
 	SetVariableHooks(pset.vars, "HIDE_TABLEAM",
 					 bool_substitute_hook,
 					 hide_tableam_hook);
+	SetVariableHooks(pset.vars, "WATCH_INTERVAL",
+					 watch_interval_substitute_hook,
+					 watch_interval_hook);
 }
diff --git a/src/bin/psql/t/001_basic.pl b/src/bin/psql/t/001_basic.pl
index dca34ac975a..7192d96049d 100644
--- a/src/bin/psql/t/001_basic.pl
+++ b/src/bin/psql/t/001_basic.pl
@@ -375,6 +375,12 @@ psql_like(
 	$node, sprintf('SELECT 1 \watch c=3 i=%g', 0.0001),
 	qr/1\n1\n1/, '\watch with 3 iterations, interval of 0.0001');
 
+# Test zero interval
+psql_like(
+	$node, '\set WATCH_INTERVAL 0
+SELECT 1 \watch c=3',
+	qr/1\n1\n1/, '\watch with 3 iterations, interval of 0');
+
 # Check \watch minimum row count
 psql_fails_like(
 	$node,
@@ -426,6 +432,24 @@ psql_fails_like(
 	qr/iteration count is specified more than once/,
 	'\watch, iteration count is specified more than once');
 
+# Check WATCH_INTERVAL
+psql_like(
+	$node,
+	'\echo :WATCH_INTERVAL
+\set WATCH_INTERVAL 0.001
+\echo :WATCH_INTERVAL
+\unset WATCH_INTERVAL
+\echo :WATCH_INTERVAL',
+	qr/^2$
+^0.001$
+^2$/m,
+	'WATCH_INTERVAL variable is set and updated');
+psql_fails_like(
+	$node,
+	'\set WATCH_INTERVAL 1e500',
+	qr/is out of range/,
+	'WATCH_INTERVAL variable is out of range');
+
 # Test \g output piped into a program.
 # The program is perl -pe '' to simply copy the input to the output.
 my $g_file = "$tempdir/g_file_1.out";
diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c
index 59956028918..5150eb0532b 100644
--- a/src/bin/psql/variables.c
+++ b/src/bin/psql/variables.c
@@ -7,6 +7,8 @@
  */
 #include "postgres_fe.h"
 
+#include <math.h>
+
 #include "common.h"
 #include "common/logging.h"
 #include "variables.h"
@@ -179,6 +181,74 @@ ParseVariableNum(const char *value, const char *name, int *result)
 	}
 }
 
+/*
+ * Try to interpret "value" as a double value, and if successful store it in
+ * *result. If unsuccessful, *result isn't clobbered. "name" is the variable
+ * which is being assigned, the value of which is only used to produce a good
+ * error message. Pass NULL as the name to suppress the error message.  The
+ * value must be within the range [min,max] in order to be considered valid.
+ *
+ * Returns true, with *result containing the interpreted value, if "value" is
+ * syntactically valid, else false (with *result unchanged).
+ */
+bool
+ParseVariableDouble(const char *value, const char *name, double *result, double min, double max)
+{
+	char	   *end;
+	double		dblval;
+
+	/*
+	 * Empty-string input has historically been treated differently by strtod
+	 * on various platforms, so handle that by specifically checking for it.
+	 */
+	if ((value == NULL) || (*value == '\0'))
+	{
+		if (name)
+			pg_log_error("invalid input syntax for \"%s\"", name);
+		return false;
+	}
+
+	errno = 0;
+	dblval = strtod(value, &end);
+	if (errno == 0 && *end == '\0' && end != value)
+	{
+		if (dblval < min)
+		{
+			if (name)
+				pg_log_error("invalid value \"%s\" for \"%s\": must be greater than %.2f",
+							 value, name, min);
+			return false;
+		}
+		else if (dblval > max)
+		{
+			if (name)
+				pg_log_error("invalid value \"%s\" for \"%s\": must be less than %.2f",
+							 value, name, max);
+		}
+		*result = dblval;
+		return true;
+	}
+
+	/*
+	 * Cater for platforms which treat values which aren't zero, but that are
+	 * too close to zero to have full precision, by checking for zero or real
+	 * out-of-range values.
+	 */
+	else if ((errno = ERANGE) &&
+			 (dblval == 0.0 || dblval >= HUGE_VAL || dblval <= -HUGE_VAL))
+	{
+		if (name)
+			pg_log_error("\"%s\" is out of range for \"%s\"", value, name);
+		return false;
+	}
+	else
+	{
+		if (name)
+			pg_log_error("invalid value \"%s\" for \"%s\"", value, name);
+		return false;
+	}
+}
+
 /*
  * Print values of all variables.
  */
diff --git a/src/bin/psql/variables.h b/src/bin/psql/variables.h
index a95bc29f407..df23ccb987d 100644
--- a/src/bin/psql/variables.h
+++ b/src/bin/psql/variables.h
@@ -81,6 +81,9 @@ bool		ParseVariableBool(const char *value, const char *name,
 bool		ParseVariableNum(const char *value, const char *name,
 							 int *result);
 
+bool		ParseVariableDouble(const char *value, const char *name,
+								double *result, double min, double max);
+
 void		PrintVariables(VariableSpace space);
 
 bool		SetVariable(VariableSpace space, const char *name, const char *value);
-- 
2.39.3 (Apple Git-146)

