Improvements in psql hooks for variables
Hi,
Following the discussion on forbidding an AUTOCOMMIT off->on
switch mid-transaction [1]/messages/by-id/f2cb5838-0ee9-4fe3-acc0-df77aeb7d4c7@mm, attached is a patch that let the hooks
return a boolean indicating whether a change is allowed.
Using the hooks, bogus assignments to built-in variables can
be dealt with more strictly.
For example, pre-patch behavior:
=# \set ECHO errors
=# \set ECHO on
unrecognized value "on" for "ECHO"; assuming "none"
=# \echo :ECHO
on
which has two problems:
- we have to assume a value, even though we can't know what the user meant.
- after assignment, the user-visible value of the variable diverges from its
internal counterpart (pset.echo in this case).
Post-patch:
=# \set ECHO errors
=# \set ECHO on
unrecognized value "on" for "ECHO"
\set: error while setting variable
=# \echo :ECHO
errors
Both the internal pset.* state and the user-visible value are kept unchanged
is the input value is incorrect.
Concerning AUTOCOMMIT, autocommit_hook() could return false to forbid
a switch when the conditions are not met.
Another user-visible effect of the patch is that, using a bogus value
for a built-in variable on the command-line becomes a fatal error
that prevents psql to continue.
This is not directly intended by the patch but is a consequence
of SetVariable() failing.
Example:
$ ./psql -vECHO=bogus
unrecognized value "bogus" for "ECHO"
psql: could not set variable "ECHO"
$ echo $?
1
The built-in vars concerned by the change are:
booleans: AUTOCOMMIT, ON_ERROR_STOP, QUIET, SINGLELINE, SINGLESTEP
non-booleans: ECHO, ECHO_HIDDEN, ON_ERROR_ROLLBACK, COMP_KEYWORD_CASE,
HISTCONTROL, VERBOSITY, SHOW_CONTEXT
We could go further to close the gap between pset.* and the built-in
variables,
by changing how they're initialized and forbidding deletion as Tom
suggests in [2]/messages/by-id/4695.1473961140@sss.pgh.pa.us, but if there's negative feedback on the above changes,
I think we should hear it first.
[1]: /messages/by-id/f2cb5838-0ee9-4fe3-acc0-df77aeb7d4c7@mm
/messages/by-id/f2cb5838-0ee9-4fe3-acc0-df77aeb7d4c7@mm
[2]: /messages/by-id/4695.1473961140@sss.pgh.pa.us
/messages/by-id/4695.1473961140@sss.pgh.pa.us
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
Attachments:
psql-var-hooks-v1.patchtext/plainDownload
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 7ce05fb..9dcfc0a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -786,43 +786,59 @@ showVersion(void)
* This isn't an amazingly good place for them, but neither is anywhere else.
*/
-static void
+/*
+ * Hook to set an internal flag from a user-supplied string value.
+ * If the syntax is correct, affect *flag and return true.
+ * Otherwise, keep *flag untouched and return false.
+ */
+static bool
+generic_boolean_hook(const char *newval, const char* varname, bool *flag)
+{
+ bool isvalid;
+ bool val = ParseVariableBool(newval, varname, &isvalid);
+ if (isvalid)
+ *flag = val;
+ return isvalid;
+}
+
+static bool
autocommit_hook(const char *newval)
{
- pset.autocommit = ParseVariableBool(newval, "AUTOCOMMIT");
+ return generic_boolean_hook(newval, "AUTOCOMMIT", &pset.autocommit);
}
-static void
+static bool
on_error_stop_hook(const char *newval)
{
- pset.on_error_stop = ParseVariableBool(newval, "ON_ERROR_STOP");
+ return generic_boolean_hook(newval, "ON_ERROR_STOP", &pset.on_error_stop);
}
-static void
+static bool
quiet_hook(const char *newval)
{
- pset.quiet = ParseVariableBool(newval, "QUIET");
+ return generic_boolean_hook(newval, "QUIET", &pset.quiet);
}
-static void
+static bool
singleline_hook(const char *newval)
{
- pset.singleline = ParseVariableBool(newval, "SINGLELINE");
+ return generic_boolean_hook(newval, "SINGLELINE", &pset.singleline);
}
-static void
+static bool
singlestep_hook(const char *newval)
{
- pset.singlestep = ParseVariableBool(newval, "SINGLESTEP");
+ return generic_boolean_hook(newval, "SINGLESTEP", &pset.singlestep);
}
-static void
+static bool
fetch_count_hook(const char *newval)
{
pset.fetch_count = ParseVariableNum(newval, -1, -1, false);
+ return true;
}
-static void
+static bool
echo_hook(const char *newval)
{
if (newval == NULL)
@@ -837,39 +853,52 @@ echo_hook(const char *newval)
pset.echo = PSQL_ECHO_NONE;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "ECHO", "none");
- pset.echo = PSQL_ECHO_NONE;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "ECHO");
+ return false;
}
+ return true;
}
-static void
+static bool
echo_hidden_hook(const char *newval)
{
if (newval == NULL)
pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
else if (pg_strcasecmp(newval, "noexec") == 0)
pset.echo_hidden = PSQL_ECHO_HIDDEN_NOEXEC;
- else if (ParseVariableBool(newval, "ECHO_HIDDEN"))
- pset.echo_hidden = PSQL_ECHO_HIDDEN_ON;
- else /* ParseVariableBool printed msg if needed */
- pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
+ else
+ {
+ bool isvalid;
+ bool val = ParseVariableBool(newval, "ECHO_HIDDEN", &isvalid);
+ if (!isvalid)
+ return false; /* ParseVariableBool printed msg */
+ pset.echo_hidden = val ? PSQL_ECHO_HIDDEN_ON : PSQL_ECHO_HIDDEN_OFF;
+ }
+ return true;
}
-static void
+static bool
on_error_rollback_hook(const char *newval)
{
if (newval == NULL)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
else if (pg_strcasecmp(newval, "interactive") == 0)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_INTERACTIVE;
- else if (ParseVariableBool(newval, "ON_ERROR_ROLLBACK"))
- pset.on_error_rollback = PSQL_ERROR_ROLLBACK_ON;
- else /* ParseVariableBool printed msg if needed */
- pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
+ else
+ {
+ bool isvalid;
+ bool val = ParseVariableBool(newval, "ON_ERROR_ROLLBACK", &isvalid);
+ if (isvalid)
+ pset.on_error_rollback = val ? PSQL_ERROR_ROLLBACK_ON : PSQL_ERROR_ROLLBACK_OFF;
+ else
+ /* ParseVariableBool printed msg if needed */
+ return false;
+ }
+ return true;
}
-static void
+static bool
comp_keyword_case_hook(const char *newval)
{
if (newval == NULL)
@@ -884,13 +913,14 @@ comp_keyword_case_hook(const char *newval)
pset.comp_case = PSQL_COMP_CASE_LOWER;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "COMP_KEYWORD_CASE", "preserve-upper");
- pset.comp_case = PSQL_COMP_CASE_PRESERVE_UPPER;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "COMP_KEYWORD_CASE");
+ return false;
}
+ return true;
}
-static void
+static bool
histcontrol_hook(const char *newval)
{
if (newval == NULL)
@@ -905,31 +935,35 @@ histcontrol_hook(const char *newval)
pset.histcontrol = hctl_none;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "HISTCONTROL", "none");
- pset.histcontrol = hctl_none;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "HISTCONTROL");
+ return false;
}
+ return true;
}
-static void
+static bool
prompt1_hook(const char *newval)
{
pset.prompt1 = newval ? newval : "";
+ return true;
}
-static void
+static bool
prompt2_hook(const char *newval)
{
pset.prompt2 = newval ? newval : "";
+ return true;
}
-static void
+static bool
prompt3_hook(const char *newval)
{
pset.prompt3 = newval ? newval : "";
+ return true;
}
-static void
+static bool
verbosity_hook(const char *newval)
{
if (newval == NULL)
@@ -942,16 +976,17 @@ verbosity_hook(const char *newval)
pset.verbosity = PQERRORS_VERBOSE;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "VERBOSITY", "default");
- pset.verbosity = PQERRORS_DEFAULT;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "VERBOSITY");
+ return false;
}
if (pset.db)
PQsetErrorVerbosity(pset.db, pset.verbosity);
+ return true;
}
-static void
+static bool
show_context_hook(const char *newval)
{
if (newval == NULL)
@@ -964,13 +999,14 @@ show_context_hook(const char *newval)
pset.show_context = PQSHOW_CONTEXT_ALWAYS;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "SHOW_CONTEXT", "errors");
- pset.show_context = PQSHOW_CONTEXT_ERRORS;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "SHOW_CONTEXT");
+ return false;
}
if (pset.db)
PQsetErrorContextVisibility(pset.db, pset.show_context);
+ return true;
}
diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c
index f43f418..4f817c3 100644
--- a/src/bin/psql/variables.c
+++ b/src/bin/psql/variables.c
@@ -86,12 +86,16 @@ GetVariable(VariableSpace space, const char *name)
*
* "name" is the name of the variable we're assigning to, to use in error
* report if any. Pass name == NULL to suppress the error report.
+ *
+ * "*valid" reports whether "value" was syntactically valid, unless valid == NULL
*/
bool
-ParseVariableBool(const char *value, const char *name)
+ParseVariableBool(const char *value, const char *name, bool *valid)
{
size_t len;
+ if (valid)
+ *valid = true;
if (value == NULL)
return false; /* not set -> assume "off" */
@@ -116,10 +120,16 @@ ParseVariableBool(const char *value, const char *name)
return false;
else
{
- /* NULL is treated as false, so a non-matching value is 'true' */
+ /*
+ * NULL is treated as false, so a non-matching value is 'true'.
+ * A caller that cares about syntactic conformance should
+ * check *valid to know whether the value was recognized.
+ */
if (name)
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- value, name, "on");
+ psql_error("unrecognized value \"%s\" for \"%s\": boolean expected\n",
+ value, name);
+ if (*valid)
+ *valid = false;
return true;
}
}
@@ -205,13 +215,19 @@ SetVariable(VariableSpace space, const char *name, const char *value)
{
if (strcmp(current->name, name) == 0)
{
- /* found entry, so update */
- if (current->value)
- free(current->value);
- current->value = pg_strdup(value);
+ /* found entry, so update, unless a hook returns false */
+ bool confirmed = true;
if (current->assign_hook)
- (*current->assign_hook) (current->value);
- return true;
+ {
+ confirmed = (*current->assign_hook) (value);
+ }
+ if (confirmed)
+ {
+ if (current->value)
+ free(current->value);
+ current->value = pg_strdup(value);
+ }
+ return confirmed;
}
}
@@ -248,7 +264,7 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
{
/* found entry, so update */
current->assign_hook = hook;
- (*hook) (current->value);
+ (void)(*hook) (current->value); /* ignore return value */
return true;
}
}
@@ -260,7 +276,7 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
current->assign_hook = hook;
current->next = NULL;
previous->next = current;
- (*hook) (NULL);
+ (void)(*hook) (NULL); /* ignore return value */
return true;
}
diff --git a/src/bin/psql/variables.h b/src/bin/psql/variables.h
index d7a05a1..9836fc5 100644
--- a/src/bin/psql/variables.h
+++ b/src/bin/psql/variables.h
@@ -20,7 +20,7 @@
* Note: if value == NULL then the variable is logically unset, but we are
* keeping the struct around so as not to forget about its hook function.
*/
-typedef void (*VariableAssignHook) (const char *newval);
+typedef bool (*VariableAssignHook) (const char *newval);
struct _variable
{
@@ -35,7 +35,7 @@ typedef struct _variable *VariableSpace;
VariableSpace CreateVariableSpace(void);
const char *GetVariable(VariableSpace space, const char *name);
-bool ParseVariableBool(const char *value, const char *name);
+bool ParseVariableBool(const char *value, const char *name, bool *valid);
int ParseVariableNum(const char *val,
int defaultval,
int faultval,
You may want to add this to the November commitfest
https://commitfest.postgresql.org/11/.
On Fri, Sep 16, 2016 at 6:05 PM, Daniel Verite <daniel@manitou-mail.org> wrote:
Hi,
Following the discussion on forbidding an AUTOCOMMIT off->on
switch mid-transaction [1], attached is a patch that let the hooks
return a boolean indicating whether a change is allowed.Using the hooks, bogus assignments to built-in variables can
be dealt with more strictly.For example, pre-patch behavior:
=# \set ECHO errors
=# \set ECHO on
unrecognized value "on" for "ECHO"; assuming "none"
=# \echo :ECHO
onwhich has two problems:
- we have to assume a value, even though we can't know what the user meant.
- after assignment, the user-visible value of the variable diverges from its
internal counterpart (pset.echo in this case).Post-patch:
=# \set ECHO errors
=# \set ECHO on
unrecognized value "on" for "ECHO"
\set: error while setting variable
=# \echo :ECHO
errorsBoth the internal pset.* state and the user-visible value are kept unchanged
is the input value is incorrect.Concerning AUTOCOMMIT, autocommit_hook() could return false to forbid
a switch when the conditions are not met.Another user-visible effect of the patch is that, using a bogus value
for a built-in variable on the command-line becomes a fatal error
that prevents psql to continue.
This is not directly intended by the patch but is a consequence
of SetVariable() failing.Example:
$ ./psql -vECHO=bogus
unrecognized value "bogus" for "ECHO"
psql: could not set variable "ECHO"
$ echo $?
1The built-in vars concerned by the change are:
booleans: AUTOCOMMIT, ON_ERROR_STOP, QUIET, SINGLELINE, SINGLESTEP
non-booleans: ECHO, ECHO_HIDDEN, ON_ERROR_ROLLBACK, COMP_KEYWORD_CASE,
HISTCONTROL, VERBOSITY, SHOW_CONTEXTWe could go further to close the gap between pset.* and the built-in
variables,
by changing how they're initialized and forbidding deletion as Tom
suggests in [2], but if there's negative feedback on the above changes,
I think we should hear it first.[1]
/messages/by-id/f2cb5838-0ee9-4fe3-acc0-df77aeb7d4c7@mm
[2]
/messages/by-id/4695.1473961140@sss.pgh.pa.usBest regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
--
Best Wishes,
Ashutosh Bapat
EnterpriseDB Corporation
The Postgres Database Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello,
I am beginning to review this patch. Initial comment. I got following
compilation error when I applied the patch on latest sources and run make.
command.c: In function ‘exec_command’:
*command.c:257:5*: error: too few arguments to function ‘ParseVariableBool’
ParseVariableBool(opt1 + sizeof(prefix) - 1, prefix) ?
^
In file included from settings.h:12:0,
from command.c:50:
variables.h:38:7: note: declared here
bool ParseVariableBool(const char *value, const char *name, bool *valid);
^
*command.c:1551:4*: error: too few arguments to function ‘ParseVariableBool’
pset.timing = ParseVariableBool(opt, "\\timing");
^
In file included from settings.h:12:0,
from command.c:50:
variables.h:38:7: note: declared here
bool ParseVariableBool(const char *value, const char *name, bool *valid);
^
command.c: In function ‘do_pset’:
*command.c:2663:4*: error: too few arguments to function ‘ParseVariableBool’
popt->topt.expanded = ParseVariableBool(value, param);
^
In file included from settings.h:12:0,
from command.c:50:
variables.h:38:7: note: declared here
bool ParseVariableBool(const char *value, const char *name, bool *valid);
^
*command.c:2672:4*: error: too few arguments to function ‘ParseVariableBool’
popt->topt.numericLocale = ParseVariableBool(value, param);
^
In file included from settings.h:12:0,
from command.c:50:
variables.h:38:7: note: declared here
bool ParseVariableBool(const char *value, const char *name, bool *valid);
^
*command.c:2727:4*: error: too few arguments to function ‘ParseVariableBool’
popt->topt.tuples_only = ParseVariableBool(value, param);
^
In file included from settings.h:12:0,
from command.c:50:
variables.h:38:7: note: declared here
bool ParseVariableBool(const char *value, const char *name, bool *valid);
^
*command.c:2759:4*: error: too few arguments to function ‘ParseVariableBool’
if (ParseVariableBool(value, param))
^
In file included from settings.h:12:0,
from command.c:50:
variables.h:38:7: note: declared here
bool ParseVariableBool(const char *value, const char *name, bool *valid);
^
*command.c:2781:4:* error: too few arguments to function ‘ParseVariableBool’
popt->topt.default_footer = ParseVariableBool(value, param);
^
In file included from settings.h:12:0,
from command.c:50:
variables.h:38:7: note: declared here
bool ParseVariableBool(const char *value, const char *name, bool *valid);
^
Thank you,
Rahila Syed
On Mon, Sep 19, 2016 at 11:21 AM, Ashutosh Bapat <
ashutosh.bapat@enterprisedb.com> wrote:
Show quoted text
You may want to add this to the November commitfest
https://commitfest.postgresql.org/11/.On Fri, Sep 16, 2016 at 6:05 PM, Daniel Verite <daniel@manitou-mail.org>
wrote:Hi,
Following the discussion on forbidding an AUTOCOMMIT off->on
switch mid-transaction [1], attached is a patch that let the hooks
return a boolean indicating whether a change is allowed.Using the hooks, bogus assignments to built-in variables can
be dealt with more strictly.For example, pre-patch behavior:
=# \set ECHO errors
=# \set ECHO on
unrecognized value "on" for "ECHO"; assuming "none"
=# \echo :ECHO
onwhich has two problems:
- we have to assume a value, even though we can't know what the usermeant.
- after assignment, the user-visible value of the variable diverges from
its
internal counterpart (pset.echo in this case).
Post-patch:
=# \set ECHO errors
=# \set ECHO on
unrecognized value "on" for "ECHO"
\set: error while setting variable
=# \echo :ECHO
errorsBoth the internal pset.* state and the user-visible value are kept
unchanged
is the input value is incorrect.
Concerning AUTOCOMMIT, autocommit_hook() could return false to forbid
a switch when the conditions are not met.Another user-visible effect of the patch is that, using a bogus value
for a built-in variable on the command-line becomes a fatal error
that prevents psql to continue.
This is not directly intended by the patch but is a consequence
of SetVariable() failing.Example:
$ ./psql -vECHO=bogus
unrecognized value "bogus" for "ECHO"
psql: could not set variable "ECHO"
$ echo $?
1The built-in vars concerned by the change are:
booleans: AUTOCOMMIT, ON_ERROR_STOP, QUIET, SINGLELINE, SINGLESTEP
non-booleans: ECHO, ECHO_HIDDEN, ON_ERROR_ROLLBACK, COMP_KEYWORD_CASE,
HISTCONTROL, VERBOSITY, SHOW_CONTEXTWe could go further to close the gap between pset.* and the built-in
variables,
by changing how they're initialized and forbidding deletion as Tom
suggests in [2], but if there's negative feedback on the above changes,
I think we should hear it first.acc0-df77aeb7d4c7%40mm
[2]
/messages/by-id/4695.1473961140@sss.pgh.pa.usBest regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers--
Best Wishes,
Ashutosh Bapat
EnterpriseDB Corporation
The Postgres Database Company--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Rahila Syed wrote:
I am beginning to review this patch. Initial comment. I got following
compilation error when I applied the patch on latest sources and run make.
Sorry about that, I forgot to make clean, here's an updated patch.
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
Attachments:
psql-var-hooks-v2.patchtext/plainDownload
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index a9a2fdb..61b2304 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -254,7 +254,7 @@ exec_command(const char *cmd,
if (opt1 != NULL && strncmp(opt1, prefix, sizeof(prefix) - 1) == 0)
{
reuse_previous =
- ParseVariableBool(opt1 + sizeof(prefix) - 1, prefix) ?
+ ParseVariableBool(opt1 + sizeof(prefix) - 1, prefix, NULL) ?
TRI_YES : TRI_NO;
free(opt1);
@@ -1548,7 +1548,7 @@ exec_command(const char *cmd,
OT_NORMAL, NULL, false);
if (opt)
- pset.timing = ParseVariableBool(opt, "\\timing");
+ pset.timing = ParseVariableBool(opt, "\\timing", NULL);
else
pset.timing = !pset.timing;
if (!pset.quiet)
@@ -2660,7 +2660,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
if (value && pg_strcasecmp(value, "auto") == 0)
popt->topt.expanded = 2;
else if (value)
- popt->topt.expanded = ParseVariableBool(value, param);
+ popt->topt.expanded = ParseVariableBool(value, param, NULL);
else
popt->topt.expanded = !popt->topt.expanded;
}
@@ -2669,7 +2669,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "numericlocale") == 0)
{
if (value)
- popt->topt.numericLocale = ParseVariableBool(value, param);
+ popt->topt.numericLocale = ParseVariableBool(value, param, NULL);
else
popt->topt.numericLocale = !popt->topt.numericLocale;
}
@@ -2724,7 +2724,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "t") == 0 || strcmp(param, "tuples_only") == 0)
{
if (value)
- popt->topt.tuples_only = ParseVariableBool(value, param);
+ popt->topt.tuples_only = ParseVariableBool(value, param, NULL);
else
popt->topt.tuples_only = !popt->topt.tuples_only;
}
@@ -2756,7 +2756,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
popt->topt.pager = 2;
else if (value)
{
- if (ParseVariableBool(value, param))
+ if (ParseVariableBool(value, param, NULL))
popt->topt.pager = 1;
else
popt->topt.pager = 0;
@@ -2778,7 +2778,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "footer") == 0)
{
if (value)
- popt->topt.default_footer = ParseVariableBool(value, param);
+ popt->topt.default_footer = ParseVariableBool(value, param, NULL);
else
popt->topt.default_footer = !popt->topt.default_footer;
}
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 7ce05fb..9dcfc0a 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -786,43 +786,59 @@ showVersion(void)
* This isn't an amazingly good place for them, but neither is anywhere else.
*/
-static void
+/*
+ * Hook to set an internal flag from a user-supplied string value.
+ * If the syntax is correct, affect *flag and return true.
+ * Otherwise, keep *flag untouched and return false.
+ */
+static bool
+generic_boolean_hook(const char *newval, const char* varname, bool *flag)
+{
+ bool isvalid;
+ bool val = ParseVariableBool(newval, varname, &isvalid);
+ if (isvalid)
+ *flag = val;
+ return isvalid;
+}
+
+static bool
autocommit_hook(const char *newval)
{
- pset.autocommit = ParseVariableBool(newval, "AUTOCOMMIT");
+ return generic_boolean_hook(newval, "AUTOCOMMIT", &pset.autocommit);
}
-static void
+static bool
on_error_stop_hook(const char *newval)
{
- pset.on_error_stop = ParseVariableBool(newval, "ON_ERROR_STOP");
+ return generic_boolean_hook(newval, "ON_ERROR_STOP", &pset.on_error_stop);
}
-static void
+static bool
quiet_hook(const char *newval)
{
- pset.quiet = ParseVariableBool(newval, "QUIET");
+ return generic_boolean_hook(newval, "QUIET", &pset.quiet);
}
-static void
+static bool
singleline_hook(const char *newval)
{
- pset.singleline = ParseVariableBool(newval, "SINGLELINE");
+ return generic_boolean_hook(newval, "SINGLELINE", &pset.singleline);
}
-static void
+static bool
singlestep_hook(const char *newval)
{
- pset.singlestep = ParseVariableBool(newval, "SINGLESTEP");
+ return generic_boolean_hook(newval, "SINGLESTEP", &pset.singlestep);
}
-static void
+static bool
fetch_count_hook(const char *newval)
{
pset.fetch_count = ParseVariableNum(newval, -1, -1, false);
+ return true;
}
-static void
+static bool
echo_hook(const char *newval)
{
if (newval == NULL)
@@ -837,39 +853,52 @@ echo_hook(const char *newval)
pset.echo = PSQL_ECHO_NONE;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "ECHO", "none");
- pset.echo = PSQL_ECHO_NONE;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "ECHO");
+ return false;
}
+ return true;
}
-static void
+static bool
echo_hidden_hook(const char *newval)
{
if (newval == NULL)
pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
else if (pg_strcasecmp(newval, "noexec") == 0)
pset.echo_hidden = PSQL_ECHO_HIDDEN_NOEXEC;
- else if (ParseVariableBool(newval, "ECHO_HIDDEN"))
- pset.echo_hidden = PSQL_ECHO_HIDDEN_ON;
- else /* ParseVariableBool printed msg if needed */
- pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
+ else
+ {
+ bool isvalid;
+ bool val = ParseVariableBool(newval, "ECHO_HIDDEN", &isvalid);
+ if (!isvalid)
+ return false; /* ParseVariableBool printed msg */
+ pset.echo_hidden = val ? PSQL_ECHO_HIDDEN_ON : PSQL_ECHO_HIDDEN_OFF;
+ }
+ return true;
}
-static void
+static bool
on_error_rollback_hook(const char *newval)
{
if (newval == NULL)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
else if (pg_strcasecmp(newval, "interactive") == 0)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_INTERACTIVE;
- else if (ParseVariableBool(newval, "ON_ERROR_ROLLBACK"))
- pset.on_error_rollback = PSQL_ERROR_ROLLBACK_ON;
- else /* ParseVariableBool printed msg if needed */
- pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
+ else
+ {
+ bool isvalid;
+ bool val = ParseVariableBool(newval, "ON_ERROR_ROLLBACK", &isvalid);
+ if (isvalid)
+ pset.on_error_rollback = val ? PSQL_ERROR_ROLLBACK_ON : PSQL_ERROR_ROLLBACK_OFF;
+ else
+ /* ParseVariableBool printed msg if needed */
+ return false;
+ }
+ return true;
}
-static void
+static bool
comp_keyword_case_hook(const char *newval)
{
if (newval == NULL)
@@ -884,13 +913,14 @@ comp_keyword_case_hook(const char *newval)
pset.comp_case = PSQL_COMP_CASE_LOWER;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "COMP_KEYWORD_CASE", "preserve-upper");
- pset.comp_case = PSQL_COMP_CASE_PRESERVE_UPPER;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "COMP_KEYWORD_CASE");
+ return false;
}
+ return true;
}
-static void
+static bool
histcontrol_hook(const char *newval)
{
if (newval == NULL)
@@ -905,31 +935,35 @@ histcontrol_hook(const char *newval)
pset.histcontrol = hctl_none;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "HISTCONTROL", "none");
- pset.histcontrol = hctl_none;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "HISTCONTROL");
+ return false;
}
+ return true;
}
-static void
+static bool
prompt1_hook(const char *newval)
{
pset.prompt1 = newval ? newval : "";
+ return true;
}
-static void
+static bool
prompt2_hook(const char *newval)
{
pset.prompt2 = newval ? newval : "";
+ return true;
}
-static void
+static bool
prompt3_hook(const char *newval)
{
pset.prompt3 = newval ? newval : "";
+ return true;
}
-static void
+static bool
verbosity_hook(const char *newval)
{
if (newval == NULL)
@@ -942,16 +976,17 @@ verbosity_hook(const char *newval)
pset.verbosity = PQERRORS_VERBOSE;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "VERBOSITY", "default");
- pset.verbosity = PQERRORS_DEFAULT;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "VERBOSITY");
+ return false;
}
if (pset.db)
PQsetErrorVerbosity(pset.db, pset.verbosity);
+ return true;
}
-static void
+static bool
show_context_hook(const char *newval)
{
if (newval == NULL)
@@ -964,13 +999,14 @@ show_context_hook(const char *newval)
pset.show_context = PQSHOW_CONTEXT_ALWAYS;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "SHOW_CONTEXT", "errors");
- pset.show_context = PQSHOW_CONTEXT_ERRORS;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "SHOW_CONTEXT");
+ return false;
}
if (pset.db)
PQsetErrorContextVisibility(pset.db, pset.show_context);
+ return true;
}
diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c
index f43f418..24ca750 100644
--- a/src/bin/psql/variables.c
+++ b/src/bin/psql/variables.c
@@ -86,12 +86,16 @@ GetVariable(VariableSpace space, const char *name)
*
* "name" is the name of the variable we're assigning to, to use in error
* report if any. Pass name == NULL to suppress the error report.
+ *
+ * "*valid" reports whether "value" was syntactically valid, unless valid == NULL
*/
bool
-ParseVariableBool(const char *value, const char *name)
+ParseVariableBool(const char *value, const char *name, bool *valid)
{
size_t len;
+ if (valid)
+ *valid = true;
if (value == NULL)
return false; /* not set -> assume "off" */
@@ -116,10 +120,16 @@ ParseVariableBool(const char *value, const char *name)
return false;
else
{
- /* NULL is treated as false, so a non-matching value is 'true' */
+ /*
+ * NULL is treated as false, so a non-matching value is 'true'.
+ * A caller that cares about syntactic conformance should
+ * check *valid to know whether the value was recognized.
+ */
if (name)
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- value, name, "on");
+ psql_error("unrecognized value \"%s\" for \"%s\": boolean expected\n",
+ value, name);
+ if (valid)
+ *valid = false;
return true;
}
}
@@ -205,13 +215,19 @@ SetVariable(VariableSpace space, const char *name, const char *value)
{
if (strcmp(current->name, name) == 0)
{
- /* found entry, so update */
- if (current->value)
- free(current->value);
- current->value = pg_strdup(value);
+ /* found entry, so update, unless a hook returns false */
+ bool confirmed = true;
if (current->assign_hook)
- (*current->assign_hook) (current->value);
- return true;
+ {
+ confirmed = (*current->assign_hook) (value);
+ }
+ if (confirmed)
+ {
+ if (current->value)
+ free(current->value);
+ current->value = pg_strdup(value);
+ }
+ return confirmed;
}
}
@@ -248,7 +264,7 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
{
/* found entry, so update */
current->assign_hook = hook;
- (*hook) (current->value);
+ (void)(*hook) (current->value); /* ignore return value */
return true;
}
}
@@ -260,7 +276,7 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
current->assign_hook = hook;
current->next = NULL;
previous->next = current;
- (*hook) (NULL);
+ (void)(*hook) (NULL); /* ignore return value */
return true;
}
diff --git a/src/bin/psql/variables.h b/src/bin/psql/variables.h
index d7a05a1..9836fc5 100644
--- a/src/bin/psql/variables.h
+++ b/src/bin/psql/variables.h
@@ -20,7 +20,7 @@
* Note: if value == NULL then the variable is logically unset, but we are
* keeping the struct around so as not to forget about its hook function.
*/
-typedef void (*VariableAssignHook) (const char *newval);
+typedef bool (*VariableAssignHook) (const char *newval);
struct _variable
{
@@ -35,7 +35,7 @@ typedef struct _variable *VariableSpace;
VariableSpace CreateVariableSpace(void);
const char *GetVariable(VariableSpace space, const char *name);
-bool ParseVariableBool(const char *value, const char *name);
+bool ParseVariableBool(const char *value, const char *name, bool *valid);
int ParseVariableNum(const char *val,
int defaultval,
int faultval,
Sorry about that, I forgot to make clean, here's an updated patch.
Ongoing CMake changes will help to avoid such things, "out of source build".
On Mon, Sep 19, 2016 at 6:20 PM, Daniel Verite <daniel@manitou-mail.org>
wrote:
Show quoted text
Rahila Syed wrote:
I am beginning to review this patch. Initial comment. I got following
compilation error when I applied the patch on latest sources and runmake.
Sorry about that, I forgot to make clean, here's an updated patch.
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Ashutosh Bapat wrote:
You may want to add this to the November commitfest
https://commitfest.postgresql.org/11/.
Done. It's at https://commitfest.postgresql.org/11/799/
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello,
I have applied this patch on latest HEAD and have done basic testing which
works fine.
Some comments,
if (current->assign_hook) - (*current->assign_hook) (current->value); - return true; + { + confirmed = (*current->assign_hook) (value); + } + if (confirmed)
Spurious brackets
static bool +generic_boolean_hook(const char *newval, const char* varname, bool *flag) +{
Contrary to what name suggests this function does not seem to have other
implementations as in a hook.
Also this takes care of rejecting a syntactically wrong value only for
boolean variable hooks like autocommit_hook,
on_error_stop_hook. However, there are other variable hooks which call
ParseVariableBool.
For instance, echo_hidden_hook which is handled separately in the patch.
Thus there is some duplication of code between generic_boolean_hook and
echo_hidden_hook.
Similarly for on_error_rollback_hook.
-static void
+static bool
fetch_count_hook(const char *newval)
{
pset.fetch_count = ParseVariableNum(newval, -1, -1, false);
+ return true;
}
Shouldn't invalid numeric string assignment for numeric variables be
handled too?
Instead of generic_boolean_hook cant we have something like follows which
like generic_boolean_hook can be called from specific variable assignment
hooks,
static bool
ParseVariable(newval, VariableName, &pset.var)
{
if (VariableName == ‘AUTOCOMMIT’ || ECHO_HIDDEN || other variable with
hooks which call ParseVariableBool )
<logic here same as generic_boolean_hook in patch
<additional lines as there in the patch for ECHO_HIDDEN,
ON_ERROR_ROLLBACK>
else if (VariableName == ‘FETCH_COUNT’)
ParseVariableNum();
}
This will help merge the logic which is to check for valid syntax before
assigning and returning false on error, in one place.
@@ -260,7 +276,7 @@ SetVariableAssignHook(VariableSpace space, const char
*name, VariableAssignHook
current->assign_hook = hook; current->next = NULL; previous->next = current; - (*hook) (NULL); + (void)(*hook) (NULL); /* ignore return value */
Sorry for my lack of understanding, can you explain why is above change
needed?
Thank you,
Rahila Syed
On Tue, Sep 20, 2016 at 11:16 PM, Daniel Verite <daniel@manitou-mail.org>
wrote:
Show quoted text
Ashutosh Bapat wrote:
You may want to add this to the November commitfest
https://commitfest.postgresql.org/11/.Done. It's at https://commitfest.postgresql.org/11/799/
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Rahila Syed wrote:
I have applied this patch on latest HEAD and have done basic testing which
works fine.
Thanks for reviewing this patch!
if (current->assign_hook) - (*current->assign_hook) (current->value); - return true; + { + confirmed = (*current->assign_hook) (value); + } + if (confirmed)Spurious brackets
OK.
static bool +generic_boolean_hook(const char *newval, const char* varname, bool *flag) +{Contrary to what name suggests this function does not seem to have other
implementations as in a hook.
Also this takes care of rejecting a syntactically wrong value only for
boolean variable hooks like autocommit_hook,
on_error_stop_hook. However, there are other variable hooks which call
ParseVariableBool.
For instance, echo_hidden_hook which is handled separately in the patch.
Thus there is some duplication of code between generic_boolean_hook and
echo_hidden_hook.
Similarly for on_error_rollback_hook.
The purpose of generic_boolean_hook() is to handle the case of a
boolean variable that only accepts ON or OFF, and has its pset.varname
declared as bool. I thought of this case as "generic" because that's
the base case and several variables need no more than that.
ECHO_HIDDEN differs from the generic boolean case because it also
accepts "noexec" and pset.echo_hidden is an enum, not a boolean. When
considering refactoring echo_hidden_hook() to call
generic_boolean_hook() instead of ParseVariableBool() after
having established that the value is not "noexec", I don't see
any advantage in clarity or code size, so I'm not in favor of that change.
The same applies to on_error_rollback_hook(), which has to deal
with a specific enum as well.
-static void
+static bool
fetch_count_hook(const char *newval)
{
pset.fetch_count = ParseVariableNum(newval, -1, -1, false);
+ return true;
}Shouldn't invalid numeric string assignment for numeric variables be
handled too?
Agreed. Assignments like "\set FETCH_COUNT bogus" don't provoke any
user feedback currently, which is not ideal. I'll add this in a
v3 of the patch tomorrow.
Instead of generic_boolean_hook cant we have something like follows which
like generic_boolean_hook can be called from specific variable assignment
hooks,static bool
ParseVariable(newval, VariableName, &pset.var)
{
if (VariableName == ‘AUTOCOMMIT’ || ECHO_HIDDEN || other variable with
hooks which call ParseVariableBool )
<logic here same as generic_boolean_hook in patch
<additional lines as there in the patch for ECHO_HIDDEN,
ON_ERROR_ROLLBACK>
else if (VariableName == ‘FETCH_COUNT’)
ParseVariableNum();
}
It's not possible because pset.var corresponds to different fields from
struct _psqlSettings that have different types: bool, int and some
enum types.
Besides, I don't think it would go well with hooks. If we wanted one
big function that knows all about parsing all built-in variables, we
could just as well dispense with hooks, since their current purpose in
psql is to achieve this parsing, but in a decentralized way.
Or if we keep them, our various built-in variables would be
essentially tied to the same one-big-hook-that-does-all, but isn't
that an antipattern for hooks?
@@ -260,7 +276,7 @@ SetVariableAssignHook(VariableSpace space, const char
*name, VariableAssignHook
current->assign_hook = hook; current->next = NULL; previous->next = current; - (*hook) (NULL); + (void)(*hook) (NULL); /* ignore return value */Sorry for my lack of understanding, can you explain why is above change
needed?
"hook" is changed by the patch from [pointer to function returning
void], to [pointer to function returning bool]. The cast to void is
not essential, it just indicates that we deliberately want to
ignore the return value here. I expect some compilers might
complain under a high level of warnings without this cast, although
TBH if you ask me, I wouldn't know which compiler with which flags
exactly.
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
ECHO_HIDDEN differs from the generic boolean case because it also
accepts "noexec" and pset.echo_hidden is an enum, not a boolean. When
considering refactoring echo_hidden_hook() to call
generic_boolean_hook() instead of ParseVariableBool() after
having established that the value is not "noexec", I don't see
any advantage in clarity or code size, so I'm not in favor of that change
I agree if generic_boolean_hook() is used in its current form instead of
ParseVariableBool() inside
echo_hidden_hook or on_error_rollback_hook the code will not change much.
I was suggesting change on the lines of extending generic_boolean_hook to
include
enum(part in enum_hooks which calls ParseVariableBool) and integer parsing
as well.
Besides, I don't think it would go well with hooks. If we wanted one
big function that knows all about parsing all built-in variables, we
could just as well dispense with hooks, since their current purpose in
psql is to achieve this parsing, but in a decentralized way.
Or if we keep them, our various built-in variables would be
essentially tied to the same one-big-hook-that-does-all, but isn't
that an antipattern for hooks?
I was suggesting something on the lines of having common parsing logic for
each implementation of hook. This is similar to what ParseVariableBool does
in
the existing code. ParseVariableBool is being called from different hooks
to
parse the boolean value of the variable. Thus representing common code in
various
implementations of hook.
"hook" is changed by the patch from [pointer to function returning
void], to [pointer to function returning bool]. The cast to void is
not essential, it just indicates that we deliberately want to
ignore the return value here. I expect some compilers might
complain under a high level of warnings without this cast, although
TBH if you ask me, I wouldn't know which compiler with which flags
exactly.
Thank you for explanation.
Thank you,
Rahila Syed
On Fri, Nov 11, 2016 at 2:57 AM, Daniel Verite <daniel@manitou-mail.org>
wrote:
Show quoted text
Rahila Syed wrote:
I have applied this patch on latest HEAD and have done basic testing
which
works fine.
Thanks for reviewing this patch!
if (current->assign_hook) - (*current->assign_hook) (current->value); - return true; + { + confirmed = (*current->assign_hook) (value); + } + if (confirmed)Spurious brackets
OK.
static bool
+generic_boolean_hook(const char *newval, const char* varname, bool*flag)
+{
Contrary to what name suggests this function does not seem to have other
implementations as in a hook.
Also this takes care of rejecting a syntactically wrong value only for
boolean variable hooks like autocommit_hook,
on_error_stop_hook. However, there are other variable hooks which call
ParseVariableBool.
For instance, echo_hidden_hook which is handled separately in the patch.
Thus there is some duplication of code between generic_boolean_hook and
echo_hidden_hook.
Similarly for on_error_rollback_hook.The purpose of generic_boolean_hook() is to handle the case of a
boolean variable that only accepts ON or OFF, and has its pset.varname
declared as bool. I thought of this case as "generic" because that's
the base case and several variables need no more than that.ECHO_HIDDEN differs from the generic boolean case because it also
accepts "noexec" and pset.echo_hidden is an enum, not a boolean. When
considering refactoring echo_hidden_hook() to call
generic_boolean_hook() instead of ParseVariableBool() after
having established that the value is not "noexec", I don't see
any advantage in clarity or code size, so I'm not in favor of that change.The same applies to on_error_rollback_hook(), which has to deal
with a specific enum as well.-static void
+static bool
fetch_count_hook(const char *newval)
{
pset.fetch_count = ParseVariableNum(newval, -1, -1, false);
+ return true;
}Shouldn't invalid numeric string assignment for numeric variables be
handled too?Agreed. Assignments like "\set FETCH_COUNT bogus" don't provoke any
user feedback currently, which is not ideal. I'll add this in a
v3 of the patch tomorrow.Instead of generic_boolean_hook cant we have something like follows which
like generic_boolean_hook can be called from specific variable assignment
hooks,static bool
ParseVariable(newval, VariableName, &pset.var)
{
if (VariableName == ‘AUTOCOMMIT’ || ECHO_HIDDEN || other variablewith
hooks which call ParseVariableBool )
<logic here same as generic_boolean_hook in patch
<additional lines as there in the patch for ECHO_HIDDEN,
ON_ERROR_ROLLBACK>
else if (VariableName == ‘FETCH_COUNT’)
ParseVariableNum();
}It's not possible because pset.var corresponds to different fields from
struct _psqlSettings that have different types: bool, int and some
enum types.
Besides, I don't think it would go well with hooks. If we wanted one
big function that knows all about parsing all built-in variables, we
could just as well dispense with hooks, since their current purpose in
psql is to achieve this parsing, but in a decentralized way.
Or if we keep them, our various built-in variables would be
essentially tied to the same one-big-hook-that-does-all, but isn't
that an antipattern for hooks?@@ -260,7 +276,7 @@ SetVariableAssignHook(VariableSpace space, const
char
*name, VariableAssignHook
current->assign_hook = hook; current->next = NULL; previous->next = current; - (*hook) (NULL); + (void)(*hook) (NULL); /* ignore return value */Sorry for my lack of understanding, can you explain why is above change
needed?"hook" is changed by the patch from [pointer to function returning
void], to [pointer to function returning bool]. The cast to void is
not essential, it just indicates that we deliberately want to
ignore the return value here. I expect some compilers might
complain under a high level of warnings without this cast, although
TBH if you ask me, I wouldn't know which compiler with which flags
exactly.Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
Hi,
I'm attaching v3 of the patch with error reporting for
FETCH_COUNT as mentioned upthread, and rebased
on the most recent master.
I was suggesting change on the lines of extending generic_boolean_hook to
include enum(part in enum_hooks which calls ParseVariableBool) and
integer parsing as well.
Well, generic_boolean_hook() is meant to change this, for instance:
static void
on_error_stop_hook(const char *newval)
{
pset.on_error_stop = ParseVariableBool(newval, "ON_ERROR_STOP");
}
into that:
static bool
on_error_stop_hook(const char *newval)
{
return generic_boolean_hook(newval, "ON_ERROR_STOP",
&pset.on_error_stop);
}
with the goal that the assignment does not occur if "newval" is bogus.
The change is really minimal.
When we're dealing with enum-or-bool variables, such as for instance
ECHO_HIDDEN, the patch replaces this:
static void
echo_hidden_hook(const char *newval)
{
if (newval == NULL)
pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
else if (pg_strcasecmp(newval, "noexec") == 0)
pset.echo_hidden = PSQL_ECHO_HIDDEN_NOEXEC;
else if (ParseVariableBool(newval, "ECHO_HIDDEN"))
pset.echo_hidden = PSQL_ECHO_HIDDEN_ON;
else /* ParseVariableBool printed msg if needed */
pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
}
with that:
static bool
echo_hidden_hook(const char *newval)
{
if (newval == NULL)
pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
else if (pg_strcasecmp(newval, "noexec") == 0)
pset.echo_hidden = PSQL_ECHO_HIDDEN_NOEXEC;
else
{
bool isvalid;
bool val = ParseVariableBool(newval, "ECHO_HIDDEN", &isvalid);
if (!isvalid)
return false; /* ParseVariableBool printed msg */
pset.echo_hidden = val ? PSQL_ECHO_HIDDEN_ON : PSQL_ECHO_HIDDEN_OFF;
}
return true;
}
The goal being again to reject a bogus assignment, as opposed to replacing
it with any hardwired value.
Code-wise, we can't call generic_boolean_hook() here because we need
to assign a non-boolean specific value after having parsed the ON/OFF
user-supplied string.
More generally, it turns out that the majority of hooks are concerned
by this patch, as they parse user-supplied values, but there
are 4 distinct categories of variables:
1- purely ON/OFF vars:
AUTOCOMMIT, ON_ERROR_STOP, QUIET, SINGLELINE, SINGLESTEP
2- ON/OFF mixed with enum values:
ECHO_HIDDEN, ON_ERROR_ROLLBACK
3- purely enum values:
COMP_KEYWORD_CASE, HISTCONTROL, ECHO, VERBOSITY, SHOW_CONTEXT
4- numeric values:
FETCH_COUNT
If you suggest that the patch should refactor the implementation
of hooks for case #2, only two hooks are concerned and they consist
of non-mergeable enum-specific code interleaved with generic code,
so I don't foresee any gain in fusioning. I have the same opinion about
merging any of #1, #2, #3, #4 together.
But feel free to post code implementing your idea if you disagree,
maybe I just don't figure out what you have in mind.
For case #3, these hooks clearly follow a common pattern, but I also
don't see any benefit in an opportunistic rewrite given the nature of
the functions.
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
Attachments:
psql-var-hooks-v3.patchtext/plainDownload
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index a9a2fdb..61b2304 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -254,7 +254,7 @@ exec_command(const char *cmd,
if (opt1 != NULL && strncmp(opt1, prefix, sizeof(prefix) - 1) == 0)
{
reuse_previous =
- ParseVariableBool(opt1 + sizeof(prefix) - 1, prefix) ?
+ ParseVariableBool(opt1 + sizeof(prefix) - 1, prefix, NULL) ?
TRI_YES : TRI_NO;
free(opt1);
@@ -1548,7 +1548,7 @@ exec_command(const char *cmd,
OT_NORMAL, NULL, false);
if (opt)
- pset.timing = ParseVariableBool(opt, "\\timing");
+ pset.timing = ParseVariableBool(opt, "\\timing", NULL);
else
pset.timing = !pset.timing;
if (!pset.quiet)
@@ -2660,7 +2660,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
if (value && pg_strcasecmp(value, "auto") == 0)
popt->topt.expanded = 2;
else if (value)
- popt->topt.expanded = ParseVariableBool(value, param);
+ popt->topt.expanded = ParseVariableBool(value, param, NULL);
else
popt->topt.expanded = !popt->topt.expanded;
}
@@ -2669,7 +2669,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "numericlocale") == 0)
{
if (value)
- popt->topt.numericLocale = ParseVariableBool(value, param);
+ popt->topt.numericLocale = ParseVariableBool(value, param, NULL);
else
popt->topt.numericLocale = !popt->topt.numericLocale;
}
@@ -2724,7 +2724,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "t") == 0 || strcmp(param, "tuples_only") == 0)
{
if (value)
- popt->topt.tuples_only = ParseVariableBool(value, param);
+ popt->topt.tuples_only = ParseVariableBool(value, param, NULL);
else
popt->topt.tuples_only = !popt->topt.tuples_only;
}
@@ -2756,7 +2756,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
popt->topt.pager = 2;
else if (value)
{
- if (ParseVariableBool(value, param))
+ if (ParseVariableBool(value, param, NULL))
popt->topt.pager = 1;
else
popt->topt.pager = 0;
@@ -2778,7 +2778,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "footer") == 0)
{
if (value)
- popt->topt.default_footer = ParseVariableBool(value, param);
+ popt->topt.default_footer = ParseVariableBool(value, param, NULL);
else
popt->topt.default_footer = !popt->topt.default_footer;
}
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 7ce05fb..ba094f0 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -786,43 +786,68 @@ showVersion(void)
* This isn't an amazingly good place for them, but neither is anywhere else.
*/
-static void
+/*
+ * Hook to set an internal flag from a user-supplied string value.
+ * If the syntax is correct, affect *flag and return true.
+ * Otherwise, keep *flag untouched and return false.
+ */
+static bool
+generic_boolean_hook(const char *newval, const char* varname, bool *flag)
+{
+ bool isvalid;
+ bool val = ParseVariableBool(newval, varname, &isvalid);
+ if (isvalid)
+ *flag = val;
+ return isvalid;
+}
+
+static bool
autocommit_hook(const char *newval)
{
- pset.autocommit = ParseVariableBool(newval, "AUTOCOMMIT");
+ return generic_boolean_hook(newval, "AUTOCOMMIT", &pset.autocommit);
}
-static void
+static bool
on_error_stop_hook(const char *newval)
{
- pset.on_error_stop = ParseVariableBool(newval, "ON_ERROR_STOP");
+ return generic_boolean_hook(newval, "ON_ERROR_STOP", &pset.on_error_stop);
}
-static void
+static bool
quiet_hook(const char *newval)
{
- pset.quiet = ParseVariableBool(newval, "QUIET");
+ return generic_boolean_hook(newval, "QUIET", &pset.quiet);
}
-static void
+static bool
singleline_hook(const char *newval)
{
- pset.singleline = ParseVariableBool(newval, "SINGLELINE");
+ return generic_boolean_hook(newval, "SINGLELINE", &pset.singleline);
}
-static void
+static bool
singlestep_hook(const char *newval)
{
- pset.singlestep = ParseVariableBool(newval, "SINGLESTEP");
+ return generic_boolean_hook(newval, "SINGLESTEP", &pset.singlestep);
}
-static void
+static bool
fetch_count_hook(const char *newval)
{
- pset.fetch_count = ParseVariableNum(newval, -1, -1, false);
+ bool valid;
+ int value = ParseCheckVariableNum(newval, -1, &valid);
+ if (valid)
+ pset.fetch_count = value;
+ else
+ {
+ psql_error("invalid value \"%s\" for \"%s\"\n",
+ newval, "FETCH_COUNT");
+ return false;
+ }
+ return true;
}
-static void
+static bool
echo_hook(const char *newval)
{
if (newval == NULL)
@@ -837,39 +862,52 @@ echo_hook(const char *newval)
pset.echo = PSQL_ECHO_NONE;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "ECHO", "none");
- pset.echo = PSQL_ECHO_NONE;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "ECHO");
+ return false;
}
+ return true;
}
-static void
+static bool
echo_hidden_hook(const char *newval)
{
if (newval == NULL)
pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
else if (pg_strcasecmp(newval, "noexec") == 0)
pset.echo_hidden = PSQL_ECHO_HIDDEN_NOEXEC;
- else if (ParseVariableBool(newval, "ECHO_HIDDEN"))
- pset.echo_hidden = PSQL_ECHO_HIDDEN_ON;
- else /* ParseVariableBool printed msg if needed */
- pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
+ else
+ {
+ bool isvalid;
+ bool val = ParseVariableBool(newval, "ECHO_HIDDEN", &isvalid);
+ if (!isvalid)
+ return false; /* ParseVariableBool printed msg */
+ pset.echo_hidden = val ? PSQL_ECHO_HIDDEN_ON : PSQL_ECHO_HIDDEN_OFF;
+ }
+ return true;
}
-static void
+static bool
on_error_rollback_hook(const char *newval)
{
if (newval == NULL)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
else if (pg_strcasecmp(newval, "interactive") == 0)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_INTERACTIVE;
- else if (ParseVariableBool(newval, "ON_ERROR_ROLLBACK"))
- pset.on_error_rollback = PSQL_ERROR_ROLLBACK_ON;
- else /* ParseVariableBool printed msg if needed */
- pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
+ else
+ {
+ bool isvalid;
+ bool val = ParseVariableBool(newval, "ON_ERROR_ROLLBACK", &isvalid);
+ if (isvalid)
+ pset.on_error_rollback = val ? PSQL_ERROR_ROLLBACK_ON : PSQL_ERROR_ROLLBACK_OFF;
+ else
+ /* ParseVariableBool printed msg if needed */
+ return false;
+ }
+ return true;
}
-static void
+static bool
comp_keyword_case_hook(const char *newval)
{
if (newval == NULL)
@@ -884,13 +922,14 @@ comp_keyword_case_hook(const char *newval)
pset.comp_case = PSQL_COMP_CASE_LOWER;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "COMP_KEYWORD_CASE", "preserve-upper");
- pset.comp_case = PSQL_COMP_CASE_PRESERVE_UPPER;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "COMP_KEYWORD_CASE");
+ return false;
}
+ return true;
}
-static void
+static bool
histcontrol_hook(const char *newval)
{
if (newval == NULL)
@@ -905,31 +944,35 @@ histcontrol_hook(const char *newval)
pset.histcontrol = hctl_none;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "HISTCONTROL", "none");
- pset.histcontrol = hctl_none;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "HISTCONTROL");
+ return false;
}
+ return true;
}
-static void
+static bool
prompt1_hook(const char *newval)
{
pset.prompt1 = newval ? newval : "";
+ return true;
}
-static void
+static bool
prompt2_hook(const char *newval)
{
pset.prompt2 = newval ? newval : "";
+ return true;
}
-static void
+static bool
prompt3_hook(const char *newval)
{
pset.prompt3 = newval ? newval : "";
+ return true;
}
-static void
+static bool
verbosity_hook(const char *newval)
{
if (newval == NULL)
@@ -942,16 +985,17 @@ verbosity_hook(const char *newval)
pset.verbosity = PQERRORS_VERBOSE;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "VERBOSITY", "default");
- pset.verbosity = PQERRORS_DEFAULT;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "VERBOSITY");
+ return false;
}
if (pset.db)
PQsetErrorVerbosity(pset.db, pset.verbosity);
+ return true;
}
-static void
+static bool
show_context_hook(const char *newval)
{
if (newval == NULL)
@@ -964,13 +1008,14 @@ show_context_hook(const char *newval)
pset.show_context = PQSHOW_CONTEXT_ALWAYS;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "SHOW_CONTEXT", "errors");
- pset.show_context = PQSHOW_CONTEXT_ERRORS;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "SHOW_CONTEXT");
+ return false;
}
if (pset.db)
PQsetErrorContextVisibility(pset.db, pset.show_context);
+ return true;
}
diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c
index f43f418..5e68b67 100644
--- a/src/bin/psql/variables.c
+++ b/src/bin/psql/variables.c
@@ -86,12 +86,16 @@ GetVariable(VariableSpace space, const char *name)
*
* "name" is the name of the variable we're assigning to, to use in error
* report if any. Pass name == NULL to suppress the error report.
+ *
+ * "*valid" reports whether "value" was syntactically valid, unless valid == NULL
*/
bool
-ParseVariableBool(const char *value, const char *name)
+ParseVariableBool(const char *value, const char *name, bool *valid)
{
size_t len;
+ if (valid)
+ *valid = true;
if (value == NULL)
return false; /* not set -> assume "off" */
@@ -116,10 +120,16 @@ ParseVariableBool(const char *value, const char *name)
return false;
else
{
- /* NULL is treated as false, so a non-matching value is 'true' */
+ /*
+ * NULL is treated as false, so a non-matching value is 'true'.
+ * A caller that cares about syntactic conformance should
+ * check *valid to know whether the value was recognized.
+ */
if (name)
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- value, name, "on");
+ psql_error("unrecognized value \"%s\" for \"%s\": boolean expected\n",
+ value, name);
+ if (valid)
+ *valid = false;
return true;
}
}
@@ -154,6 +164,35 @@ ParseVariableNum(const char *val,
return result;
}
+/*
+ * Read numeric variable, or defaultval if it is not set.
+ * Trailing contents are not allowed.
+ * "valid" points to a bool reporting whether the value was valid.
+ */
+int
+ParseCheckVariableNum(const char *val,
+ int defaultval,
+ bool *valid)
+{
+ int result;
+
+ if (!val)
+ {
+ *valid = true;
+ result = defaultval;
+ }
+ else
+ {
+ char *end;
+
+ errno = 0;
+ result = strtol(val, &end, 0);
+ *valid = (val[0] != '\0' && errno == 0 && *end == '\0');
+ }
+
+ return result;
+}
+
int
GetVariableNum(VariableSpace space,
const char *name,
@@ -205,13 +244,17 @@ SetVariable(VariableSpace space, const char *name, const char *value)
{
if (strcmp(current->name, name) == 0)
{
- /* found entry, so update */
- if (current->value)
- free(current->value);
- current->value = pg_strdup(value);
- if (current->assign_hook)
- (*current->assign_hook) (current->value);
- return true;
+ /* found entry, so update, unless a hook returns false */
+ bool confirmed = current->assign_hook ?
+ (*current->assign_hook) (value) : true;
+
+ if (confirmed)
+ {
+ if (current->value)
+ free(current->value);
+ current->value = pg_strdup(value);
+ }
+ return confirmed;
}
}
@@ -248,7 +291,7 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
{
/* found entry, so update */
current->assign_hook = hook;
- (*hook) (current->value);
+ (*hook) (current->value); /* ignore return value */
return true;
}
}
@@ -260,7 +303,7 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
current->assign_hook = hook;
current->next = NULL;
previous->next = current;
- (*hook) (NULL);
+ (void)(*hook) (NULL); /* ignore return value */
return true;
}
diff --git a/src/bin/psql/variables.h b/src/bin/psql/variables.h
index d7a05a1..38396ba 100644
--- a/src/bin/psql/variables.h
+++ b/src/bin/psql/variables.h
@@ -20,7 +20,7 @@
* Note: if value == NULL then the variable is logically unset, but we are
* keeping the struct around so as not to forget about its hook function.
*/
-typedef void (*VariableAssignHook) (const char *newval);
+typedef bool (*VariableAssignHook) (const char *newval);
struct _variable
{
@@ -35,11 +35,14 @@ typedef struct _variable *VariableSpace;
VariableSpace CreateVariableSpace(void);
const char *GetVariable(VariableSpace space, const char *name);
-bool ParseVariableBool(const char *value, const char *name);
+bool ParseVariableBool(const char *value, const char *name, bool *valid);
int ParseVariableNum(const char *val,
int defaultval,
int faultval,
bool allowtrail);
+int ParseCheckVariableNum(const char *val,
+ int defaultval,
+ bool *valid);
int GetVariableNum(VariableSpace space,
const char *name,
int defaultval,
Daniel,
* Daniel Verite (daniel@manitou-mail.org) wrote:
I'm attaching v3 of the patch with error reporting for
FETCH_COUNT as mentioned upthread, and rebased
on the most recent master.
Just fyi, there seems to be some issues with this patch because setting
my PROMPT1 and PROMPT2 variables result in rather odd behavior.
Here's what I use:
-------------
\set PROMPT1 '\033[33;1m%M(from '`hostname`').%/.%n.%> [%`date`]\033[0m\n=%x%# '
\set PROMPT2 '-%x%# '
-------------
In reviewing this patch, I also noticed that it's set up to assume a
'true' result when a variable can't be parsed by ParseVariableBool.
-----------
postgres=# \timing off
Timing is off.
postgres=# \timing asdsa
unrecognized value "asdsa" for "\timing": boolean expected
Timing is on.
-----------
That certainly doesn't feel right. I'm thinking that if we're going to
throw an error back to the user about a value being invalid then we
shouldn't change the current value.
My initial thought was that perhaps we should pass the current value to
ParseVariableBool() and let it return whatever the "right" answer is,
however, your use of ParseVariableBool() for enums that happen to accept
on/off means that won't really work.
Perhaps the right answer is to flip this around a bit and treat booleans
as a special case of enums and have a generic solution for enums.
Consider something like:
ParseVariableEnum(valid_enums, str_value, name, curr_value);
'valid_enums' would be a simple list of what the valid values are for a
given variable and their corresponding value, str_value the string the
user typed, name the name of the variable, and curr_value the current
value of the variable.
ParseVariableEnum() could then detect if the string passed in is valid
or not and report to the user if it's incorrect and leave the existing
value alone. This could also generically handle the question of if the
string passed in is a unique prefix of a correct value by comparing it
to all of the valid values and seeing if there's a unique match or not.
Thoughts?
Thanks!
Stephen
Stephen Frost <sfrost@snowman.net> writes:
In reviewing this patch, I also noticed that it's set up to assume a
'true' result when a variable can't be parsed by ParseVariableBool.
I suppose that's meant to be backwards-compatible with the current
behavior:
regression=# \timing foo
unrecognized value "foo" for "\timing"; assuming "on"
Timing is on.
but I agree that if we're changing things in this area, that would
be high on my list of things to change. I think what we want going
forward is to disallow setting "special" variables to invalid values,
and that should hold both for regular variables that have special
behaviors, and for the special-syntax cases like \timing.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
* Tom Lane (tgl@sss.pgh.pa.us) wrote:
Stephen Frost <sfrost@snowman.net> writes:
In reviewing this patch, I also noticed that it's set up to assume a
'true' result when a variable can't be parsed by ParseVariableBool.I suppose that's meant to be backwards-compatible with the current
behavior:
Ah, good point, however..
but I agree that if we're changing things in this area, that would
be high on my list of things to change. I think what we want going
forward is to disallow setting "special" variables to invalid values,
and that should hold both for regular variables that have special
behaviors, and for the special-syntax cases like \timing.
I completely agree with you here. We shouldn't be assuming "invalid"
means "true".
Thanks!
Stephen
Tom Lane wrote:
Stephen Frost <sfrost@snowman.net> writes:
In reviewing this patch, I also noticed that it's set up to assume a
'true' result when a variable can't be parsed by ParseVariableBool.I suppose that's meant to be backwards-compatible with the current
behavior:regression=# \timing foo
unrecognized value "foo" for "\timing"; assuming "on"
Timing is on.
Exactly. The scope of the patch is limited to the effect
of \set assignments to built-in variables.
but I agree that if we're changing things in this area, that would
be high on my list of things to change. I think what we want going
forward is to disallow setting "special" variables to invalid values,
and that should hold both for regular variables that have special
behaviors, and for the special-syntax cases like \timing.
+1
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Daniel,
* Daniel Verite (daniel@manitou-mail.org) wrote:
Tom Lane wrote:
Stephen Frost <sfrost@snowman.net> writes:
In reviewing this patch, I also noticed that it's set up to assume a
'true' result when a variable can't be parsed by ParseVariableBool.I suppose that's meant to be backwards-compatible with the current
behavior:regression=# \timing foo
unrecognized value "foo" for "\timing"; assuming "on"
Timing is on.Exactly. The scope of the patch is limited to the effect
of \set assignments to built-in variables.but I agree that if we're changing things in this area, that would
be high on my list of things to change. I think what we want going
forward is to disallow setting "special" variables to invalid values,
and that should hold both for regular variables that have special
behaviors, and for the special-syntax cases like \timing.+1
Not sure I follow your reply here. There seems to be broad agreement to
improve how we handle both \set and "special" variables and the code
paths are related and this patch is touching them, so it seems like the
correct next step here is to adjust the patch to update the code based
on that agreement.
Are you working to make those changes? I'd rather we make the changes
to this code once rather than push what you have now only to turn around
and change it significantly again.
Thanks!
Stephen
Stephen Frost wrote:
Just fyi, there seems to be some issues with this patch because setting
my PROMPT1 and PROMPT2 variables result in rather odd behavior.
Good catch! The issue is that the patch broke the assumption
of prompt hooks that their argument points to a long-lived buffer.
The attached v4 fixes the bug by passing to hooks a pointer to the final
strdup'ed value in VariableSpace rather than temp space, as commented
in SetVariable().
Also I've changed something else in ParseVariableBool(). The code assumes
"false" when value==NULL, but when value is an empty string, the result
was true and considered valid, due to the following test being
positive when len==0 (both with HEAD or the v3 patch from this thread):
if (pg_strncasecmp(value, "true", len) == 0)
return true;
It happens that "" as a value yields the same result than "true" for this
test so it passes, but it seems rather unintentional.
The resulting problem from the POV of the user is that we can do that,
for instance:
test=> \set AUTOCOMMIT
without error message or feedback, and that leaves us without much
clue about autocommit being enabled:
test=> \echo :AUTOCOMMIT
test=>
So I've changed ParseVariableBool() in v4 to reject empty string as an
invalid boolean (but not NULL since the startup logic requires NULL
meaning false in the early initialization of these variables).
"make check" seems OK with that, I hope it doesn't cause any regression
elsewhere.
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
Attachments:
psql-var-hooks-v4.patchtext/plainDownload
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index a9a2fdb..61b2304 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -254,7 +254,7 @@ exec_command(const char *cmd,
if (opt1 != NULL && strncmp(opt1, prefix, sizeof(prefix) - 1) == 0)
{
reuse_previous =
- ParseVariableBool(opt1 + sizeof(prefix) - 1, prefix) ?
+ ParseVariableBool(opt1 + sizeof(prefix) - 1, prefix, NULL) ?
TRI_YES : TRI_NO;
free(opt1);
@@ -1548,7 +1548,7 @@ exec_command(const char *cmd,
OT_NORMAL, NULL, false);
if (opt)
- pset.timing = ParseVariableBool(opt, "\\timing");
+ pset.timing = ParseVariableBool(opt, "\\timing", NULL);
else
pset.timing = !pset.timing;
if (!pset.quiet)
@@ -2660,7 +2660,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
if (value && pg_strcasecmp(value, "auto") == 0)
popt->topt.expanded = 2;
else if (value)
- popt->topt.expanded = ParseVariableBool(value, param);
+ popt->topt.expanded = ParseVariableBool(value, param, NULL);
else
popt->topt.expanded = !popt->topt.expanded;
}
@@ -2669,7 +2669,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "numericlocale") == 0)
{
if (value)
- popt->topt.numericLocale = ParseVariableBool(value, param);
+ popt->topt.numericLocale = ParseVariableBool(value, param, NULL);
else
popt->topt.numericLocale = !popt->topt.numericLocale;
}
@@ -2724,7 +2724,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "t") == 0 || strcmp(param, "tuples_only") == 0)
{
if (value)
- popt->topt.tuples_only = ParseVariableBool(value, param);
+ popt->topt.tuples_only = ParseVariableBool(value, param, NULL);
else
popt->topt.tuples_only = !popt->topt.tuples_only;
}
@@ -2756,7 +2756,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
popt->topt.pager = 2;
else if (value)
{
- if (ParseVariableBool(value, param))
+ if (ParseVariableBool(value, param, NULL))
popt->topt.pager = 1;
else
popt->topt.pager = 0;
@@ -2778,7 +2778,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "footer") == 0)
{
if (value)
- popt->topt.default_footer = ParseVariableBool(value, param);
+ popt->topt.default_footer = ParseVariableBool(value, param, NULL);
else
popt->topt.default_footer = !popt->topt.default_footer;
}
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 7ce05fb..ba094f0 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -786,43 +786,68 @@ showVersion(void)
* This isn't an amazingly good place for them, but neither is anywhere else.
*/
-static void
+/*
+ * Hook to set an internal flag from a user-supplied string value.
+ * If the syntax is correct, affect *flag and return true.
+ * Otherwise, keep *flag untouched and return false.
+ */
+static bool
+generic_boolean_hook(const char *newval, const char* varname, bool *flag)
+{
+ bool isvalid;
+ bool val = ParseVariableBool(newval, varname, &isvalid);
+ if (isvalid)
+ *flag = val;
+ return isvalid;
+}
+
+static bool
autocommit_hook(const char *newval)
{
- pset.autocommit = ParseVariableBool(newval, "AUTOCOMMIT");
+ return generic_boolean_hook(newval, "AUTOCOMMIT", &pset.autocommit);
}
-static void
+static bool
on_error_stop_hook(const char *newval)
{
- pset.on_error_stop = ParseVariableBool(newval, "ON_ERROR_STOP");
+ return generic_boolean_hook(newval, "ON_ERROR_STOP", &pset.on_error_stop);
}
-static void
+static bool
quiet_hook(const char *newval)
{
- pset.quiet = ParseVariableBool(newval, "QUIET");
+ return generic_boolean_hook(newval, "QUIET", &pset.quiet);
}
-static void
+static bool
singleline_hook(const char *newval)
{
- pset.singleline = ParseVariableBool(newval, "SINGLELINE");
+ return generic_boolean_hook(newval, "SINGLELINE", &pset.singleline);
}
-static void
+static bool
singlestep_hook(const char *newval)
{
- pset.singlestep = ParseVariableBool(newval, "SINGLESTEP");
+ return generic_boolean_hook(newval, "SINGLESTEP", &pset.singlestep);
}
-static void
+static bool
fetch_count_hook(const char *newval)
{
- pset.fetch_count = ParseVariableNum(newval, -1, -1, false);
+ bool valid;
+ int value = ParseCheckVariableNum(newval, -1, &valid);
+ if (valid)
+ pset.fetch_count = value;
+ else
+ {
+ psql_error("invalid value \"%s\" for \"%s\"\n",
+ newval, "FETCH_COUNT");
+ return false;
+ }
+ return true;
}
-static void
+static bool
echo_hook(const char *newval)
{
if (newval == NULL)
@@ -837,39 +862,52 @@ echo_hook(const char *newval)
pset.echo = PSQL_ECHO_NONE;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "ECHO", "none");
- pset.echo = PSQL_ECHO_NONE;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "ECHO");
+ return false;
}
+ return true;
}
-static void
+static bool
echo_hidden_hook(const char *newval)
{
if (newval == NULL)
pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
else if (pg_strcasecmp(newval, "noexec") == 0)
pset.echo_hidden = PSQL_ECHO_HIDDEN_NOEXEC;
- else if (ParseVariableBool(newval, "ECHO_HIDDEN"))
- pset.echo_hidden = PSQL_ECHO_HIDDEN_ON;
- else /* ParseVariableBool printed msg if needed */
- pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
+ else
+ {
+ bool isvalid;
+ bool val = ParseVariableBool(newval, "ECHO_HIDDEN", &isvalid);
+ if (!isvalid)
+ return false; /* ParseVariableBool printed msg */
+ pset.echo_hidden = val ? PSQL_ECHO_HIDDEN_ON : PSQL_ECHO_HIDDEN_OFF;
+ }
+ return true;
}
-static void
+static bool
on_error_rollback_hook(const char *newval)
{
if (newval == NULL)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
else if (pg_strcasecmp(newval, "interactive") == 0)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_INTERACTIVE;
- else if (ParseVariableBool(newval, "ON_ERROR_ROLLBACK"))
- pset.on_error_rollback = PSQL_ERROR_ROLLBACK_ON;
- else /* ParseVariableBool printed msg if needed */
- pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
+ else
+ {
+ bool isvalid;
+ bool val = ParseVariableBool(newval, "ON_ERROR_ROLLBACK", &isvalid);
+ if (isvalid)
+ pset.on_error_rollback = val ? PSQL_ERROR_ROLLBACK_ON : PSQL_ERROR_ROLLBACK_OFF;
+ else
+ /* ParseVariableBool printed msg if needed */
+ return false;
+ }
+ return true;
}
-static void
+static bool
comp_keyword_case_hook(const char *newval)
{
if (newval == NULL)
@@ -884,13 +922,14 @@ comp_keyword_case_hook(const char *newval)
pset.comp_case = PSQL_COMP_CASE_LOWER;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "COMP_KEYWORD_CASE", "preserve-upper");
- pset.comp_case = PSQL_COMP_CASE_PRESERVE_UPPER;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "COMP_KEYWORD_CASE");
+ return false;
}
+ return true;
}
-static void
+static bool
histcontrol_hook(const char *newval)
{
if (newval == NULL)
@@ -905,31 +944,35 @@ histcontrol_hook(const char *newval)
pset.histcontrol = hctl_none;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "HISTCONTROL", "none");
- pset.histcontrol = hctl_none;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "HISTCONTROL");
+ return false;
}
+ return true;
}
-static void
+static bool
prompt1_hook(const char *newval)
{
pset.prompt1 = newval ? newval : "";
+ return true;
}
-static void
+static bool
prompt2_hook(const char *newval)
{
pset.prompt2 = newval ? newval : "";
+ return true;
}
-static void
+static bool
prompt3_hook(const char *newval)
{
pset.prompt3 = newval ? newval : "";
+ return true;
}
-static void
+static bool
verbosity_hook(const char *newval)
{
if (newval == NULL)
@@ -942,16 +985,17 @@ verbosity_hook(const char *newval)
pset.verbosity = PQERRORS_VERBOSE;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "VERBOSITY", "default");
- pset.verbosity = PQERRORS_DEFAULT;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "VERBOSITY");
+ return false;
}
if (pset.db)
PQsetErrorVerbosity(pset.db, pset.verbosity);
+ return true;
}
-static void
+static bool
show_context_hook(const char *newval)
{
if (newval == NULL)
@@ -964,13 +1008,14 @@ show_context_hook(const char *newval)
pset.show_context = PQSHOW_CONTEXT_ALWAYS;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "SHOW_CONTEXT", "errors");
- pset.show_context = PQSHOW_CONTEXT_ERRORS;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "SHOW_CONTEXT");
+ return false;
}
if (pset.db)
PQsetErrorContextVisibility(pset.db, pset.show_context);
+ return true;
}
diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c
index f43f418..8532af4 100644
--- a/src/bin/psql/variables.c
+++ b/src/bin/psql/variables.c
@@ -86,24 +86,28 @@ GetVariable(VariableSpace space, const char *name)
*
* "name" is the name of the variable we're assigning to, to use in error
* report if any. Pass name == NULL to suppress the error report.
+ *
+ * "*valid" reports whether "value" was syntactically valid, unless valid == NULL
*/
bool
-ParseVariableBool(const char *value, const char *name)
+ParseVariableBool(const char *value, const char *name, bool *valid)
{
size_t len;
+ if (valid)
+ *valid = true;
if (value == NULL)
return false; /* not set -> assume "off" */
len = strlen(value);
- if (pg_strncasecmp(value, "true", len) == 0)
+ if (len > 0 && pg_strncasecmp(value, "true", len) == 0)
return true;
- else if (pg_strncasecmp(value, "false", len) == 0)
+ else if (len > 0 && pg_strncasecmp(value, "false", len) == 0)
return false;
- else if (pg_strncasecmp(value, "yes", len) == 0)
+ else if (len > 0 && pg_strncasecmp(value, "yes", len) == 0)
return true;
- else if (pg_strncasecmp(value, "no", len) == 0)
+ else if (len > 0 && pg_strncasecmp(value, "no", len) == 0)
return false;
/* 'o' is not unique enough */
else if (pg_strncasecmp(value, "on", (len > 2 ? len : 2)) == 0)
@@ -116,10 +120,16 @@ ParseVariableBool(const char *value, const char *name)
return false;
else
{
- /* NULL is treated as false, so a non-matching value is 'true' */
+ /*
+ * NULL is treated as false, so a non-matching value is 'true'.
+ * A caller that cares about syntactic conformance should
+ * check *valid to know whether the value was recognized.
+ */
if (name)
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- value, name, "on");
+ psql_error("unrecognized value \"%s\" for \"%s\": boolean expected\n",
+ value, name);
+ if (valid)
+ *valid = false;
return true;
}
}
@@ -154,6 +164,35 @@ ParseVariableNum(const char *val,
return result;
}
+/*
+ * Read numeric variable, or defaultval if it is not set.
+ * Trailing contents are not allowed.
+ * "valid" points to a bool reporting whether the value was valid.
+ */
+int
+ParseCheckVariableNum(const char *val,
+ int defaultval,
+ bool *valid)
+{
+ int result;
+
+ if (!val)
+ {
+ *valid = true;
+ result = defaultval;
+ }
+ else
+ {
+ char *end;
+
+ errno = 0;
+ result = strtol(val, &end, 0);
+ *valid = (val[0] != '\0' && errno == 0 && *end == '\0');
+ }
+
+ return result;
+}
+
int
GetVariableNum(VariableSpace space,
const char *name,
@@ -205,13 +244,26 @@ SetVariable(VariableSpace space, const char *name, const char *value)
{
if (strcmp(current->name, name) == 0)
{
- /* found entry, so update */
- if (current->value)
- free(current->value);
- current->value = pg_strdup(value);
- if (current->assign_hook)
- (*current->assign_hook) (current->value);
- return true;
+ /*
+ * Found entry, so update, unless a hook returns false.
+ * The hook needs the value in a buffer with the same lifespan
+ * as the variable, so allocate it right away, even if it needs
+ * to be freed just after when the hook returns false.
+ */
+ char *new_value = pg_strdup(value);
+
+ bool confirmed = current->assign_hook ?
+ (*current->assign_hook) (new_value) : true;
+
+ if (confirmed)
+ {
+ pg_free(current->value);
+ current->value = new_value;
+ }
+ else
+ pg_free(new_value); /* and current->value is left unchanged */
+
+ return confirmed;
}
}
@@ -248,7 +300,7 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
{
/* found entry, so update */
current->assign_hook = hook;
- (*hook) (current->value);
+ (*hook) (current->value); /* ignore return value */
return true;
}
}
@@ -260,7 +312,7 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
current->assign_hook = hook;
current->next = NULL;
previous->next = current;
- (*hook) (NULL);
+ (void)(*hook) (NULL); /* ignore return value */
return true;
}
diff --git a/src/bin/psql/variables.h b/src/bin/psql/variables.h
index d7a05a1..38396ba 100644
--- a/src/bin/psql/variables.h
+++ b/src/bin/psql/variables.h
@@ -20,7 +20,7 @@
* Note: if value == NULL then the variable is logically unset, but we are
* keeping the struct around so as not to forget about its hook function.
*/
-typedef void (*VariableAssignHook) (const char *newval);
+typedef bool (*VariableAssignHook) (const char *newval);
struct _variable
{
@@ -35,11 +35,14 @@ typedef struct _variable *VariableSpace;
VariableSpace CreateVariableSpace(void);
const char *GetVariable(VariableSpace space, const char *name);
-bool ParseVariableBool(const char *value, const char *name);
+bool ParseVariableBool(const char *value, const char *name, bool *valid);
int ParseVariableNum(const char *val,
int defaultval,
int faultval,
bool allowtrail);
+int ParseCheckVariableNum(const char *val,
+ int defaultval,
+ bool *valid);
int GetVariableNum(VariableSpace space,
const char *name,
int defaultval,
Stephen Frost wrote:
Are you working to make those changes? I'd rather we make the changes
to this code once rather than push what you have now only to turn around
and change it significantly again.
If, as a maintainer, you prefer this together in one patch, I'm fine
with that. So I'll update it shortly with changes in \timing and
a few other callers of ParseVariableBool().
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Daniel,
* Daniel Verite (daniel@manitou-mail.org) wrote:
"make check" seems OK with that, I hope it doesn't cause any regression
elsewhere.
You can see what the code coverage of psql is in our current regression
tests by going here:
http://coverage.postgresql.org/src/bin/psql/index.html
It's not exactly a pretty sight and certainly not all callers of
ParseVariableBool() are covered.
I'd strongly suggest you either do sufficient manual testing, or add
regression tests, most likely using the tap test system (you can see an
example of that in src/bin/pg_dump/t and in other 't' directories).
You can generate that report after you make changes yourself using
'make coverage-html'.
Thanks!
Stephen
Daniel,
* Daniel Verite (daniel@manitou-mail.org) wrote:
Stephen Frost wrote:
Are you working to make those changes? I'd rather we make the changes
to this code once rather than push what you have now only to turn around
and change it significantly again.If, as a maintainer, you prefer this together in one patch, I'm fine
with that. So I'll update it shortly with changes in \timing and
a few other callers of ParseVariableBool().
Did you get a chance to review and consider the other comments from my
initial review about how we might use a different approach for bools, et
al?
Thanks!
Stephen
Stephen Frost wrote:
Are you working to make those changes? I'd rather we make the changes
to this code once rather than push what you have now only to turn around
and change it significantly again.
So I went through the psql commands which don't fail on parse errors
for booleans. I'd like to attract attention on \c, because I see
several options.
\c [-reuse-previous=BOOL] ...other args..
Current: if we can't parse BOOL, assume it's ON, and go on with reconnecting.
Option1: if we can't parse BOOL, stop here, don't reconnect, set
the command result as "failed", also ignoring the other arguments.
Option2: maybe we want to create a difference between interactive
and non-interactive use, as there's already one in handling
the failure to connect through \c.
The manpage says:
If the connection attempt failed (wrong user name, access denied,
etc.), the previous connection will only be kept if psql is in
interactive mode. When executing a non-interactive script,
processing will immediately stop with an error.
Proposal: if interactive, same as Option1.
If not interactive, -reuse-previous=bogus has the same outcome
as a failure to connect. Which I think means two subcases:
if it's through \i then kill the connection but don't quit psql
if it's through -f then quit psql.
Option3: leave this command unchanged, avoiding trouble.
\timing BOOL
Current: non-parseable BOOL interpreted as TRUE. Empty BOOL toggles the
state.
Proposal: on non-parseable BOOL, command failure and pset.timing is
left unchanged.
\pset [x | expanded | vertical ] BOOL
\pset numericlocale BOOL
\pset [tuples_only | t] BOOL
\pset footer BOOL
\t BOOL (falls into pset_do("tuples_only", ...))
\pset pager BOOL
Current: non-parseable non-empty BOOL interpreted as TRUE. Empty BOOL
toggles the on/off state. In some cases, BOOL interpretation is attempted
only after specific built-in values have been ruled out first.
Proposal: non-parseable BOOL implies command failure and unchanged state.
About the empty string when a BOOL is expected. Only \c -reuse-previous
seems concerned:
#= \c -reuse-previous=
acts the same as
#= \c -reuse-previous=ON
Proposal: handle empty as when the value is bogus.
The other commands interpret this lack of value specifically to toggle
the state, so it's a non-issue for them.
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Stephen Frost wrote:
That certainly doesn't feel right. I'm thinking that if we're going to
throw an error back to the user about a value being invalid then we
shouldn't change the current value.My initial thought was that perhaps we should pass the current value to
ParseVariableBool() and let it return whatever the "right" answer is,
however, your use of ParseVariableBool() for enums that happen to accept
on/off means that won't really work.
That's not needed once ParseVariableBool() informs the caller about
the validity of the boolean expression, which is what the patch already
does.
For instance I just implemented it for \timing and the diff consists of just
that:
if (opt)
- pset.timing = ParseVariableBool(opt, "\\timing",
NULL);
+ {
+ bool newval = ParseVariableBool(opt, "\\timing",
&success);
+ if (success)
+ pset.timing = newval;
+ }
else
pset.timing = !pset.timing;
That makes \timing foobar being rejected as a bad command with a
proper error message and no change of state, which is just what we want.
Perhaps the right answer is to flip this around a bit and treat booleans
as a special case of enums and have a generic solution for enums.
Consider something like:ParseVariableEnum(valid_enums, str_value, name, curr_value);
'valid_enums' would be a simple list of what the valid values are for a
given variable and their corresponding value, str_value the string the
user typed, name the name of the variable, and curr_value the current
value of the variable.
Firstly I'd like to insist that such a refactoring is not necessary
for this patch and I feel like it would be out of place in it.
That being said, if we wanted this, I think it would be successful
only if we'd first change our internal variables pset.* from a struct
of different types to a list of variables from some kind of common
abstract type and an abstraction layer to access them.
That would be an order of magnitude more sophisticated than what we
have.
Otherwise as I tried to explain in [1]/messages/by-id/fc879967-da93-43b6-aa5a-92f2d825e786@mm, I don't see how we could write
a ParseVariableEnum() that would return different types
and take variable inputs.
Or if we say that ParseVariableEnum should not return the value
but affect the variable directly, that would require refactoring
all call sites, and what's the benefit that would justify
such large changes?
Plus we have two different non-mergeable concepts of variables
that need this parser:
psql variables from VariableSpace stored as strings,
and C variables directly instantiated as native types.
Also, the argument that bools are just another type of enums
is legitimate in theory, but as in psql we accept any left-anchored
match of true/false/on/off/0/1, it means that the enumeration
of values is in fact:
0
1
t
tr
tru
true
f
fa
fal
fals
false
on
of
off
I don't see that it would help if the code treated the above like just a
vanilla list of values, comparable to the other qualifiers like "auto",
"expanded", "vertical", an so on, notwithstanding the fact
that they don't share the same types.
I think that the current code with ParseVariableBool() that only
handles booleans is better in terms of separation of concerns
and readability.
[1]: /messages/by-id/fc879967-da93-43b6-aa5a-92f2d825e786@mm
/messages/by-id/fc879967-da93-43b6-aa5a-92f2d825e786@mm
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I wrote:
So I went through the psql commands which don't fail on parse errors
for booleans
[...]
Here's a v5 patch implementing the suggestions mentioned upthread:
all meta-commands calling ParseVariableBool() now fail
when the boolean argument can't be parsed successfully.
Also includes a minor change to SetVariableAssignHook() that now
returns the result of the hook it calls after installing it.
It doesn't make any difference in psql behavior since callers
of SetVariableAssignHook() ignore its return value, but it's
more consistent with SetVariable().
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
Attachments:
psql-var-hooks-v5.patchtext/plainDownload
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index a9a2fdb..356467b 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -254,25 +254,30 @@ exec_command(const char *cmd,
if (opt1 != NULL && strncmp(opt1, prefix, sizeof(prefix) - 1) == 0)
{
reuse_previous =
- ParseVariableBool(opt1 + sizeof(prefix) - 1, prefix) ?
+ ParseVariableBool(opt1 + sizeof(prefix) - 1, prefix, &success) ?
TRI_YES : TRI_NO;
-
- free(opt1);
- opt1 = read_connect_arg(scan_state);
+ if (success)
+ {
+ free(opt1);
+ opt1 = read_connect_arg(scan_state);
+ }
}
else
reuse_previous = TRI_DEFAULT;
- opt2 = read_connect_arg(scan_state);
- opt3 = read_connect_arg(scan_state);
- opt4 = read_connect_arg(scan_state);
+ if (success) /* do not attempt to connect if reuse_previous argument was invalid */
+ {
+ opt2 = read_connect_arg(scan_state);
+ opt3 = read_connect_arg(scan_state);
+ opt4 = read_connect_arg(scan_state);
- success = do_connect(reuse_previous, opt1, opt2, opt3, opt4);
+ success = do_connect(reuse_previous, opt1, opt2, opt3, opt4);
+ free(opt2);
+ free(opt3);
+ free(opt4);
+ }
free(opt1);
- free(opt2);
- free(opt3);
- free(opt4);
}
/* \cd */
@@ -1548,7 +1553,11 @@ exec_command(const char *cmd,
OT_NORMAL, NULL, false);
if (opt)
- pset.timing = ParseVariableBool(opt, "\\timing");
+ {
+ bool newval = ParseVariableBool(opt, "\\timing", &success);
+ if (success)
+ pset.timing = newval;
+ }
else
pset.timing = !pset.timing;
if (!pset.quiet)
@@ -2660,7 +2669,14 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
if (value && pg_strcasecmp(value, "auto") == 0)
popt->topt.expanded = 2;
else if (value)
- popt->topt.expanded = ParseVariableBool(value, param);
+ {
+ bool valid;
+ bool newval = ParseVariableBool(value, param, &valid);
+ if (valid)
+ popt->topt.expanded = newval;
+ else
+ return false;
+ }
else
popt->topt.expanded = !popt->topt.expanded;
}
@@ -2668,8 +2684,16 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
/* locale-aware numeric output */
else if (strcmp(param, "numericlocale") == 0)
{
+
if (value)
- popt->topt.numericLocale = ParseVariableBool(value, param);
+ {
+ bool valid;
+ bool newval = ParseVariableBool(value, param, &valid);
+ if (valid)
+ popt->topt.numericLocale = newval;
+ else
+ return false;
+ }
else
popt->topt.numericLocale = !popt->topt.numericLocale;
}
@@ -2724,7 +2748,14 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "t") == 0 || strcmp(param, "tuples_only") == 0)
{
if (value)
- popt->topt.tuples_only = ParseVariableBool(value, param);
+ {
+ bool valid;
+ bool newval = ParseVariableBool(value, param, &valid);
+ if (valid)
+ popt->topt.tuples_only = newval;
+ else
+ return false;
+ }
else
popt->topt.tuples_only = !popt->topt.tuples_only;
}
@@ -2756,10 +2787,11 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
popt->topt.pager = 2;
else if (value)
{
- if (ParseVariableBool(value, param))
- popt->topt.pager = 1;
- else
- popt->topt.pager = 0;
+ bool valid;
+ bool on = ParseVariableBool(value, param, &valid);
+ if (!valid)
+ return false;
+ popt->topt.pager = on ? 1 : 0;
}
else if (popt->topt.pager == 1)
popt->topt.pager = 0;
@@ -2778,7 +2810,14 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "footer") == 0)
{
if (value)
- popt->topt.default_footer = ParseVariableBool(value, param);
+ {
+ bool valid;
+ bool newval = ParseVariableBool(value, param, &valid);
+ if (valid)
+ popt->topt.default_footer = newval;
+ else
+ return false;
+ }
else
popt->topt.default_footer = !popt->topt.default_footer;
}
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 7ce05fb..ba094f0 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -786,43 +786,68 @@ showVersion(void)
* This isn't an amazingly good place for them, but neither is anywhere else.
*/
-static void
+/*
+ * Hook to set an internal flag from a user-supplied string value.
+ * If the syntax is correct, affect *flag and return true.
+ * Otherwise, keep *flag untouched and return false.
+ */
+static bool
+generic_boolean_hook(const char *newval, const char* varname, bool *flag)
+{
+ bool isvalid;
+ bool val = ParseVariableBool(newval, varname, &isvalid);
+ if (isvalid)
+ *flag = val;
+ return isvalid;
+}
+
+static bool
autocommit_hook(const char *newval)
{
- pset.autocommit = ParseVariableBool(newval, "AUTOCOMMIT");
+ return generic_boolean_hook(newval, "AUTOCOMMIT", &pset.autocommit);
}
-static void
+static bool
on_error_stop_hook(const char *newval)
{
- pset.on_error_stop = ParseVariableBool(newval, "ON_ERROR_STOP");
+ return generic_boolean_hook(newval, "ON_ERROR_STOP", &pset.on_error_stop);
}
-static void
+static bool
quiet_hook(const char *newval)
{
- pset.quiet = ParseVariableBool(newval, "QUIET");
+ return generic_boolean_hook(newval, "QUIET", &pset.quiet);
}
-static void
+static bool
singleline_hook(const char *newval)
{
- pset.singleline = ParseVariableBool(newval, "SINGLELINE");
+ return generic_boolean_hook(newval, "SINGLELINE", &pset.singleline);
}
-static void
+static bool
singlestep_hook(const char *newval)
{
- pset.singlestep = ParseVariableBool(newval, "SINGLESTEP");
+ return generic_boolean_hook(newval, "SINGLESTEP", &pset.singlestep);
}
-static void
+static bool
fetch_count_hook(const char *newval)
{
- pset.fetch_count = ParseVariableNum(newval, -1, -1, false);
+ bool valid;
+ int value = ParseCheckVariableNum(newval, -1, &valid);
+ if (valid)
+ pset.fetch_count = value;
+ else
+ {
+ psql_error("invalid value \"%s\" for \"%s\"\n",
+ newval, "FETCH_COUNT");
+ return false;
+ }
+ return true;
}
-static void
+static bool
echo_hook(const char *newval)
{
if (newval == NULL)
@@ -837,39 +862,52 @@ echo_hook(const char *newval)
pset.echo = PSQL_ECHO_NONE;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "ECHO", "none");
- pset.echo = PSQL_ECHO_NONE;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "ECHO");
+ return false;
}
+ return true;
}
-static void
+static bool
echo_hidden_hook(const char *newval)
{
if (newval == NULL)
pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
else if (pg_strcasecmp(newval, "noexec") == 0)
pset.echo_hidden = PSQL_ECHO_HIDDEN_NOEXEC;
- else if (ParseVariableBool(newval, "ECHO_HIDDEN"))
- pset.echo_hidden = PSQL_ECHO_HIDDEN_ON;
- else /* ParseVariableBool printed msg if needed */
- pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
+ else
+ {
+ bool isvalid;
+ bool val = ParseVariableBool(newval, "ECHO_HIDDEN", &isvalid);
+ if (!isvalid)
+ return false; /* ParseVariableBool printed msg */
+ pset.echo_hidden = val ? PSQL_ECHO_HIDDEN_ON : PSQL_ECHO_HIDDEN_OFF;
+ }
+ return true;
}
-static void
+static bool
on_error_rollback_hook(const char *newval)
{
if (newval == NULL)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
else if (pg_strcasecmp(newval, "interactive") == 0)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_INTERACTIVE;
- else if (ParseVariableBool(newval, "ON_ERROR_ROLLBACK"))
- pset.on_error_rollback = PSQL_ERROR_ROLLBACK_ON;
- else /* ParseVariableBool printed msg if needed */
- pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
+ else
+ {
+ bool isvalid;
+ bool val = ParseVariableBool(newval, "ON_ERROR_ROLLBACK", &isvalid);
+ if (isvalid)
+ pset.on_error_rollback = val ? PSQL_ERROR_ROLLBACK_ON : PSQL_ERROR_ROLLBACK_OFF;
+ else
+ /* ParseVariableBool printed msg if needed */
+ return false;
+ }
+ return true;
}
-static void
+static bool
comp_keyword_case_hook(const char *newval)
{
if (newval == NULL)
@@ -884,13 +922,14 @@ comp_keyword_case_hook(const char *newval)
pset.comp_case = PSQL_COMP_CASE_LOWER;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "COMP_KEYWORD_CASE", "preserve-upper");
- pset.comp_case = PSQL_COMP_CASE_PRESERVE_UPPER;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "COMP_KEYWORD_CASE");
+ return false;
}
+ return true;
}
-static void
+static bool
histcontrol_hook(const char *newval)
{
if (newval == NULL)
@@ -905,31 +944,35 @@ histcontrol_hook(const char *newval)
pset.histcontrol = hctl_none;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "HISTCONTROL", "none");
- pset.histcontrol = hctl_none;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "HISTCONTROL");
+ return false;
}
+ return true;
}
-static void
+static bool
prompt1_hook(const char *newval)
{
pset.prompt1 = newval ? newval : "";
+ return true;
}
-static void
+static bool
prompt2_hook(const char *newval)
{
pset.prompt2 = newval ? newval : "";
+ return true;
}
-static void
+static bool
prompt3_hook(const char *newval)
{
pset.prompt3 = newval ? newval : "";
+ return true;
}
-static void
+static bool
verbosity_hook(const char *newval)
{
if (newval == NULL)
@@ -942,16 +985,17 @@ verbosity_hook(const char *newval)
pset.verbosity = PQERRORS_VERBOSE;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "VERBOSITY", "default");
- pset.verbosity = PQERRORS_DEFAULT;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "VERBOSITY");
+ return false;
}
if (pset.db)
PQsetErrorVerbosity(pset.db, pset.verbosity);
+ return true;
}
-static void
+static bool
show_context_hook(const char *newval)
{
if (newval == NULL)
@@ -964,13 +1008,14 @@ show_context_hook(const char *newval)
pset.show_context = PQSHOW_CONTEXT_ALWAYS;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "SHOW_CONTEXT", "errors");
- pset.show_context = PQSHOW_CONTEXT_ERRORS;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "SHOW_CONTEXT");
+ return false;
}
if (pset.db)
PQsetErrorContextVisibility(pset.db, pset.show_context);
+ return true;
}
diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c
index f43f418..1302a32 100644
--- a/src/bin/psql/variables.c
+++ b/src/bin/psql/variables.c
@@ -86,24 +86,28 @@ GetVariable(VariableSpace space, const char *name)
*
* "name" is the name of the variable we're assigning to, to use in error
* report if any. Pass name == NULL to suppress the error report.
+ *
+ * "*valid" reports whether "value" was syntactically valid, unless valid == NULL
*/
bool
-ParseVariableBool(const char *value, const char *name)
+ParseVariableBool(const char *value, const char *name, bool *valid)
{
size_t len;
+ if (valid)
+ *valid = true;
if (value == NULL)
return false; /* not set -> assume "off" */
len = strlen(value);
- if (pg_strncasecmp(value, "true", len) == 0)
+ if (len > 0 && pg_strncasecmp(value, "true", len) == 0)
return true;
- else if (pg_strncasecmp(value, "false", len) == 0)
+ else if (len > 0 && pg_strncasecmp(value, "false", len) == 0)
return false;
- else if (pg_strncasecmp(value, "yes", len) == 0)
+ else if (len > 0 && pg_strncasecmp(value, "yes", len) == 0)
return true;
- else if (pg_strncasecmp(value, "no", len) == 0)
+ else if (len > 0 && pg_strncasecmp(value, "no", len) == 0)
return false;
/* 'o' is not unique enough */
else if (pg_strncasecmp(value, "on", (len > 2 ? len : 2)) == 0)
@@ -116,10 +120,16 @@ ParseVariableBool(const char *value, const char *name)
return false;
else
{
- /* NULL is treated as false, so a non-matching value is 'true' */
+ /*
+ * NULL is treated as false, so a non-matching value is 'true'.
+ * A caller that cares about syntactic conformance should
+ * check *valid to know whether the value was recognized.
+ */
if (name)
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- value, name, "on");
+ psql_error("unrecognized value \"%s\" for \"%s\": boolean expected\n",
+ value, name);
+ if (valid)
+ *valid = false;
return true;
}
}
@@ -154,6 +164,35 @@ ParseVariableNum(const char *val,
return result;
}
+/*
+ * Read numeric variable, or defaultval if it is not set.
+ * Trailing contents are not allowed.
+ * "valid" points to a bool reporting whether the value was valid.
+ */
+int
+ParseCheckVariableNum(const char *val,
+ int defaultval,
+ bool *valid)
+{
+ int result;
+
+ if (!val)
+ {
+ *valid = true;
+ result = defaultval;
+ }
+ else
+ {
+ char *end;
+
+ errno = 0;
+ result = strtol(val, &end, 0);
+ *valid = (val[0] != '\0' && errno == 0 && *end == '\0');
+ }
+
+ return result;
+}
+
int
GetVariableNum(VariableSpace space,
const char *name,
@@ -205,13 +244,26 @@ SetVariable(VariableSpace space, const char *name, const char *value)
{
if (strcmp(current->name, name) == 0)
{
- /* found entry, so update */
- if (current->value)
- free(current->value);
- current->value = pg_strdup(value);
- if (current->assign_hook)
- (*current->assign_hook) (current->value);
- return true;
+ /*
+ * Found entry, so update, unless a hook returns false.
+ * The hook needs the value in a buffer with the same lifespan
+ * as the variable, so allocate it right away, even if it needs
+ * to be freed just after when the hook returns false.
+ */
+ char *new_value = pg_strdup(value);
+
+ bool confirmed = current->assign_hook ?
+ (*current->assign_hook) (new_value) : true;
+
+ if (confirmed)
+ {
+ pg_free(current->value);
+ current->value = new_value;
+ }
+ else
+ pg_free(new_value); /* and current->value is left unchanged */
+
+ return confirmed;
}
}
@@ -248,8 +300,7 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
{
/* found entry, so update */
current->assign_hook = hook;
- (*hook) (current->value);
- return true;
+ return (*hook) (current->value);
}
}
@@ -260,8 +311,7 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
current->assign_hook = hook;
current->next = NULL;
previous->next = current;
- (*hook) (NULL);
- return true;
+ return (*hook) (NULL);
}
bool
diff --git a/src/bin/psql/variables.h b/src/bin/psql/variables.h
index d7a05a1..38396ba 100644
--- a/src/bin/psql/variables.h
+++ b/src/bin/psql/variables.h
@@ -20,7 +20,7 @@
* Note: if value == NULL then the variable is logically unset, but we are
* keeping the struct around so as not to forget about its hook function.
*/
-typedef void (*VariableAssignHook) (const char *newval);
+typedef bool (*VariableAssignHook) (const char *newval);
struct _variable
{
@@ -35,11 +35,14 @@ typedef struct _variable *VariableSpace;
VariableSpace CreateVariableSpace(void);
const char *GetVariable(VariableSpace space, const char *name);
-bool ParseVariableBool(const char *value, const char *name);
+bool ParseVariableBool(const char *value, const char *name, bool *valid);
int ParseVariableNum(const char *val,
int defaultval,
int faultval,
bool allowtrail);
+int ParseCheckVariableNum(const char *val,
+ int defaultval,
+ bool *valid);
int GetVariableNum(VariableSpace space,
const char *name,
int defaultval,
On Wed, Nov 23, 2016 at 11:17 PM, Daniel Verite <daniel@manitou-mail.org>
wrote:
I wrote:
So I went through the psql commands which don't fail on parse errors
for booleans
[...]Here's a v5 patch implementing the suggestions mentioned upthread:
all meta-commands calling ParseVariableBool() now fail
when the boolean argument can't be parsed successfully.Also includes a minor change to SetVariableAssignHook() that now
returns the result of the hook it calls after installing it.
It doesn't make any difference in psql behavior since callers
of SetVariableAssignHook() ignore its return value, but it's
more consistent with SetVariable().
Moved to next CF with "needs review" status.
Regards,
Hari Babu
Fujitsu Australia
I applied and tested the patch on latest master branch.
Kindly consider following comments,
ParseVariableBool(const char *value, const char *name)
+ParseVariableBool(const char *value, const char *name, bool *valid)
{
size_t len;
+ if (valid)
+ *valid = true;
psql_error("unrecognized value \"%s\" for \"%s\": boolean
expected\n",
+ value, name);
+ if (valid)
+ *valid = false;
Why do we need this? IMO, valid should be always set to true if the value
is parsed to be correct.
There should not be an option to the caller to not follow the behaviour of
setting valid to either true/false.
As it is in the current patch, all callers of ParseVariableBool follow the
behaviour of setting valid with either true/false.
In following examples, incorrect error message is begin displayed.
“ON_ERROR_ROLLBACK” is an enum and also
accepts value 'interactive' . The error message says boolean expected.
postgres=# \set ON_ERROR_ROLLBACK eretere
unrecognized value "eretere" for "ON_ERROR_ROLLBACK": boolean expected
\set: error while setting variable
Similarly for ECHO_HIDDEN which is also an enum and accepts value 'no_exec'
postgres=# \set ECHO_HIDDEN NULL
unrecognized value "NULL" for "ECHO_HIDDEN": boolean expected
\set: error while setting variable
+ bool newval = ParseVariableBool(opt, "\\timing", &success);
+ if (success)
+ pset.timing = newval;
+ }
else
pset.timing = !pset.timing;
if (!pset.quiet)
@@ -2660,7 +2669,14 @@ do_pset(const char *param, const char *value,
printQueryOpt *popt, bool quiet)
if (value && pg_strcasecmp(value, "auto") == 0)
popt->topt.expanded = 2;
else if (value)
- popt->topt.expanded = ParseVariableBool(value, param);
+ {
+ bool valid;
+ bool newval = ParseVariableBool(value, param, &valid);
+ if (valid)
Should same variable names (success / valid) be used for consistency?
Thank you,
Rahila Syed
On Wed, Nov 23, 2016 at 5:47 PM, Daniel Verite <daniel@manitou-mail.org>
wrote:
Show quoted text
I wrote:
So I went through the psql commands which don't fail on parse errors
for booleans
[...]Here's a v5 patch implementing the suggestions mentioned upthread:
all meta-commands calling ParseVariableBool() now fail
when the boolean argument can't be parsed successfully.Also includes a minor change to SetVariableAssignHook() that now
returns the result of the hook it calls after installing it.
It doesn't make any difference in psql behavior since callers
of SetVariableAssignHook() ignore its return value, but it's
more consistent with SetVariable().Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
Rahila Syed wrote:
Kindly consider following comments,
Sorry for taking so long to post an update.
There should not be an option to the caller to not follow the behaviour of
setting valid to either true/false.
OK, fixed.
In following examples, incorrect error message is begin displayed.
“ON_ERROR_ROLLBACK” is an enum and also
accepts value 'interactive' . The error message says boolean expected.
Indeed. Fixed for all callers of ParseVariableBool() than can accept
non-boolean arguments too.
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
Attachments:
psql-var-hooks-v6.patchtext/plainDownload
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index a9a2fdb..46bcf19 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -254,25 +254,30 @@ exec_command(const char *cmd,
if (opt1 != NULL && strncmp(opt1, prefix, sizeof(prefix) - 1) == 0)
{
reuse_previous =
- ParseVariableBool(opt1 + sizeof(prefix) - 1, prefix) ?
+ ParseVariableBool(opt1 + sizeof(prefix) - 1, prefix, &success) ?
TRI_YES : TRI_NO;
-
- free(opt1);
- opt1 = read_connect_arg(scan_state);
+ if (success)
+ {
+ free(opt1);
+ opt1 = read_connect_arg(scan_state);
+ }
}
else
reuse_previous = TRI_DEFAULT;
- opt2 = read_connect_arg(scan_state);
- opt3 = read_connect_arg(scan_state);
- opt4 = read_connect_arg(scan_state);
+ if (success) /* do not attempt to connect if reuse_previous argument was invalid */
+ {
+ opt2 = read_connect_arg(scan_state);
+ opt3 = read_connect_arg(scan_state);
+ opt4 = read_connect_arg(scan_state);
- success = do_connect(reuse_previous, opt1, opt2, opt3, opt4);
+ success = do_connect(reuse_previous, opt1, opt2, opt3, opt4);
+ free(opt2);
+ free(opt3);
+ free(opt4);
+ }
free(opt1);
- free(opt2);
- free(opt3);
- free(opt4);
}
/* \cd */
@@ -1548,7 +1553,11 @@ exec_command(const char *cmd,
OT_NORMAL, NULL, false);
if (opt)
- pset.timing = ParseVariableBool(opt, "\\timing");
+ {
+ bool newval = ParseVariableBool(opt, "\\timing", &success);
+ if (success)
+ pset.timing = newval;
+ }
else
pset.timing = !pset.timing;
if (!pset.quiet)
@@ -2660,7 +2669,18 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
if (value && pg_strcasecmp(value, "auto") == 0)
popt->topt.expanded = 2;
else if (value)
- popt->topt.expanded = ParseVariableBool(value, param);
+ {
+ bool valid;
+ bool newval = ParseVariableBool(value, NULL, &valid);
+ if (valid)
+ popt->topt.expanded = newval;
+ else
+ {
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ value, param);
+ return false;
+ }
+ }
else
popt->topt.expanded = !popt->topt.expanded;
}
@@ -2669,7 +2689,14 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "numericlocale") == 0)
{
if (value)
- popt->topt.numericLocale = ParseVariableBool(value, param);
+ {
+ bool valid;
+ bool newval = ParseVariableBool(value, param, &valid);
+ if (valid)
+ popt->topt.numericLocale = newval;
+ else
+ return false;
+ }
else
popt->topt.numericLocale = !popt->topt.numericLocale;
}
@@ -2724,7 +2751,18 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "t") == 0 || strcmp(param, "tuples_only") == 0)
{
if (value)
- popt->topt.tuples_only = ParseVariableBool(value, param);
+ {
+ bool valid;
+ bool newval = ParseVariableBool(value, NULL, &valid);
+ if (valid)
+ popt->topt.tuples_only = newval;
+ else
+ {
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ value, param);
+ return false;
+ }
+ }
else
popt->topt.tuples_only = !popt->topt.tuples_only;
}
@@ -2756,10 +2794,15 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
popt->topt.pager = 2;
else if (value)
{
- if (ParseVariableBool(value, param))
- popt->topt.pager = 1;
- else
- popt->topt.pager = 0;
+ bool valid;
+ bool on = ParseVariableBool(value, NULL, &valid);
+ if (!valid)
+ {
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ value, param);
+ return false;
+ }
+ popt->topt.pager = on ? 1 : 0;
}
else if (popt->topt.pager == 1)
popt->topt.pager = 0;
@@ -2778,7 +2821,14 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "footer") == 0)
{
if (value)
- popt->topt.default_footer = ParseVariableBool(value, param);
+ {
+ bool valid;
+ bool newval = ParseVariableBool(value, param, &valid);
+ if (valid)
+ popt->topt.default_footer = newval;
+ else
+ return false;
+ }
else
popt->topt.default_footer = !popt->topt.default_footer;
}
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 7ce05fb..b8281d4 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -786,43 +786,68 @@ showVersion(void)
* This isn't an amazingly good place for them, but neither is anywhere else.
*/
-static void
+/*
+ * Hook to set an internal flag from a user-supplied string value.
+ * If the syntax is correct, affect *flag and return true.
+ * Otherwise, keep *flag untouched and return false.
+ */
+static bool
+generic_boolean_hook(const char *newval, const char* varname, bool *flag)
+{
+ bool isvalid;
+ bool val = ParseVariableBool(newval, varname, &isvalid);
+ if (isvalid)
+ *flag = val;
+ return isvalid;
+}
+
+static bool
autocommit_hook(const char *newval)
{
- pset.autocommit = ParseVariableBool(newval, "AUTOCOMMIT");
+ return generic_boolean_hook(newval, "AUTOCOMMIT", &pset.autocommit);
}
-static void
+static bool
on_error_stop_hook(const char *newval)
{
- pset.on_error_stop = ParseVariableBool(newval, "ON_ERROR_STOP");
+ return generic_boolean_hook(newval, "ON_ERROR_STOP", &pset.on_error_stop);
}
-static void
+static bool
quiet_hook(const char *newval)
{
- pset.quiet = ParseVariableBool(newval, "QUIET");
+ return generic_boolean_hook(newval, "QUIET", &pset.quiet);
}
-static void
+static bool
singleline_hook(const char *newval)
{
- pset.singleline = ParseVariableBool(newval, "SINGLELINE");
+ return generic_boolean_hook(newval, "SINGLELINE", &pset.singleline);
}
-static void
+static bool
singlestep_hook(const char *newval)
{
- pset.singlestep = ParseVariableBool(newval, "SINGLESTEP");
+ return generic_boolean_hook(newval, "SINGLESTEP", &pset.singlestep);
}
-static void
+static bool
fetch_count_hook(const char *newval)
{
- pset.fetch_count = ParseVariableNum(newval, -1, -1, false);
+ bool valid;
+ int value = ParseCheckVariableNum(newval, -1, &valid);
+ if (valid)
+ pset.fetch_count = value;
+ else
+ {
+ psql_error("invalid value \"%s\" for \"%s\"\n",
+ newval, "FETCH_COUNT");
+ return false;
+ }
+ return true;
}
-static void
+static bool
echo_hook(const char *newval)
{
if (newval == NULL)
@@ -837,39 +862,59 @@ echo_hook(const char *newval)
pset.echo = PSQL_ECHO_NONE;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "ECHO", "none");
- pset.echo = PSQL_ECHO_NONE;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "ECHO");
+ return false;
}
+ return true;
}
-static void
+static bool
echo_hidden_hook(const char *newval)
{
if (newval == NULL)
pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
else if (pg_strcasecmp(newval, "noexec") == 0)
pset.echo_hidden = PSQL_ECHO_HIDDEN_NOEXEC;
- else if (ParseVariableBool(newval, "ECHO_HIDDEN"))
- pset.echo_hidden = PSQL_ECHO_HIDDEN_ON;
- else /* ParseVariableBool printed msg if needed */
- pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
+ else
+ {
+ bool isvalid;
+ bool val = ParseVariableBool(newval, NULL, &isvalid);
+ if (!isvalid)
+ {
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "ECHO_HIDDEN");
+ return false;
+ }
+ pset.echo_hidden = val ? PSQL_ECHO_HIDDEN_ON : PSQL_ECHO_HIDDEN_OFF;
+ }
+ return true;
}
-static void
+static bool
on_error_rollback_hook(const char *newval)
{
if (newval == NULL)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
else if (pg_strcasecmp(newval, "interactive") == 0)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_INTERACTIVE;
- else if (ParseVariableBool(newval, "ON_ERROR_ROLLBACK"))
- pset.on_error_rollback = PSQL_ERROR_ROLLBACK_ON;
- else /* ParseVariableBool printed msg if needed */
- pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
+ else
+ {
+ bool isvalid;
+ bool val = ParseVariableBool(newval, NULL, &isvalid);
+ if (isvalid)
+ pset.on_error_rollback = val ? PSQL_ERROR_ROLLBACK_ON : PSQL_ERROR_ROLLBACK_OFF;
+ else
+ {
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "ON_ERROR_ROLLBACK");
+ return false;
+ }
+ }
+ return true;
}
-static void
+static bool
comp_keyword_case_hook(const char *newval)
{
if (newval == NULL)
@@ -884,13 +929,14 @@ comp_keyword_case_hook(const char *newval)
pset.comp_case = PSQL_COMP_CASE_LOWER;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "COMP_KEYWORD_CASE", "preserve-upper");
- pset.comp_case = PSQL_COMP_CASE_PRESERVE_UPPER;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "COMP_KEYWORD_CASE");
+ return false;
}
+ return true;
}
-static void
+static bool
histcontrol_hook(const char *newval)
{
if (newval == NULL)
@@ -905,31 +951,35 @@ histcontrol_hook(const char *newval)
pset.histcontrol = hctl_none;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "HISTCONTROL", "none");
- pset.histcontrol = hctl_none;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "HISTCONTROL");
+ return false;
}
+ return true;
}
-static void
+static bool
prompt1_hook(const char *newval)
{
pset.prompt1 = newval ? newval : "";
+ return true;
}
-static void
+static bool
prompt2_hook(const char *newval)
{
pset.prompt2 = newval ? newval : "";
+ return true;
}
-static void
+static bool
prompt3_hook(const char *newval)
{
pset.prompt3 = newval ? newval : "";
+ return true;
}
-static void
+static bool
verbosity_hook(const char *newval)
{
if (newval == NULL)
@@ -942,16 +992,17 @@ verbosity_hook(const char *newval)
pset.verbosity = PQERRORS_VERBOSE;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "VERBOSITY", "default");
- pset.verbosity = PQERRORS_DEFAULT;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "VERBOSITY");
+ return false;
}
if (pset.db)
PQsetErrorVerbosity(pset.db, pset.verbosity);
+ return true;
}
-static void
+static bool
show_context_hook(const char *newval)
{
if (newval == NULL)
@@ -964,13 +1015,14 @@ show_context_hook(const char *newval)
pset.show_context = PQSHOW_CONTEXT_ALWAYS;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "SHOW_CONTEXT", "errors");
- pset.show_context = PQSHOW_CONTEXT_ERRORS;
+ psql_error("unrecognized value \"%s\" for \"%s\"\n",
+ newval, "SHOW_CONTEXT");
+ return false;
}
if (pset.db)
PQsetErrorContextVisibility(pset.db, pset.show_context);
+ return true;
}
diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c
index f43f418..453ef46 100644
--- a/src/bin/psql/variables.c
+++ b/src/bin/psql/variables.c
@@ -86,24 +86,27 @@ GetVariable(VariableSpace space, const char *name)
*
* "name" is the name of the variable we're assigning to, to use in error
* report if any. Pass name == NULL to suppress the error report.
+ *
+ * "*valid" reports whether "value" was syntactically valid
*/
bool
-ParseVariableBool(const char *value, const char *name)
+ParseVariableBool(const char *value, const char *name, bool *valid)
{
size_t len;
+ *valid = true;
if (value == NULL)
return false; /* not set -> assume "off" */
len = strlen(value);
- if (pg_strncasecmp(value, "true", len) == 0)
+ if (len > 0 && pg_strncasecmp(value, "true", len) == 0)
return true;
- else if (pg_strncasecmp(value, "false", len) == 0)
+ else if (len > 0 && pg_strncasecmp(value, "false", len) == 0)
return false;
- else if (pg_strncasecmp(value, "yes", len) == 0)
+ else if (len > 0 && pg_strncasecmp(value, "yes", len) == 0)
return true;
- else if (pg_strncasecmp(value, "no", len) == 0)
+ else if (len > 0 && pg_strncasecmp(value, "no", len) == 0)
return false;
/* 'o' is not unique enough */
else if (pg_strncasecmp(value, "on", (len > 2 ? len : 2)) == 0)
@@ -116,10 +119,15 @@ ParseVariableBool(const char *value, const char *name)
return false;
else
{
- /* NULL is treated as false, so a non-matching value is 'true' */
+ /*
+ * NULL is treated as false, so a non-matching value is 'true'.
+ * A caller that cares about syntactic conformance should
+ * check *valid to know whether the value was recognized.
+ */
if (name)
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- value, name, "on");
+ psql_error("unrecognized value \"%s\" for \"%s\": boolean expected\n",
+ value, name);
+ *valid = false;
return true;
}
}
@@ -154,6 +162,35 @@ ParseVariableNum(const char *val,
return result;
}
+/*
+ * Read numeric variable, or defaultval if it is not set.
+ * Trailing contents are not allowed.
+ * "valid" points to a bool reporting whether the value was valid.
+ */
+int
+ParseCheckVariableNum(const char *val,
+ int defaultval,
+ bool *valid)
+{
+ int result;
+
+ if (!val)
+ {
+ *valid = true;
+ result = defaultval;
+ }
+ else
+ {
+ char *end;
+
+ errno = 0;
+ result = strtol(val, &end, 0);
+ *valid = (val[0] != '\0' && errno == 0 && *end == '\0');
+ }
+
+ return result;
+}
+
int
GetVariableNum(VariableSpace space,
const char *name,
@@ -205,13 +242,26 @@ SetVariable(VariableSpace space, const char *name, const char *value)
{
if (strcmp(current->name, name) == 0)
{
- /* found entry, so update */
- if (current->value)
- free(current->value);
- current->value = pg_strdup(value);
- if (current->assign_hook)
- (*current->assign_hook) (current->value);
- return true;
+ /*
+ * Found entry, so update, unless a hook returns false.
+ * The hook needs the value in a buffer with the same lifespan
+ * as the variable, so allocate it right away, even if it needs
+ * to be freed just after when the hook returns false.
+ */
+ char *new_value = pg_strdup(value);
+
+ bool confirmed = current->assign_hook ?
+ (*current->assign_hook) (new_value) : true;
+
+ if (confirmed)
+ {
+ pg_free(current->value);
+ current->value = new_value;
+ }
+ else
+ pg_free(new_value); /* and current->value is left unchanged */
+
+ return confirmed;
}
}
@@ -248,8 +298,7 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
{
/* found entry, so update */
current->assign_hook = hook;
- (*hook) (current->value);
- return true;
+ return (*hook) (current->value);
}
}
@@ -260,8 +309,7 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
current->assign_hook = hook;
current->next = NULL;
previous->next = current;
- (*hook) (NULL);
- return true;
+ return (*hook) (NULL);
}
bool
diff --git a/src/bin/psql/variables.h b/src/bin/psql/variables.h
index d7a05a1..38396ba 100644
--- a/src/bin/psql/variables.h
+++ b/src/bin/psql/variables.h
@@ -20,7 +20,7 @@
* Note: if value == NULL then the variable is logically unset, but we are
* keeping the struct around so as not to forget about its hook function.
*/
-typedef void (*VariableAssignHook) (const char *newval);
+typedef bool (*VariableAssignHook) (const char *newval);
struct _variable
{
@@ -35,11 +35,14 @@ typedef struct _variable *VariableSpace;
VariableSpace CreateVariableSpace(void);
const char *GetVariable(VariableSpace space, const char *name);
-bool ParseVariableBool(const char *value, const char *name);
+bool ParseVariableBool(const char *value, const char *name, bool *valid);
int ParseVariableNum(const char *val,
int defaultval,
int faultval,
bool allowtrail);
+int ParseCheckVariableNum(const char *val,
+ int defaultval,
+ bool *valid);
int GetVariableNum(VariableSpace space,
const char *name,
int defaultval,
"Daniel Verite" <daniel@manitou-mail.org> writes:
[ psql-var-hooks-v6.patch ]
I took a quick look through this. It seems to be going in generally
the right direction, but here's a couple of thoughts:
* It seems like you're making life hard for yourself, and confusing for
readers, by having opposite API conventions at different levels. You've
got ParseVariableBool returning the parsed value as function result with
validity flag going into an output parameter, but the boolean variable
hooks do it the other way.
I'm inclined to think that the best choice is for the function result
to be the ok/not ok flag, and pass the variable-to-be-modified as an
output parameter. That fits better with the notion that the variable
is not to be modified on failure. You're having to change every caller
of ParseVariableBool anyway, so changing them a bit more doesn't seem
like a problem. I think actually you don't need generic_boolean_hook()
at all if you do that; it appears to do nothing except fix this impedance
mismatch.
Also, why aren't you using ParseVariableBool's existing ability to report
errors? It seems like this:
else if (value)
! {
! bool valid;
! bool newval = ParseVariableBool(value, NULL, &valid);
! if (valid)
! popt->topt.expanded = newval;
! else
! {
! psql_error("unrecognized value \"%s\" for \"%s\"\n",
! value, param);
! return false;
! }
! }
is really the hard way and you could have just done
- popt->topt.expanded = ParseVariableBool(value, param);
+ success = ParseVariableBool(value, param, &popt->topt.expanded);
I'd do it the same way for ParseCheckVariableNum. Also, is there a reason
why that's adding new code rather than changing ParseVariableNum?
I think if we're going to have a policy that bool variables must be valid
bools, there's no reason not to insist similarly for integers.
* More attention is needed to comments. Most glaringly, you've changed
the API for VariableAssignHook without any attention to the API spec
above that typedef. (That comment block is a bit confused anyway, since
half of it is an overall explanation of what the module does and the
other half is an API spec for the hooks. I think I'd move the overall
explanation into the file header comment.) Also, I don't like this way
of explaining an output parameter:
+ * "valid" points to a bool reporting whether the value was valid.
because it's not really clear that the function is setting that value
rather than expecting it to be set to something by the caller.
Assuming you take my advice in the previous point, you could document
ParseVariableBool and ParseVariableNum along the lines of
* Returns true if string contents represent a valid value, false if not.
* If the string is valid, the value is stored into *value, else *value
* remains unchanged.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Tom Lane wrote:
"Daniel Verite" <daniel@manitou-mail.org> writes:
[ psql-var-hooks-v6.patch ]
I took a quick look through this. It seems to be going in generally
the right direction, but here's a couple of thoughts:
Thanks for looking into this!
I'm inclined to think that the best choice is for the function result
to be the ok/not ok flag, and pass the variable-to-be-modified as an
output parameter. That fits better with the notion that the variable
is not to be modified on failure.
Agreed, if never clobbering the variable, having the valid/invalid state
returned by ParseVariableBool() allows for simpler code. I'm changing it
that way.
Also, why aren't you using ParseVariableBool's existing ability to report
errors?
Its' because there are two cases:
- either only a boolean can be given to the command or variable,
in which we let ParseVariableBool() tell:
unrecognized value "bogus" for "command": boolean expected
- or other parameters besides boolean are acceptable, in which case we
can't say "boolean expected", because in fact a boolean is no more or
less expected than the other valid values.
We could shave code by reducing ParseVariableBool()'s error message to:
unrecognized value "bogus" for "name"
clearing the distinction between [only booleans are expected]
and [booleans or enum are expected].
Then almost all callers that have their own message could rely
on ParseVariableBool() instead, as they did previously.
Do we care about the "boolean expected" part of the error message?
else if (value)
! {
! bool valid;
! bool newval = ParseVariableBool(value, NULL, &valid);
! if (valid)
! popt->topt.expanded = newval;
! else
! {
! psql_error("unrecognized value \"%s\" for \"%s\"\n",
! value, param);
! return false;
! }
! }
is really the hard way and you could have just done- popt->topt.expanded = ParseVariableBool(value, param); + success = ParseVariableBool(value, param, &popt->topt.expanded);
I get the idea, except that in this example, the compiler is
unhappy because popt->topt.expanded is not a bool, and an
explicit cast here would be kludgy.
For the error printing part, it would go away with the above
suggestion
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
"Daniel Verite" <daniel@manitou-mail.org> writes:
Tom Lane wrote:
Also, why aren't you using ParseVariableBool's existing ability to report
errors?
Its' because there are two cases:
- either only a boolean can be given to the command or variable,
in which we let ParseVariableBool() tell:
unrecognized value "bogus" for "command": boolean expected
- or other parameters besides boolean are acceptable, in which case we
can't say "boolean expected", because in fact a boolean is no more or
less expected than the other valid values.
Ah. Maybe it's time for a ParseVariableEnum, or some other way of
dealing with those cases in a more unified fashion.
Do we care about the "boolean expected" part of the error message?
I'm not especially in love with that particular wording, but I'm doubtful
that we should give up all indication of what valid values are, especially
in the cases where there are more than just bool-equivalent values.
I think the right thing to do here is to fix it so that the input routine
has full information about all the valid values. On the backend side,
we've gone to considerable lengths to make sure that error messages are
helpful for such cases, eg:
regression=# set backslash_quote to foo;
ERROR: invalid value for parameter "backslash_quote": "foo"
HINT: Available values: safe_encoding, on, off.
and I think it may be worth working equally hard here.
I get the idea, except that in this example, the compiler is
unhappy because popt->topt.expanded is not a bool, and an
explicit cast here would be kludgy.
Yeah, you'll need an intermediate variable if you're trying to use
ParseVariableBool for such a case.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hi,
I had a quick look into this patch and it seems to me like it takes
care of all the built-in variables except ENCODING type. I think we
need to apply such rule for ENCODING variable too.
postgres=# \echo :ENCODING
UTF8
postgres=# \set ENCODING xyz
postgres=# \echo :ENCODING
xyz
I think currently we are not even showing what are the different valid
encoding names to the end users like we show it for other built-in
variables
VERBOSITY, ECHO etc. I mean if i run '\set VERBOSITY' followed by tab
command it does show me the valid values for VERBOSITY but not for
ENCODING.
postgres=# \set VERBOSITY
default terse verbose
Moreover, I feel we are just passing the error message to end users
for any bogus assignments but not the hint message showing the correct
set of values that are accepted.
postgres=# \set ECHO on
unrecognized value "on" for "ECHO"
\set: error while setting variable
Above error message should also have some expected values with it.
Please note that I haven't gone through the entire mail chain so not
sure if above thoughts have already been raised by others. Sorry about
that.
With Regards,
Ashutosh Sharma
EnterpriseDB: http://www.enterprisedb.com
On Mon, Jan 16, 2017 at 10:12 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
"Daniel Verite" <daniel@manitou-mail.org> writes:
Tom Lane wrote:
Also, why aren't you using ParseVariableBool's existing ability to report
errors?Its' because there are two cases:
- either only a boolean can be given to the command or variable,
in which we let ParseVariableBool() tell:
unrecognized value "bogus" for "command": boolean expected
- or other parameters besides boolean are acceptable, in which case we
can't say "boolean expected", because in fact a boolean is no more or
less expected than the other valid values.Ah. Maybe it's time for a ParseVariableEnum, or some other way of
dealing with those cases in a more unified fashion.Do we care about the "boolean expected" part of the error message?
I'm not especially in love with that particular wording, but I'm doubtful
that we should give up all indication of what valid values are, especially
in the cases where there are more than just bool-equivalent values.
I think the right thing to do here is to fix it so that the input routine
has full information about all the valid values. On the backend side,
we've gone to considerable lengths to make sure that error messages are
helpful for such cases, eg:regression=# set backslash_quote to foo;
ERROR: invalid value for parameter "backslash_quote": "foo"
HINT: Available values: safe_encoding, on, off.and I think it may be worth working equally hard here.
I get the idea, except that in this example, the compiler is
unhappy because popt->topt.expanded is not a bool, and an
explicit cast here would be kludgy.Yeah, you'll need an intermediate variable if you're trying to use
ParseVariableBool for such a case.regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Ashutosh Sharma wrote:
postgres=# \echo :ENCODING
UTF8
postgres=# \set ENCODING xyz
postgres=# \echo :ENCODING
xyzI think currently we are not even showing what are the different valid
encoding names to the end users like we show it for other built-in
variables
VERBOSITY, ECHO etc. I mean if i run '\set VERBOSITY' followed by tab
command it does show me the valid values for VERBOSITY but not for
ENCODING.
Setting ENCODING has no effect, like DBNAME, USER, HOST and PORT.
In a way, it's a read-only variable that's here to inform the user,
not as a means to change the encoding (\encoding does that and
has proper support for tab completion)
What we could do as of this patch is emit an error when we try
to change ENCODING, with a hook returning false and
a proper error message hinting to \encoding.
I'm working on adding such messages to other variables.
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
"Daniel Verite" <daniel@manitou-mail.org> writes:
Setting ENCODING has no effect, like DBNAME, USER, HOST and PORT.
In a way, it's a read-only variable that's here to inform the user,
not as a means to change the encoding (\encoding does that and
has proper support for tab completion)
Right.
What we could do as of this patch is emit an error when we try
to change ENCODING, with a hook returning false and
a proper error message hinting to \encoding.
I think that the current behavior is intentional: it avoids making
those variables reserved. That is, if you're unaware that psql
sets them and try to use them for your own purposes, it will work.
However, it only really works if psql never overwrites the values
after startup, whereas I believe all of these are overwritten by
a \c command.
So maybe it's more user-friendly to make these variables fully
reserved, even at the risk of breaking existing scripts. But
I don't think it's exactly an open-and-shut question.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Tom Lane wrote:
However, it only really works if psql never overwrites the values
after startup, whereas I believe all of these are overwritten by
a \c command.
Yes, there are reset to reflect the properties of the new connection.
So maybe it's more user-friendly to make these variables fully
reserved, even at the risk of breaking existing scripts. But
I don't think it's exactly an open-and-shut question.
You mean if we make that fail: \set ENCODING UTF8
it's going to make that fail too:
SELECT something AS "ENCODING"[,...] \gset
and I agree it's not obvious that this trade-off has to be
made. Personally I'm fine with the status quo and will
not add that hook into the patch unless pressed to.
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Tom Lane wrote:
I took a quick look through this. It seems to be going in generally
the right direction, but here's a couple of thoughts:
Here's an update with these changes:
per Tom's suggestions upthread:
- change ParseVariableBool() signature to return validity as bool.
- remove ParseCheckVariableNum() in favor of using tightened up
ParseVariableNum() and GetVariableNum().
- updated header comments in variables.h
other changes:
- autocommit_hook rejects transitions from OFF to ON when inside a
transaction, per suggestion of Rahila Syed (which was the original
motivation for the set of changes of this patch).
- slight doc update for HISTCONTROL (values outside of enum not longer
allowed)
- add enum-style suggestions on invalid input for \pset x, \pset pager,
and \set of ECHO, ECHO_HIDDEN, ON_ERROR_ROLLBACK, COMP_KEYWORD_CASE,
HISTCONTROL, VERBOSITY, SHOW_CONTEXT, \x, \pager
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
Attachments:
psql-var-hooks-v7.patchtext/plainDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 9915731..042d277 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -3213,9 +3213,8 @@ bar
list. If set to a value of <literal>ignoredups</literal>, lines
matching the previous history line are not entered. A value of
<literal>ignoreboth</literal> combines the two options. If
- unset, or if set to <literal>none</literal> (or any other value
- than those above), all lines read in interactive mode are
- saved on the history list.
+ unset, or if set to <literal>none</literal>, all lines read
+ in interactive mode are saved on the history list.
</para>
<note>
<para>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 4139b77..091a138 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -253,26 +253,31 @@ exec_command(const char *cmd,
opt1 = read_connect_arg(scan_state);
if (opt1 != NULL && strncmp(opt1, prefix, sizeof(prefix) - 1) == 0)
{
- reuse_previous =
- ParseVariableBool(opt1 + sizeof(prefix) - 1, prefix) ?
- TRI_YES : TRI_NO;
-
- free(opt1);
- opt1 = read_connect_arg(scan_state);
+ bool on_off;
+ success = ParseVariableBool(opt1 + sizeof(prefix) - 1, prefix, &on_off);
+ if (success)
+ {
+ reuse_previous = on_off ? TRI_YES : TRI_NO;
+ free(opt1);
+ opt1 = read_connect_arg(scan_state);
+ }
}
else
reuse_previous = TRI_DEFAULT;
- opt2 = read_connect_arg(scan_state);
- opt3 = read_connect_arg(scan_state);
- opt4 = read_connect_arg(scan_state);
+ if (success) /* do not attempt to connect if reuse_previous argument was invalid */
+ {
+ opt2 = read_connect_arg(scan_state);
+ opt3 = read_connect_arg(scan_state);
+ opt4 = read_connect_arg(scan_state);
- success = do_connect(reuse_previous, opt1, opt2, opt3, opt4);
+ success = do_connect(reuse_previous, opt1, opt2, opt3, opt4);
+ free(opt2);
+ free(opt3);
+ free(opt4);
+ }
free(opt1);
- free(opt2);
- free(opt3);
- free(opt4);
}
/* \cd */
@@ -1548,7 +1553,7 @@ exec_command(const char *cmd,
OT_NORMAL, NULL, false);
if (opt)
- pset.timing = ParseVariableBool(opt, "\\timing");
+ success = ParseVariableBool(opt, "\\timing", &pset.timing);
else
pset.timing = !pset.timing;
if (!pset.quiet)
@@ -2660,7 +2665,16 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
if (value && pg_strcasecmp(value, "auto") == 0)
popt->topt.expanded = 2;
else if (value)
- popt->topt.expanded = ParseVariableBool(value, param);
+ {
+ bool on_off;
+ if (ParseVariableBool(value, NULL, &on_off))
+ popt->topt.expanded = on_off ? 1 : 0;
+ else
+ {
+ PsqlVarEnumError(param, value, "on, off, auto");
+ return false;
+ }
+ }
else
popt->topt.expanded = !popt->topt.expanded;
}
@@ -2669,7 +2683,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "numericlocale") == 0)
{
if (value)
- popt->topt.numericLocale = ParseVariableBool(value, param);
+ return ParseVariableBool(value, param, &popt->topt.numericLocale);
else
popt->topt.numericLocale = !popt->topt.numericLocale;
}
@@ -2724,7 +2738,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "t") == 0 || strcmp(param, "tuples_only") == 0)
{
if (value)
- popt->topt.tuples_only = ParseVariableBool(value, param);
+ return ParseVariableBool(value, param, &popt->topt.tuples_only);
else
popt->topt.tuples_only = !popt->topt.tuples_only;
}
@@ -2756,10 +2770,13 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
popt->topt.pager = 2;
else if (value)
{
- if (ParseVariableBool(value, param))
- popt->topt.pager = 1;
- else
- popt->topt.pager = 0;
+ bool on_off;
+ if (!ParseVariableBool(value, NULL, &on_off))
+ {
+ PsqlVarEnumError(param, value, "on, off, always");
+ return false;
+ }
+ popt->topt.pager = on_off ? 1 : 0;
}
else if (popt->topt.pager == 1)
popt->topt.pager = 0;
@@ -2778,7 +2795,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
else if (strcmp(param, "footer") == 0)
{
if (value)
- popt->topt.default_footer = ParseVariableBool(value, param);
+ return ParseVariableBool(value, param, &popt->topt.default_footer);
else
popt->topt.default_footer = !popt->topt.default_footer;
}
diff --git a/src/bin/psql/input.c b/src/bin/psql/input.c
index 972bea4..3e3e97a 100644
--- a/src/bin/psql/input.c
+++ b/src/bin/psql/input.c
@@ -541,7 +541,7 @@ finishInput(void)
{
int hist_size;
- hist_size = GetVariableNum(pset.vars, "HISTSIZE", 500, -1, true);
+ hist_size = GetVariableNum(pset.vars, "HISTSIZE", 500, -1);
(void) saveHistory(psql_history, hist_size);
free(psql_history);
psql_history = NULL;
diff --git a/src/bin/psql/mainloop.c b/src/bin/psql/mainloop.c
index bb306a4..dc25b4b 100644
--- a/src/bin/psql/mainloop.c
+++ b/src/bin/psql/mainloop.c
@@ -162,7 +162,7 @@ MainLoop(FILE *source)
/* This tries to mimic bash's IGNOREEOF feature. */
count_eof++;
- if (count_eof < GetVariableNum(pset.vars, "IGNOREEOF", 0, 10, false))
+ if (count_eof < GetVariableNum(pset.vars, "IGNOREEOF", 0, 10))
{
if (!pset.quiet)
printf(_("Use \"\\q\" to leave %s.\n"), pset.progname);
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 85aac4a..0b4f43e 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -786,43 +786,71 @@ showVersion(void)
* This isn't an amazingly good place for them, but neither is anywhere else.
*/
-static void
+
+static bool
autocommit_hook(const char *newval)
{
- pset.autocommit = ParseVariableBool(newval, "AUTOCOMMIT");
+ bool autocommit;
+ if (ParseVariableBool(newval, "AUTOCOMMIT", &autocommit))
+ {
+ /*
+ * Switching AUTOCOMMIT from OFF to ON is not allowed when inside a
+ * transaction, because it cannot be effective until the current
+ * transaction is ended.
+ */
+ if (autocommit && !pset.autocommit &&
+ pset.db && PQtransactionStatus(pset.db) == PQTRANS_INTRANS)
+ {
+ psql_error("cannot set AUTOCOMMIT to %s when inside a transaction\n", newval);
+ }
+ else
+ {
+ pset.autocommit = autocommit;
+ return true;
+ }
+ }
+ return false;
}
-static void
+static bool
on_error_stop_hook(const char *newval)
{
- pset.on_error_stop = ParseVariableBool(newval, "ON_ERROR_STOP");
+ return ParseVariableBool(newval, "ON_ERROR_STOP", &pset.on_error_stop);
}
-static void
+static bool
quiet_hook(const char *newval)
{
- pset.quiet = ParseVariableBool(newval, "QUIET");
+ return ParseVariableBool(newval, "QUIET", &pset.quiet);
}
-static void
+static bool
singleline_hook(const char *newval)
{
- pset.singleline = ParseVariableBool(newval, "SINGLELINE");
+ return ParseVariableBool(newval, "SINGLELINE", &pset.singleline);
}
-static void
+static bool
singlestep_hook(const char *newval)
{
- pset.singlestep = ParseVariableBool(newval, "SINGLESTEP");
+ return ParseVariableBool(newval, "SINGLESTEP", &pset.singlestep);
}
-static void
+static bool
fetch_count_hook(const char *newval)
{
- pset.fetch_count = ParseVariableNum(newval, -1, -1, false);
+ if (newval == NULL)
+ pset.fetch_count = -1; /* default value */
+ else if (!ParseVariableNum(newval, &pset.fetch_count))
+ {
+ psql_error("Invalid value \"%s\" for \"%s\"\nAn integer is expected\n",
+ newval, "FETCH_COUNT");
+ return false;
+ }
+ return true;
}
-static void
+static bool
echo_hook(const char *newval)
{
if (newval == NULL)
@@ -837,39 +865,55 @@ echo_hook(const char *newval)
pset.echo = PSQL_ECHO_NONE;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "ECHO", "none");
- pset.echo = PSQL_ECHO_NONE;
+ PsqlVarEnumError("ECHO", newval, "none, errors, queries, all");
+ return false;
}
+ return true;
}
-static void
+static bool
echo_hidden_hook(const char *newval)
{
if (newval == NULL)
pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
else if (pg_strcasecmp(newval, "noexec") == 0)
pset.echo_hidden = PSQL_ECHO_HIDDEN_NOEXEC;
- else if (ParseVariableBool(newval, "ECHO_HIDDEN"))
- pset.echo_hidden = PSQL_ECHO_HIDDEN_ON;
- else /* ParseVariableBool printed msg if needed */
- pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
+ else
+ {
+ bool on_off;
+ if (ParseVariableBool(newval, NULL, &on_off))
+ pset.echo_hidden = on_off ? PSQL_ECHO_HIDDEN_ON : PSQL_ECHO_HIDDEN_OFF;
+ else
+ {
+ PsqlVarEnumError("ECHO_HIDDEN", newval, "on, off, noexec");
+ return false;
+ }
+ }
+ return true;
}
-static void
+static bool
on_error_rollback_hook(const char *newval)
{
if (newval == NULL)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
else if (pg_strcasecmp(newval, "interactive") == 0)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_INTERACTIVE;
- else if (ParseVariableBool(newval, "ON_ERROR_ROLLBACK"))
- pset.on_error_rollback = PSQL_ERROR_ROLLBACK_ON;
- else /* ParseVariableBool printed msg if needed */
- pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
+ else
+ {
+ bool on_off;
+ if (ParseVariableBool(newval, NULL, &on_off))
+ pset.on_error_rollback = on_off ? PSQL_ERROR_ROLLBACK_ON : PSQL_ERROR_ROLLBACK_OFF;
+ else
+ {
+ PsqlVarEnumError("ON_ERROR_ROLLBACK", newval, "on, off, interactive");
+ return false;
+ }
+ }
+ return true;
}
-static void
+static bool
comp_keyword_case_hook(const char *newval)
{
if (newval == NULL)
@@ -884,13 +928,14 @@ comp_keyword_case_hook(const char *newval)
pset.comp_case = PSQL_COMP_CASE_LOWER;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "COMP_KEYWORD_CASE", "preserve-upper");
- pset.comp_case = PSQL_COMP_CASE_PRESERVE_UPPER;
+ PsqlVarEnumError("COMP_KEYWORD_CASE", newval,
+ "lower, upper, preserve-lower, preserve-upper");
+ return false;
}
+ return true;
}
-static void
+static bool
histcontrol_hook(const char *newval)
{
if (newval == NULL)
@@ -905,31 +950,35 @@ histcontrol_hook(const char *newval)
pset.histcontrol = hctl_none;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "HISTCONTROL", "none");
- pset.histcontrol = hctl_none;
+ PsqlVarEnumError("HISTCONTROL", newval,
+ "none, ignorespace, ignoredups, ignoreboth");
+ return false;
}
+ return true;
}
-static void
+static bool
prompt1_hook(const char *newval)
{
pset.prompt1 = newval ? newval : "";
+ return true;
}
-static void
+static bool
prompt2_hook(const char *newval)
{
pset.prompt2 = newval ? newval : "";
+ return true;
}
-static void
+static bool
prompt3_hook(const char *newval)
{
pset.prompt3 = newval ? newval : "";
+ return true;
}
-static void
+static bool
verbosity_hook(const char *newval)
{
if (newval == NULL)
@@ -942,16 +991,16 @@ verbosity_hook(const char *newval)
pset.verbosity = PQERRORS_VERBOSE;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "VERBOSITY", "default");
- pset.verbosity = PQERRORS_DEFAULT;
+ PsqlVarEnumError("VERBOSITY", newval, "default, terse, verbose");
+ return false;
}
if (pset.db)
PQsetErrorVerbosity(pset.db, pset.verbosity);
+ return true;
}
-static void
+static bool
show_context_hook(const char *newval)
{
if (newval == NULL)
@@ -964,13 +1013,13 @@ show_context_hook(const char *newval)
pset.show_context = PQSHOW_CONTEXT_ALWAYS;
else
{
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- newval, "SHOW_CONTEXT", "errors");
- pset.show_context = PQSHOW_CONTEXT_ERRORS;
+ PsqlVarEnumError("SHOW_CONTEXT", newval, "never, errors, always");
+ return false;
}
if (pset.db)
PQsetErrorContextVisibility(pset.db, pset.show_context);
+ return true;
}
diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c
index 2669572..d0de198 100644
--- a/src/bin/psql/variables.c
+++ b/src/bin/psql/variables.c
@@ -79,92 +79,103 @@ GetVariable(VariableSpace space, const char *name)
}
/*
- * Try to interpret "value" as boolean value.
+ * Try to interpret "value" as boolean value, and if successful,
+ * put it in *result. Otherwise don't clobber *result.
*
* Valid values are: true, false, yes, no, on, off, 1, 0; as well as unique
* prefixes thereof.
*
* "name" is the name of the variable we're assigning to, to use in error
* report if any. Pass name == NULL to suppress the error report.
+ *
+ * Return true when "value" is syntactically valid, false otherwise.
*/
bool
-ParseVariableBool(const char *value, const char *name)
+ParseVariableBool(const char *value, const char *name, bool *result)
{
size_t len;
+ bool valid = true;
if (value == NULL)
- return false; /* not set -> assume "off" */
+ {
+ *result = false; /* not set -> assume "off" */
+ return valid;
+ }
len = strlen(value);
- if (pg_strncasecmp(value, "true", len) == 0)
- return true;
- else if (pg_strncasecmp(value, "false", len) == 0)
- return false;
- else if (pg_strncasecmp(value, "yes", len) == 0)
- return true;
- else if (pg_strncasecmp(value, "no", len) == 0)
- return false;
+ if (len > 0 && pg_strncasecmp(value, "true", len) == 0)
+ *result = true;
+ else if (len > 0 && pg_strncasecmp(value, "false", len) == 0)
+ *result = false;
+ else if (len > 0 && pg_strncasecmp(value, "yes", len) == 0)
+ *result = true;
+ else if (len > 0 && pg_strncasecmp(value, "no", len) == 0)
+ *result = false;
/* 'o' is not unique enough */
else if (pg_strncasecmp(value, "on", (len > 2 ? len : 2)) == 0)
- return true;
+ *result = true;
else if (pg_strncasecmp(value, "off", (len > 2 ? len : 2)) == 0)
- return false;
+ *result = false;
else if (pg_strcasecmp(value, "1") == 0)
- return true;
+ *result = true;
else if (pg_strcasecmp(value, "0") == 0)
- return false;
+ *result = false;
else
{
- /* NULL is treated as false, so a non-matching value is 'true' */
+ /*
+ * The string is not recognized. We don't clobber *result in this case.
+ */
if (name)
- psql_error("unrecognized value \"%s\" for \"%s\"; assuming \"%s\"\n",
- value, name, "on");
- return true;
+ psql_error("unrecognized value \"%s\" for \"%s\": boolean expected\n",
+ value, name);
+ valid = false;
}
+ return valid;
}
/*
- * Read numeric variable, or defaultval if it is not set, or faultval if its
- * value is not a valid numeric string. If allowtrail is false, this will
- * include the case where there are trailing characters after the number.
+ * Parse a numeric variable from its string representation.
+ * If the syntax is valid, return true and set *result to the value.
+ * Otherwise return false and leave *result unchanged.
*/
-int
-ParseVariableNum(const char *val,
- int defaultval,
- int faultval,
- bool allowtrail)
+bool
+ParseVariableNum(const char *val, int *result)
{
- int result;
+ char *end;
+ int numval;
- if (!val)
- result = defaultval;
- else if (!val[0])
- result = faultval;
- else
+ if (!val || !val[0])
+ return false;
+ errno = 0;
+ numval = strtol(val, &end, 0);
+ if (errno == 0 && *end == '\0')
{
- char *end;
-
- result = strtol(val, &end, 0);
- if (!allowtrail && *end)
- result = faultval;
+ *result = numval;
+ return true;
}
-
- return result;
+ return false;
}
+
int
GetVariableNum(VariableSpace space,
const char *name,
int defaultval,
- int faultval,
- bool allowtrail)
+ int faultval)
{
const char *val;
+ int result;
val = GetVariable(space, name);
- return ParseVariableNum(val, defaultval, faultval, allowtrail);
+ if (!val)
+ return defaultval;
+
+ if (ParseVariableNum(val, &result))
+ return result;
+ else
+ return faultval;
}
void
@@ -205,13 +216,26 @@ SetVariable(VariableSpace space, const char *name, const char *value)
{
if (strcmp(current->name, name) == 0)
{
- /* found entry, so update */
- if (current->value)
- free(current->value);
- current->value = pg_strdup(value);
- if (current->assign_hook)
- (*current->assign_hook) (current->value);
- return true;
+ /*
+ * Found entry, so update, unless a hook returns false.
+ * The hook needs the value in a buffer with the same lifespan
+ * as the variable, so allocate it right away, even if it needs
+ * to be freed just after when the hook returns false.
+ */
+ char *new_value = pg_strdup(value);
+
+ bool confirmed = current->assign_hook ?
+ (*current->assign_hook) (new_value) : true;
+
+ if (confirmed)
+ {
+ pg_free(current->value);
+ current->value = new_value;
+ }
+ else
+ pg_free(new_value); /* and current->value is left unchanged */
+
+ return confirmed;
}
}
@@ -248,8 +272,7 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
{
/* found entry, so update */
current->assign_hook = hook;
- (*hook) (current->value);
- return true;
+ return (*hook) (current->value);
}
}
@@ -260,8 +283,7 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
current->assign_hook = hook;
current->next = NULL;
previous->next = current;
- (*hook) (NULL);
- return true;
+ return (*hook) (NULL);
}
bool
@@ -303,3 +325,14 @@ DeleteVariable(VariableSpace space, const char *name)
return true;
}
+
+/*
+ * Emit error with suggestions for variables or commands
+ * accepting enum-style arguments.
+ */
+void
+PsqlVarEnumError(const char* name, const char* value, const char* suggestions)
+{
+ psql_error("Unrecognized value \"%s\" for \"%s\"\nAvailable values: %s\n",
+ value, name, suggestions);
+}
diff --git a/src/bin/psql/variables.h b/src/bin/psql/variables.h
index d235b17..c7d99d1 100644
--- a/src/bin/psql/variables.h
+++ b/src/bin/psql/variables.h
@@ -12,15 +12,19 @@
* This implements a sort of variable repository. One could also think of it
* as a cheap version of an associative array. In each one of these
* datastructures you can store name/value pairs. There can also be an
- * "assign hook" function that is called whenever the variable's value is
- * changed.
+ * "assign hook" function that is called before the variable's value is
+ * changed, and that returns a boolean indicating whether the assignment
+ * is confirmed or must be cancelled.
*
- * An "unset" operation causes the hook to be called with newval == NULL.
+ * The hook is called with newval == NULL when a variable is created
+ * along with it by SetVariableAssignHook().
+ * An "unset" operation also causes the hook to be called with newval == NULL,
+ * and is not cancellable even if the hook returns false.
*
* Note: if value == NULL then the variable is logically unset, but we are
* keeping the struct around so as not to forget about its hook function.
*/
-typedef void (*VariableAssignHook) (const char *newval);
+typedef bool (*VariableAssignHook) (const char *newval);
struct _variable
{
@@ -35,16 +39,15 @@ typedef struct _variable *VariableSpace;
VariableSpace CreateVariableSpace(void);
const char *GetVariable(VariableSpace space, const char *name);
-bool ParseVariableBool(const char *value, const char *name);
-int ParseVariableNum(const char *val,
- int defaultval,
- int faultval,
- bool allowtrail);
+bool ParseVariableBool(const char *value, const char *name, bool *result);
+
+bool ParseVariableNum(const char *val, int* result);
+
int GetVariableNum(VariableSpace space,
const char *name,
int defaultval,
- int faultval,
- bool allowtrail);
+ int faultval);
+
void PrintVariables(VariableSpace space);
@@ -53,4 +56,6 @@ bool SetVariableAssignHook(VariableSpace space, const char *name, VariableAssig
bool SetVariableBool(VariableSpace space, const char *name);
bool DeleteVariable(VariableSpace space, const char *name);
+void PsqlVarEnumError(const char* name, const char* value, const char* suggestions);
+
#endif /* VARIABLES_H */
I just wrote:
- add enum-style suggestions on invalid input for \pset x, \pset pager,
and \set of ECHO, ECHO_HIDDEN, ON_ERROR_ROLLBACK, COMP_KEYWORD_CASE,
HISTCONTROL, VERBOSITY, SHOW_CONTEXT, \x, \pager
There's no such thing as \pager, I meant to write:
\pset x, \pset pager,
and \set of ECHO, ECHO_HIDDEN, ON_ERROR_ROLLBACK, COMP_KEYWORD_CASE,
HISTCONTROL, VERBOSITY, SHOW_CONTEXT
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
"Daniel Verite" <daniel@manitou-mail.org> writes:
Here's an update with these changes:
I scanned this patch very quickly and think it addresses my previous
stylistic objections. Rahila is the reviewer of record though, so
I'll wait for his comments before going further.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Hello,
The patch works fine on applying on latest master branch and testing it for
various variables as listed in PsqlSettings struct.
I will post further comments on patch soon.
Thank you,
Rahila Syed
On Wed, Jan 25, 2017 at 1:33 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Show quoted text
"Daniel Verite" <daniel@manitou-mail.org> writes:
Here's an update with these changes:
I scanned this patch very quickly and think it addresses my previous
stylistic objections. Rahila is the reviewer of record though, so
I'll wait for his comments before going further.regards, tom lane
Hello,
Please consider following comments on the patch.
In function ParseVariableNum,
if (!val || !val[0])
return false;
Check for 'val == NULL' as in above condition is done even in callers of
ParseVariableNum().
There should be only one such check.
+ psql_error("Invalid value \"%s\" for \"%s\"\nAn integer is
expected\n",
Cant we have this error in ParseVariableNum() similar to
ParseVariableBool() ?
+ /* + * Switching AUTOCOMMIT from OFF to ON is not allowed when inside a + * transaction, because it cannot be effective until the current + * transaction is ended. + */ + if (autocommit && !pset.autocommit && + pset.db && PQtransactionStatus(pset.db) == PQTRANS_INTRANS) + { + psql_error("cannot set AUTOCOMMIT to %s when inside a
transaction\n", newval);
+ }
The above change in autocommit behaviour needs to be documented.
Thank you,
Rahila Syed
On Wed, Jan 25, 2017 at 5:48 PM, Rahila Syed <rahilasyed90@gmail.com> wrote:
Show quoted text
Hello,
The patch works fine on applying on latest master branch and testing it
for various variables as listed in PsqlSettings struct.
I will post further comments on patch soon.Thank you,
Rahila SyedOn Wed, Jan 25, 2017 at 1:33 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
"Daniel Verite" <daniel@manitou-mail.org> writes:
Here's an update with these changes:
I scanned this patch very quickly and think it addresses my previous
stylistic objections. Rahila is the reviewer of record though, so
I'll wait for his comments before going further.regards, tom lane
Rahila Syed <rahilasyed90@gmail.com> writes:
Please consider following comments on the patch.
In function ParseVariableNum,
if (!val || !val[0])
return false;Check for 'val == NULL' as in above condition is done even in callers of
ParseVariableNum().
There should be only one such check.
Meh ... I don't think it's unreasonable for ParseVariableNum to defend
itself against that. The callers might or might not be applying a check
--- for them, it would be a matter of do they need to detect presence
or absence of an option, but not about whether the option value is valid.
+ psql_error("Invalid value \"%s\" for \"%s\"\nAn integer is
expected\n",
Cant we have this error in ParseVariableNum() similar to
ParseVariableBool() ?
Agreed, error messages should be stylistically similar. I'm not sure they
can be exactly alike though. Right now the patch has ParseVariableBool
saying
+ psql_error("unrecognized value \"%s\" for \"%s\": boolean expected\n",
+ value, name);
while callers that don't use that error use PsqlVarEnumError which has
+ psql_error("Unrecognized value \"%s\" for \"%s\"\nAvailable values: %s\n",
+ value, name, suggestions);
and then ParseVariableNum is as above. So first off we've got a
capitalization inconsistency. Project style for backend messages is
no initial cap; psql seems not to be on board with that entirely,
but I'm definitely inclined to go with it here. As for "invalid"
vs. "unrecognized", I'm not sure "unrecognized" really fits the
bill for "this isn't an integer". So I'm inclined to leave that
alone. I suggest we go with these:
"invalid value \"%s\" for \"%s\": integer expected\n"
"unrecognized value \"%s\" for \"%s\": boolean expected\n"
"unrecognized value \"%s\" for \"%s\"\nAvailable values are: %s\n."
where the last case is following the message style for hints.
+ * Switching AUTOCOMMIT from OFF to ON is not allowed when inside a + * transaction, because it cannot be effective until the current + * transaction is ended.
The above change in autocommit behaviour needs to be documented.
Yeah, definitely.
I'll go ahead and push the patch with these fixes. Thanks for reviewing!
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
BTW, while testing this patch I noticed that the error reports are
a tad redundant:
regression=# \set AUTOCOMMIT foo
unrecognized value "foo" for "AUTOCOMMIT": boolean expected
\set: error while setting variable
regression=#
The "error while setting variable" message seems entirely content-free.
I think we should drop that and instead establish a rule that SetVariable
should print a message for itself about any failure. (There are a lot
of call sites that don't check for or print a message, but that's only
because they aren't expecting failure. If one were to happen, printing
a message doesn't seem unreasonable.) That would in turn propagate into
an API requirement that var hooks that return FALSE are responsible for
printing a message about the reason, which is why it would be appropriate
to make that change as part of this patch.
Barring objections PDQ, I'll make this change.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I wrote:
Rahila Syed <rahilasyed90@gmail.com> writes:
+ * Switching AUTOCOMMIT from OFF to ON is not allowed when inside a + * transaction, because it cannot be effective until the current + * transaction is ended.
The above change in autocommit behaviour needs to be documented.
Yeah, definitely.
Actually ... while trying to write some documentation for that, I found
myself wondering why we need such a prohibition at all. If you are inside
a transaction, then autocommit has no effect until after you get out of
the transaction, and the documentation about it seems clear enough on the
point. Also, if you want to argue that allowing it to change intra-
transaction is too confusing, why would we only forbid this direction
of change and not both directions?
I'm afraid we might be breaking some peoples' scripts to no particularly
good end, so I'm going to leave this out of the committed patch. If you
think this really is a valid change to make, we can commit it separately,
but let's discuss it on its own merits.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
So I pushed this, and the buildfarm members that are testing RedisFDW
immediately fell over:
*** /home/andrew/bf/root/HEAD/redis_fdw.build/test/expected/redis_fdw.out 2017-01-30 18:20:27.440677318 -0500
--- /home/andrew/bf/root/HEAD/redis_fdw.build/test/results/redis_fdw.out 2017-01-30 18:32:33.404677320 -0500
***************
*** 26,31 ****
--- 26,32 ----
options (database '15', tabletype 'zset');
-- make sure they are all empty - if any are not stop the script right now
\set ON_ERROR_STOP
+ unrecognized value "" for "ON_ERROR_STOP": boolean expected
do $$
declare
rows bigint;
======================================================================
Evidently, this test script is relying on the preceding behavior that
setting a bool variable to an empty string was equivalent to setting
it to "true". If it's just that script I would be okay with saying
"well, it's a bug in that script" ... but I'm a bit worried that this
may be the tip of the iceberg, ie maybe a lot of people have done
things like this. Should we reconsider the decision to reject empty
strings in ParseVariableBool?
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Tom Lane wrote:
Also, if you want to argue that allowing it to change intra-
transaction is too confusing, why would we only forbid this direction
of change and not both directions?
The thread "Surprising behaviour of \set AUTOCOMMIT ON" at:
/messages/by-id/CAH2L28sTP-9dio3X1AaZRyWb0-ANAx6BDBi37TGmvw1hBiu0oA@mail.gmail.com
seemed to converge towards the conclusion implemented in the autocommit_hook
proposed in the patch.
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Tom Lane wrote:
Evidently, this test script is relying on the preceding behavior that
setting a bool variable to an empty string was equivalent to setting
it to "true". If it's just that script I would be okay with saying
"well, it's a bug in that script" ... but I'm a bit worried that this
may be the tip of the iceberg, ie maybe a lot of people have done
things like this. Should we reconsider the decision to reject empty
strings in ParseVariableBool?
Sigh. It was considered upthread, I'm requoting the relevant bit:
<quote>
if (pg_strncasecmp(value, "true", len) == 0)
return true;
It happens that "" as a value yields the same result than "true" for this
test so it passes, but it seems rather unintentional.
The resulting problem from the POV of the user is that we can do that,
for instance:
test=> \set AUTOCOMMIT
without error message or feedback, and that leaves us without much
clue about autocommit being enabled:
test=> \echo :AUTOCOMMIT
test=>
So I've changed ParseVariableBool() in v4 to reject empty string as an
invalid boolean (but not NULL since the startup logic requires NULL
meaning false in the early initialization of these variables).
"make check" seems OK with that, I hope it doesn't cause any regression
elsewhere.
</quote>
So it does cause regressions. I don't know if we should reaccept
empty strings immediately to fix that, but in the long run, I think
that the above situation with the empty :AUTOCOMMIT is not really
sustainable: when we extend what we do with variables
(/if /endif and so on), it will become even more of a problem.
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
"Daniel Verite" <daniel@manitou-mail.org> writes:
Tom Lane wrote:
Evidently, this test script is relying on the preceding behavior that
setting a bool variable to an empty string was equivalent to setting
it to "true". If it's just that script I would be okay with saying
"well, it's a bug in that script" ... but I'm a bit worried that this
may be the tip of the iceberg, ie maybe a lot of people have done
things like this. Should we reconsider the decision to reject empty
strings in ParseVariableBool?...
So it does cause regressions. I don't know if we should reaccept
empty strings immediately to fix that, but in the long run, I think
that the above situation with the empty :AUTOCOMMIT is not really
sustainable: when we extend what we do with variables
(/if /endif and so on), it will become even more of a problem.
Yeah, in a green field we'd certainly not allow this, but the problem
I'm having is that in all previous versions you could do, eg,
\set ON_ERROR_STOP
... stuff expecting ON_ERROR_STOP to be on
\unset ON_ERROR_STOP
... stuff expecting ON_ERROR_STOP to be off
and it works, and looks perfectly natural, and people may well be relying
on that. Especially since the docs aren't very clear that this shouldn't
work --- psql-ref.sgml repeatedly uses phrases like "FOO is set" to
indicate that the boolean variable FOO is considered to be "on".
Moreover, the committed patch is inconsistent in that it forbids
only one of the above. Why is it okay to treat unset as "off",
but not okay to treat the default empty-string value as "on"?
Maybe it's worth breaking backwards compatibility on this point, but
I'm feeling unconvinced. This seems rather different from rejecting
clearly-wrongly-spelled values.
One possible compromise that would address your concern about display
is to modify the hook API some more so that variable hooks could actually
substitute new values. Then for example the bool-variable hooks could
effectively replace "\set AUTOCOMMIT" by "\set AUTOCOMMIT on" and
"\unset AUTOCOMMIT" by "\set AUTOCOMMIT off", ensuring that interrogation
of their values always produces sane results.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Tom Lane wrote:
Moreover, the committed patch is inconsistent in that it forbids
only one of the above. Why is it okay to treat unset as "off",
but not okay to treat the default empty-string value as "on"?
Treating unset (NULL in the value) as "off" comes from the fact
that except AUTOCOMMIT, our built-in variables are not initialized
with a default value. For instance we call this in EstablishVariableSpace();
SetVariableAssignHook(pset.vars, "ON_ERROR_STOP", on_error_stop_hook);
then on_error_stop_hook is called with NULL as the value
then ParseVariableBool(NULL, "ON_ERROR_STOP", &pset.on_error_stop)
is what initializes pset.on_error_stop to false.
The same happens if/when the variable is unset. Again the hook is called
with NULL, and it sets back the pset.* variable in a hardcoded default state,
which is false for all booleans.
Incidentally I want to suggest to change that, so that all variables
should be initialized with a non-NULL value right from the start, and the
value would possibly come to NULL only if it's unset.
This would allow the hook to distinguish between initialization and
unsetting, which in turn will allow it to deny the \unset in the
cases when it doesn't make any sense conceptually (like AUTOCOMMIT).
Forcing values for our built-in variables would also avoid the following:
=# \echo :ON_ERROR_STOP
:ON_ERROR_STOP
Even if the variable ON_ERROR_STOP does exist in the VariableSpace
and has a hook and there's an initialized corresponding pset.on_error_stop,
from the user's viewpoint, it's as if the variable doesn't exist
until they intialize it explicitly.
I suggest that it doesn't make much sense and it would be better
to have that instead right from the start:
=# \echo :ON_ERROR_STOP
off
One possible compromise that would address your concern about display
is to modify the hook API some more so that variable hooks could actually
substitute new values. Then for example the bool-variable hooks could
effectively replace "\set AUTOCOMMIT" by "\set AUTOCOMMIT on" and
"\unset AUTOCOMMIT" by "\set AUTOCOMMIT off", ensuring that interrogation
of their values always produces sane results.
Agreed, that looks like a good compromise.
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
"Daniel Verite" <daniel@manitou-mail.org> writes:
Tom Lane wrote:
One possible compromise that would address your concern about display
is to modify the hook API some more so that variable hooks could actually
substitute new values. Then for example the bool-variable hooks could
effectively replace "\set AUTOCOMMIT" by "\set AUTOCOMMIT on" and
"\unset AUTOCOMMIT" by "\set AUTOCOMMIT off", ensuring that interrogation
of their values always produces sane results.
Agreed, that looks like a good compromise.
Attached is a draft patch for that. I chose to make a second hook rather
than complicate the assign hook API, mainly because it allows more code
sharing --- all the bool vars can share the same substitute hook, and
so can the three-way vars as long as "on" and "off" are the appropriate
substitutes.
I only touched the behavior for bool vars here, but if people like this
solution it could be fleshed out to apply to all the built-in variables.
Maybe we should merge SetVariableSubstituteHook and SetVariableAssignHook
into one function?
regards, tom lane
Attachments:
improve-psql-bool-var-behavior-1.patchtext/x-diff; charset=us-ascii; name=improve-psql-bool-var-behavior-1.patchDownload
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 0574b5b..985cfcb 100644
*** a/src/bin/psql/startup.c
--- b/src/bin/psql/startup.c
*************** showVersion(void)
*** 777,787 ****
/*
! * Assign hooks for psql variables.
*
* This isn't an amazingly good place for them, but neither is anywhere else.
*/
static bool
autocommit_hook(const char *newval)
{
--- 777,804 ----
/*
! * Substitute hooks and assign hooks for psql variables.
*
* This isn't an amazingly good place for them, but neither is anywhere else.
*/
+ static char *
+ bool_substitute_hook(char *newval)
+ {
+ if (newval == NULL)
+ {
+ /* "\unset FOO" becomes "\set FOO off" */
+ newval = pg_strdup("off");
+ }
+ else if (newval[0] == '\0')
+ {
+ /* "\set FOO" becomes "\set FOO on" */
+ pg_free(newval);
+ newval = pg_strdup("on");
+ }
+ return newval;
+ }
+
static bool
autocommit_hook(const char *newval)
{
*************** EstablishVariableSpace(void)
*** 1002,1015 ****
--- 1019,1039 ----
{
pset.vars = CreateVariableSpace();
+ SetVariableSubstituteHook(pset.vars, "AUTOCOMMIT", bool_substitute_hook);
SetVariableAssignHook(pset.vars, "AUTOCOMMIT", autocommit_hook);
+ SetVariableSubstituteHook(pset.vars, "ON_ERROR_STOP", bool_substitute_hook);
SetVariableAssignHook(pset.vars, "ON_ERROR_STOP", on_error_stop_hook);
+ SetVariableSubstituteHook(pset.vars, "QUIET", bool_substitute_hook);
SetVariableAssignHook(pset.vars, "QUIET", quiet_hook);
+ SetVariableSubstituteHook(pset.vars, "SINGLELINE", bool_substitute_hook);
SetVariableAssignHook(pset.vars, "SINGLELINE", singleline_hook);
+ SetVariableSubstituteHook(pset.vars, "SINGLESTEP", bool_substitute_hook);
SetVariableAssignHook(pset.vars, "SINGLESTEP", singlestep_hook);
SetVariableAssignHook(pset.vars, "FETCH_COUNT", fetch_count_hook);
SetVariableAssignHook(pset.vars, "ECHO", echo_hook);
+ SetVariableSubstituteHook(pset.vars, "ECHO_HIDDEN", bool_substitute_hook);
SetVariableAssignHook(pset.vars, "ECHO_HIDDEN", echo_hidden_hook);
+ SetVariableSubstituteHook(pset.vars, "ON_ERROR_ROLLBACK", bool_substitute_hook);
SetVariableAssignHook(pset.vars, "ON_ERROR_ROLLBACK", on_error_rollback_hook);
SetVariableAssignHook(pset.vars, "COMP_KEYWORD_CASE", comp_keyword_case_hook);
SetVariableAssignHook(pset.vars, "HISTCONTROL", histcontrol_hook);
diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c
index 91e4ae8..61c4ccc 100644
*** a/src/bin/psql/variables.c
--- b/src/bin/psql/variables.c
*************** CreateVariableSpace(void)
*** 52,57 ****
--- 52,58 ----
ptr = pg_malloc(sizeof *ptr);
ptr->name = NULL;
ptr->value = NULL;
+ ptr->substitute_hook = NULL;
ptr->assign_hook = NULL;
ptr->next = NULL;
*************** ParseVariableBool(const char *value, con
*** 101,111 ****
size_t len;
bool valid = true;
if (value == NULL)
! {
! *result = false; /* not set -> assume "off" */
! return valid;
! }
len = strlen(value);
--- 102,110 ----
size_t len;
bool valid = true;
+ /* Treat "unset" as an empty string, which will lead to error below */
if (value == NULL)
! value = "";
len = strlen(value);
*************** ParseVariableNum(const char *value, cons
*** 152,159 ****
char *end;
long numval;
if (value == NULL)
! return false;
errno = 0;
numval = strtol(value, &end, 0);
if (errno == 0 && *end == '\0' && end != value && numval == (int) numval)
--- 151,160 ----
char *end;
long numval;
+ /* Treat "unset" as an empty string, which will lead to error below */
if (value == NULL)
! value = "";
!
errno = 0;
numval = strtol(value, &end, 0);
if (errno == 0 && *end == '\0' && end != value && numval == (int) numval)
*************** SetVariable(VariableSpace space, const c
*** 235,247 ****
if (!valid_variable_name(name))
{
psql_error("invalid variable name: \"%s\"\n", name);
return false;
}
- if (!value)
- return DeleteVariable(space, name);
-
for (previous = space, current = space->next;
current;
previous = current, current = current->next)
--- 236,248 ----
if (!valid_variable_name(name))
{
+ /* Deletion of non-existent variable is not an error */
+ if (!value)
+ return true;
psql_error("invalid variable name: \"%s\"\n", name);
return false;
}
for (previous = space, current = space->next;
current;
previous = current, current = current->next)
*************** SetVariable(VariableSpace space, const c
*** 249,262 ****
if (strcmp(current->name, name) == 0)
{
/*
! * Found entry, so update, unless hook returns false. The hook
! * may need the passed value to have the same lifespan as the
! * variable, so allocate it right away, even though we'll have to
! * free it again if the hook returns false.
*/
! char *new_value = pg_strdup(value);
bool confirmed;
if (current->assign_hook)
confirmed = (*current->assign_hook) (new_value);
else
--- 250,269 ----
if (strcmp(current->name, name) == 0)
{
/*
! * Found entry, so update, unless assign hook returns false.
! *
! * We must duplicate the passed value to start with. This
! * simplifies the API for substitute hooks. Moreover, some assign
! * hooks assume that the passed value has the same lifespan as the
! * variable. Having to free the string again on failure is a
! * small price to pay for keeping these APIs simple.
*/
! char *new_value = value ? pg_strdup(value) : NULL;
bool confirmed;
+ if (current->substitute_hook)
+ new_value = (*current->substitute_hook) (new_value);
+
if (current->assign_hook)
confirmed = (*current->assign_hook) (new_value);
else
*************** SetVariable(VariableSpace space, const c
*** 267,288 ****
if (current->value)
pg_free(current->value);
current->value = new_value;
}
! else
pg_free(new_value); /* current->value is left unchanged */
return confirmed;
}
}
/* not present, make new entry */
current = pg_malloc(sizeof *current);
current->name = pg_strdup(name);
! current->value = pg_strdup(value);
current->assign_hook = NULL;
current->next = NULL;
previous->next = current;
! return true;
}
/*
--- 274,358 ----
if (current->value)
pg_free(current->value);
current->value = new_value;
+
+ /*
+ * If we deleted the value, and there are no hooks to
+ * remember, we can discard the variable altogether.
+ */
+ if (new_value == NULL &&
+ current->substitute_hook == NULL &&
+ current->assign_hook == NULL)
+ {
+ previous->next = current->next;
+ free(current->name);
+ free(current);
+ }
}
! else if (new_value)
pg_free(new_value); /* current->value is left unchanged */
return confirmed;
}
}
+ /* not present, make new entry ... unless we were asked to delete */
+ if (value)
+ {
+ current = pg_malloc(sizeof *current);
+ current->name = pg_strdup(name);
+ current->value = pg_strdup(value);
+ current->substitute_hook = NULL;
+ current->assign_hook = NULL;
+ current->next = NULL;
+ previous->next = current;
+ }
+ return true;
+ }
+
+ /*
+ * Attach a substitute hook function to the named variable.
+ *
+ * If the variable doesn't already exist, create it with value NULL,
+ * just so we have a place to store the hook function. (But the hook
+ * might immediately change the NULL to something else.)
+ *
+ * The hook is immediately called on the variable's current value.
+ */
+ void
+ SetVariableSubstituteHook(VariableSpace space, const char *name,
+ VariableSubstituteHook hook)
+ {
+ struct _variable *current,
+ *previous;
+
+ if (!space || !name)
+ return;
+
+ if (!valid_variable_name(name))
+ return;
+
+ for (previous = space, current = space->next;
+ current;
+ previous = current, current = current->next)
+ {
+ if (strcmp(current->name, name) == 0)
+ {
+ /* found entry, so update */
+ current->substitute_hook = hook;
+ current->value = (*hook) (current->value);
+ return;
+ }
+ }
+
/* not present, make new entry */
current = pg_malloc(sizeof *current);
current->name = pg_strdup(name);
! current->value = NULL;
! current->substitute_hook = hook;
current->assign_hook = NULL;
current->next = NULL;
previous->next = current;
! current->value = (*hook) (current->value);
}
/*
*************** SetVariable(VariableSpace space, const c
*** 299,305 ****
* get called before any user-supplied value is assigned.
*/
void
! SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook hook)
{
struct _variable *current,
*previous;
--- 369,376 ----
* get called before any user-supplied value is assigned.
*/
void
! SetVariableAssignHook(VariableSpace space, const char *name,
! VariableAssignHook hook)
{
struct _variable *current,
*previous;
*************** SetVariableAssignHook(VariableSpace spac
*** 327,332 ****
--- 398,404 ----
current = pg_malloc(sizeof *current);
current->name = pg_strdup(name);
current->value = NULL;
+ current->substitute_hook = NULL;
current->assign_hook = hook;
current->next = NULL;
previous->next = current;
*************** SetVariableBool(VariableSpace space, con
*** 351,392 ****
bool
DeleteVariable(VariableSpace space, const char *name)
{
! struct _variable *current,
! *previous;
!
! if (!space)
! return true;
!
! for (previous = space, current = space->next;
! current;
! previous = current, current = current->next)
! {
! if (strcmp(current->name, name) == 0)
! {
! if (current->assign_hook)
! {
! /* Allow deletion only if hook is okay with NULL value */
! if (!(*current->assign_hook) (NULL))
! return false; /* message printed by hook */
! if (current->value)
! free(current->value);
! current->value = NULL;
! /* Don't delete entry, or we'd forget the hook function */
! }
! else
! {
! /* We can delete the entry as well as its value */
! if (current->value)
! free(current->value);
! previous->next = current->next;
! free(current->name);
! free(current);
! }
! return true;
! }
! }
!
! return true;
}
/*
--- 423,429 ----
bool
DeleteVariable(VariableSpace space, const char *name)
{
! return SetVariable(space, name, NULL);
}
/*
diff --git a/src/bin/psql/variables.h b/src/bin/psql/variables.h
index 274b4af..f382bfd 100644
*** a/src/bin/psql/variables.h
--- b/src/bin/psql/variables.h
***************
*** 18,45 ****
* prevent invalid values from being assigned, and can update internal C
* variables to keep them in sync with the variable's current value.
*
! * A hook function is called before any attempted assignment, with the
* proposed new value of the variable (or with NULL, if an \unset is being
* attempted). If it returns false, the assignment doesn't occur --- it
* should print an error message with psql_error() to tell the user why.
*
! * When a hook function is installed with SetVariableAssignHook(), it is
! * called with the variable's current value (or with NULL, if it wasn't set
* yet). But its return value is ignored in this case. The hook should be
* set before any possibly-invalid value can be assigned.
*/
typedef bool (*VariableAssignHook) (const char *newval);
/*
* Data structure representing one variable.
*
* Note: if value == NULL then the variable is logically unset, but we are
! * keeping the struct around so as not to forget about its hook function.
*/
struct _variable
{
char *name;
char *value;
VariableAssignHook assign_hook;
struct _variable *next;
};
--- 18,70 ----
* prevent invalid values from being assigned, and can update internal C
* variables to keep them in sync with the variable's current value.
*
! * An assign hook function is called before any attempted assignment, with the
* proposed new value of the variable (or with NULL, if an \unset is being
* attempted). If it returns false, the assignment doesn't occur --- it
* should print an error message with psql_error() to tell the user why.
*
! * When an assign hook function is installed with SetVariableAssignHook(), it
! * is called with the variable's current value (or with NULL, if it wasn't set
* yet). But its return value is ignored in this case. The hook should be
* set before any possibly-invalid value can be assigned.
*/
typedef bool (*VariableAssignHook) (const char *newval);
/*
+ * Variables can also be given "substitute hook" functions. The substitute
+ * hook can replace values (including NULL) with other values, allowing
+ * normalization of variable contents. For example, for a boolean variable,
+ * we wish to interpret "\unset FOO" as "\set FOO off", and we can do that
+ * by installing a substitute hook. (We can use the same substitute hook
+ * for all bool or nearly-bool variables, which is why this responsibility
+ * isn't part of the assign hook.)
+ *
+ * The substitute hook is called before any attempted assignment, and before
+ * the assign hook if any, passing the proposed new value of the variable as a
+ * malloc'd string (or NULL, if an \unset is being attempted). It can return
+ * the same value, or a different malloc'd string, or modify the string
+ * in-place. It should free the passed-in value if it's not returning it.
+ * The substitute hook generally should not complain about erroneous values;
+ * that's a job for the assign hook.
+ *
+ * When a substitute hook is installed with SetVariableSubstituteHook(),
+ * it is applied to the variable's current value (typically NULL, if it wasn't
+ * set yet). It's usually best to do that before installing the assign hook
+ * if there is one.
+ */
+ typedef char *(*VariableSubstituteHook) (char *newval);
+
+ /*
* Data structure representing one variable.
*
* Note: if value == NULL then the variable is logically unset, but we are
! * keeping the struct around so as not to forget about its hook function(s).
*/
struct _variable
{
char *name;
char *value;
+ VariableSubstituteHook substitute_hook;
VariableAssignHook assign_hook;
struct _variable *next;
};
*************** int GetVariableNum(VariableSpace space,
*** 65,70 ****
--- 90,96 ----
void PrintVariables(VariableSpace space);
bool SetVariable(VariableSpace space, const char *name, const char *value);
+ void SetVariableSubstituteHook(VariableSpace space, const char *name, VariableSubstituteHook hook);
void SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook hook);
bool SetVariableBool(VariableSpace space, const char *name);
bool DeleteVariable(VariableSpace space, const char *name);
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 420825a..026a4f0 100644
*** a/src/test/regress/expected/psql.out
--- b/src/test/regress/expected/psql.out
*************** invalid variable name: "invalid/name"
*** 11,16 ****
--- 11,33 ----
unrecognized value "foo" for "AUTOCOMMIT": boolean expected
\set FETCH_COUNT foo
invalid value "foo" for "FETCH_COUNT": integer expected
+ -- check handling of built-in boolean variable
+ \echo :ON_ERROR_ROLLBACK
+ off
+ \set ON_ERROR_ROLLBACK
+ \echo :ON_ERROR_ROLLBACK
+ on
+ \set ON_ERROR_ROLLBACK foo
+ unrecognized value "foo" for "ON_ERROR_ROLLBACK"
+ Available values are: on, off, interactive.
+ \echo :ON_ERROR_ROLLBACK
+ on
+ \set ON_ERROR_ROLLBACK on
+ \echo :ON_ERROR_ROLLBACK
+ on
+ \unset ON_ERROR_ROLLBACK
+ \echo :ON_ERROR_ROLLBACK
+ off
-- \gset
select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_
\echo :pref01_test01 :pref01_test02 :pref01_test03
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index 79624b9..d823d11 100644
*** a/src/test/regress/sql/psql.sql
--- b/src/test/regress/sql/psql.sql
***************
*** 10,15 ****
--- 10,25 ----
-- fail: invalid value for special variable
\set AUTOCOMMIT foo
\set FETCH_COUNT foo
+ -- check handling of built-in boolean variable
+ \echo :ON_ERROR_ROLLBACK
+ \set ON_ERROR_ROLLBACK
+ \echo :ON_ERROR_ROLLBACK
+ \set ON_ERROR_ROLLBACK foo
+ \echo :ON_ERROR_ROLLBACK
+ \set ON_ERROR_ROLLBACK on
+ \echo :ON_ERROR_ROLLBACK
+ \unset ON_ERROR_ROLLBACK
+ \echo :ON_ERROR_ROLLBACK
-- \gset
I wrote:
This would allow the hook to distinguish between initialization and
unsetting, which in turn will allow it to deny the \unset in the
cases when it doesn't make any sense conceptually (like AUTOCOMMIT).
I notice that in the commited patch, you added the ability
for DeleteVariable() to reject the deletion if the hook
disagrees.
+ {
+ /* Allow deletion only if hook is okay with
NULL value */
+ if (!(*current->assign_hook) (NULL))
+ return false; /* message
printed by hook */
But this can't happen in practice because as mentioned just upthread
the hook called with NULL doesn't know if the variable is getting unset
or initialized, so rejecting on NULL is not an option.
Attached is a proposed patch to add initial values to
SetVariableAssignHook() to solve this problem, and an example for
\unset AUTOCOMMIT being denied by the hook.
As a side effect, we see all buit-in variables when issuing \set
rather than just a few.
Does it make sense?
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
Attachments:
psql-vars-init-v1.patchtext/plainDownload
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 0574b5b..631b3f6 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -166,14 +166,6 @@ main(int argc, char *argv[])
SetVariable(pset.vars, "VERSION", PG_VERSION_STR);
- /* Default values for variables */
- SetVariableBool(pset.vars, "AUTOCOMMIT");
- SetVariable(pset.vars, "VERBOSITY", "default");
- SetVariable(pset.vars, "SHOW_CONTEXT", "errors");
- SetVariable(pset.vars, "PROMPT1", DEFAULT_PROMPT1);
- SetVariable(pset.vars, "PROMPT2", DEFAULT_PROMPT2);
- SetVariable(pset.vars, "PROMPT3", DEFAULT_PROMPT3);
-
parse_psql_options(argc, argv, &options);
/*
@@ -785,6 +777,11 @@ showVersion(void)
static bool
autocommit_hook(const char *newval)
{
+ if (newval == NULL)
+ {
+ psql_error("built-in variable %s cannot be unset.\n", "AUTOCOMMIT");
+ return false;
+ }
return ParseVariableBool(newval, "AUTOCOMMIT", &pset.autocommit);
}
@@ -1002,20 +999,20 @@ EstablishVariableSpace(void)
{
pset.vars = CreateVariableSpace();
- SetVariableAssignHook(pset.vars, "AUTOCOMMIT", autocommit_hook);
- SetVariableAssignHook(pset.vars, "ON_ERROR_STOP", on_error_stop_hook);
- SetVariableAssignHook(pset.vars, "QUIET", quiet_hook);
- SetVariableAssignHook(pset.vars, "SINGLELINE", singleline_hook);
- SetVariableAssignHook(pset.vars, "SINGLESTEP", singlestep_hook);
- SetVariableAssignHook(pset.vars, "FETCH_COUNT", fetch_count_hook);
- SetVariableAssignHook(pset.vars, "ECHO", echo_hook);
- SetVariableAssignHook(pset.vars, "ECHO_HIDDEN", echo_hidden_hook);
- SetVariableAssignHook(pset.vars, "ON_ERROR_ROLLBACK", on_error_rollback_hook);
- SetVariableAssignHook(pset.vars, "COMP_KEYWORD_CASE", comp_keyword_case_hook);
- SetVariableAssignHook(pset.vars, "HISTCONTROL", histcontrol_hook);
- SetVariableAssignHook(pset.vars, "PROMPT1", prompt1_hook);
- SetVariableAssignHook(pset.vars, "PROMPT2", prompt2_hook);
- SetVariableAssignHook(pset.vars, "PROMPT3", prompt3_hook);
- SetVariableAssignHook(pset.vars, "VERBOSITY", verbosity_hook);
- SetVariableAssignHook(pset.vars, "SHOW_CONTEXT", show_context_hook);
+ SetVariableAssignHook(pset.vars, "AUTOCOMMIT", autocommit_hook, "on");
+ SetVariableAssignHook(pset.vars, "ON_ERROR_STOP", on_error_stop_hook, "off");
+ SetVariableAssignHook(pset.vars, "QUIET", quiet_hook, "off");
+ SetVariableAssignHook(pset.vars, "SINGLELINE", singleline_hook, "off");
+ SetVariableAssignHook(pset.vars, "SINGLESTEP", singlestep_hook, "off");
+ SetVariableAssignHook(pset.vars, "FETCH_COUNT", fetch_count_hook, "-1");
+ SetVariableAssignHook(pset.vars, "ECHO", echo_hook, "none");
+ SetVariableAssignHook(pset.vars, "ECHO_HIDDEN", echo_hidden_hook, "off");
+ SetVariableAssignHook(pset.vars, "ON_ERROR_ROLLBACK", on_error_rollback_hook, "off");
+ SetVariableAssignHook(pset.vars, "COMP_KEYWORD_CASE", comp_keyword_case_hook, "preserve-upper");
+ SetVariableAssignHook(pset.vars, "HISTCONTROL", histcontrol_hook, "none");
+ SetVariableAssignHook(pset.vars, "PROMPT1", prompt1_hook, DEFAULT_PROMPT1);
+ SetVariableAssignHook(pset.vars, "PROMPT2", prompt2_hook, DEFAULT_PROMPT2);
+ SetVariableAssignHook(pset.vars, "PROMPT3", prompt3_hook, DEFAULT_PROMPT3);
+ SetVariableAssignHook(pset.vars, "VERBOSITY", verbosity_hook, "default");
+ SetVariableAssignHook(pset.vars, "SHOW_CONTEXT", show_context_hook, "errors");
}
diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c
index 91e4ae8..2e5c3e0 100644
--- a/src/bin/psql/variables.c
+++ b/src/bin/psql/variables.c
@@ -288,9 +288,8 @@ SetVariable(VariableSpace space, const char *name, const char *value)
/*
* Attach an assign hook function to the named variable.
*
- * If the variable doesn't already exist, create it with value NULL,
- * just so we have a place to store the hook function. (Externally,
- * this isn't different from it not being defined.)
+ * If the variable doesn't already exist, create it with init_value as the
+ * content. init_value must not be NULL.
*
* The hook is immediately called on the variable's current value. This is
* meant to let it update any derived psql state. If the hook doesn't like
@@ -299,7 +298,10 @@ SetVariable(VariableSpace space, const char *name, const char *value)
* get called before any user-supplied value is assigned.
*/
void
-SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook hook)
+SetVariableAssignHook(VariableSpace space,
+ const char *name,
+ VariableAssignHook hook,
+ const char *init_value)
{
struct _variable *current,
*previous;
@@ -326,11 +328,11 @@ SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook
/* not present, make new entry */
current = pg_malloc(sizeof *current);
current->name = pg_strdup(name);
- current->value = NULL;
+ current->value = pg_strdup(init_value);
current->assign_hook = hook;
current->next = NULL;
previous->next = current;
- (void) (*hook) (NULL);
+ (void) (*hook) (current->value);
}
/*
diff --git a/src/bin/psql/variables.h b/src/bin/psql/variables.h
index 274b4af..94e7fb8 100644
--- a/src/bin/psql/variables.h
+++ b/src/bin/psql/variables.h
@@ -24,9 +24,9 @@
* should print an error message with psql_error() to tell the user why.
*
* When a hook function is installed with SetVariableAssignHook(), it is
- * called with the variable's current value (or with NULL, if it wasn't set
- * yet). But its return value is ignored in this case. The hook should be
- * set before any possibly-invalid value can be assigned.
+ * called with the variable's initial value, but its return value is ignored
+ * in this case.
+ * The hook should be set before any possibly-invalid value can be assigned.
*/
typedef bool (*VariableAssignHook) (const char *newval);
@@ -65,7 +65,10 @@ int GetVariableNum(VariableSpace space,
void PrintVariables(VariableSpace space);
bool SetVariable(VariableSpace space, const char *name, const char *value);
-void SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook hook);
+void SetVariableAssignHook(VariableSpace space,
+ const char *name,
+ VariableAssignHook hook,
+ const char *init_value);
bool SetVariableBool(VariableSpace space, const char *name);
bool DeleteVariable(VariableSpace space, const char *name);
"Daniel Verite" <daniel@manitou-mail.org> writes:
I notice that in the commited patch, you added the ability
for DeleteVariable() to reject the deletion if the hook
disagrees.
Right.
But this can't happen in practice because as mentioned just upthread
the hook called with NULL doesn't know if the variable is getting unset
or initialized, so rejecting on NULL is not an option.
It would have required the caller to set a value before installing the
hook, which would require some reshuffling of responsibility. In the
draft patch I sent a little bit ago, this is handled by installing a
"substitution hook" first, and that hook transmogrifies NULL into an
allowed setting. That gets to the same place in a slightly different
way, but it also covers allowing "\unset FOO", which inserting initial
values wouldn't.
Attached is a proposed patch to add initial values to
SetVariableAssignHook() to solve this problem, and an example for
\unset AUTOCOMMIT being denied by the hook.
I think disallowing \unset is a nonstarter on compatibility grounds.
We should allow \unset but treat it like setting to "off" (or whatever
the default value is).
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I wrote:
Attached is a draft patch for that. I chose to make a second hook rather
than complicate the assign hook API, mainly because it allows more code
sharing --- all the bool vars can share the same substitute hook, and
so can the three-way vars as long as "on" and "off" are the appropriate
substitutes.
I only touched the behavior for bool vars here, but if people like this
solution it could be fleshed out to apply to all the built-in variables.
Attached is a finished version that includes hooks for all the variables
for which they were clearly sensible. (FETCH_COUNT doesn't seem to really
need one, and I didn't do anything with HISTSIZE or IGNOREEOF either.
It might be worth bringing the latter two into the hooks paradigm, but
that seems like fit material for a separate patch.)
I updated the documentation as well. I think this is committable if
there are not objections.
regards, tom lane
Attachments:
improve-psql-bool-var-behavior-2.patchtext/x-diff; charset=us-ascii; name=improve-psql-bool-var-behavior-2.patchDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 4e51e90..b9c8fcc 100644
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
*************** EOF
*** 455,462 ****
any, by an equal sign on the command line. To unset a variable,
leave off the equal sign. To set a variable with an empty value,
use the equal sign but leave off the value. These assignments are
! done during a very early stage of start-up, so variables reserved
! for internal purposes might get overwritten later.
</para>
</listitem>
</varlistentry>
--- 455,462 ----
any, by an equal sign on the command line. To unset a variable,
leave off the equal sign. To set a variable with an empty value,
use the equal sign but leave off the value. These assignments are
! done during command line processing, so variables that reflect
! connection state will get overwritten later.
</para>
</listitem>
</varlistentry>
*************** lo_import 152801
*** 2692,2698 ****
class="parameter">name</replaceable> to <replaceable
class="parameter">value</replaceable>, or if more than one value
is given, to the concatenation of all of them. If only one
! argument is given, the variable is set with an empty value. To
unset a variable, use the <command>\unset</command> command.
</para>
--- 2692,2698 ----
class="parameter">name</replaceable> to <replaceable
class="parameter">value</replaceable>, or if more than one value
is given, to the concatenation of all of them. If only one
! argument is given, the variable is set to an empty-string value. To
unset a variable, use the <command>\unset</command> command.
</para>
*************** lo_import 152801
*** 2709,2717 ****
</para>
<para>
! Although you are welcome to set any variable to anything you
! want, <application>psql</application> treats several variables
! as special. They are documented in the section about variables.
</para>
<note>
--- 2709,2719 ----
</para>
<para>
! Certain variables are special, in that they
! control <application>psql</application>'s behavior or are
! automatically set to reflect connection state. These variables are
! documented in <xref linkend="APP-PSQL-variables"
! endterm="APP-PSQL-variables-title">, below.
</para>
<note>
*************** testdb=> <userinput>\setenv LESS -imx
*** 2835,2840 ****
--- 2837,2850 ----
Unsets (deletes) the <application>psql</> variable <replaceable
class="parameter">name</replaceable>.
</para>
+
+ <para>
+ Most variables that control <application>psql</application>'s behavior
+ cannot be unset; instead, an <literal>\unset</> command is interpreted
+ as setting them to their default values.
+ See <xref linkend="APP-PSQL-variables"
+ endterm="APP-PSQL-variables-title">, below.
+ </para>
</listitem>
</varlistentry>
*************** bar
*** 3053,3059 ****
<para>
If you call <command>\set</command> without a second argument, the
! variable is set, with an empty string as value. To unset (i.e., delete)
a variable, use the command <command>\unset</command>. To show the
values of all variables, call <command>\set</command> without any argument.
</para>
--- 3063,3069 ----
<para>
If you call <command>\set</command> without a second argument, the
! variable is set to an empty-string value. To unset (i.e., delete)
a variable, use the command <command>\unset</command>. To show the
values of all variables, call <command>\set</command> without any argument.
</para>
*************** bar
*** 3082,3089 ****
By convention, all specially treated variables' names
consist of all upper-case ASCII letters (and possibly digits and
underscores). To ensure maximum compatibility in the future, avoid
! using such variable names for your own purposes. A list of all specially
! treated variables follows.
</para>
<variablelist>
--- 3092,3114 ----
By convention, all specially treated variables' names
consist of all upper-case ASCII letters (and possibly digits and
underscores). To ensure maximum compatibility in the future, avoid
! using such variable names for your own purposes.
! </para>
!
! <para>
! Variables that control <application>psql</application>'s behavior
! generally cannot be unset or set to invalid values. An <literal>\unset</>
! command is allowed but is interpreted as setting the variable to its
! default value. A <literal>\set</> command without a second argument is
! interpreted as setting the variable to <literal>on</>, for control
! variables that accept that value, and is rejected for others. Also,
! control variables that accept the values <literal>on</>
! and <literal>off</> will also accept other common spellings of Boolean
! values, such as <literal>true</> and <literal>false</>.
! </para>
!
! <para>
! The specially treated variables are:
</para>
<variablelist>
*************** bar
*** 3153,3159 ****
<para>
The name of the database you are currently connected to. This is
set every time you connect to a database (including program
! start-up), but can be unset.
</para>
</listitem>
</varlistentry>
--- 3178,3184 ----
<para>
The name of the database you are currently connected to. This is
set every time you connect to a database (including program
! start-up), but can be changed or unset.
</para>
</listitem>
</varlistentry>
*************** bar
*** 3171,3178 ****
as it is sent to the server. The switch to select this behavior is
<option>-e</option>. If set to <literal>errors</literal>, then only
failed queries are displayed on standard error output. The switch
! for this behavior is <option>-b</option>. If unset, or if set to
! <literal>none</literal>, then no queries are displayed.
</para>
</listitem>
</varlistentry>
--- 3196,3203 ----
as it is sent to the server. The switch to select this behavior is
<option>-e</option>. If set to <literal>errors</literal>, then only
failed queries are displayed on standard error output. The switch
! for this behavior is <option>-b</option>. If set to
! <literal>none</literal> (the default), then no queries are displayed.
</para>
</listitem>
</varlistentry>
*************** bar
*** 3187,3194 ****
<productname>PostgreSQL</productname> internals and provide
similar functionality in your own programs. (To select this behavior
on program start-up, use the switch <option>-E</option>.) If you set
! the variable to the value <literal>noexec</literal>, the queries are
just shown but are not actually sent to the server and executed.
</para>
</listitem>
</varlistentry>
--- 3212,3220 ----
<productname>PostgreSQL</productname> internals and provide
similar functionality in your own programs. (To select this behavior
on program start-up, use the switch <option>-E</option>.) If you set
! this variable to the value <literal>noexec</literal>, the queries are
just shown but are not actually sent to the server and executed.
+ The default value is <literal>off</>.
</para>
</listitem>
</varlistentry>
*************** bar
*** 3200,3206 ****
The current client character set encoding.
This is set every time you connect to a database (including
program start-up), and when you change the encoding
! with <literal>\encoding</>, but it can be unset.
</para>
</listitem>
</varlistentry>
--- 3226,3232 ----
The current client character set encoding.
This is set every time you connect to a database (including
program start-up), and when you change the encoding
! with <literal>\encoding</>, but it can be changed or unset.
</para>
</listitem>
</varlistentry>
*************** bar
*** 3209,3215 ****
<term><varname>FETCH_COUNT</varname></term>
<listitem>
<para>
! If this variable is set to an integer value > 0,
the results of <command>SELECT</command> queries are fetched
and displayed in groups of that many rows, rather than the
default behavior of collecting the entire result set before
--- 3235,3241 ----
<term><varname>FETCH_COUNT</varname></term>
<listitem>
<para>
! If this variable is set to an integer value greater than zero,
the results of <command>SELECT</command> queries are fetched
and displayed in groups of that many rows, rather than the
default behavior of collecting the entire result set before
*************** bar
*** 3220,3225 ****
--- 3246,3258 ----
Keep in mind that when using this feature, a query might
fail after having already displayed some rows.
</para>
+
+ <para>
+ <varname>FETCH_COUNT</varname> is ignored if it is unset or does not
+ have a positive value. It cannot be set to a value that is not
+ syntactically an integer.
+ </para>
+
<tip>
<para>
Although you can use any output format with this feature,
*************** bar
*** 3241,3247 ****
list. If set to a value of <literal>ignoredups</literal>, lines
matching the previous history line are not entered. A value of
<literal>ignoreboth</literal> combines the two options. If
! unset, or if set to <literal>none</literal> (the default), all lines
read in interactive mode are saved on the history list.
</para>
<note>
--- 3274,3280 ----
list. If set to a value of <literal>ignoredups</literal>, lines
matching the previous history line are not entered. A value of
<literal>ignoreboth</literal> combines the two options. If
! set to <literal>none</literal> (the default), all lines
read in interactive mode are saved on the history list.
</para>
<note>
*************** bar
*** 3257,3264 ****
<term><varname>HISTFILE</varname></term>
<listitem>
<para>
! The file name that will be used to store the history list. The default
! value is <filename>~/.psql_history</filename>. For example, putting:
<programlisting>
\set HISTFILE ~/.psql_history- :DBNAME
</programlisting>
--- 3290,3301 ----
<term><varname>HISTFILE</varname></term>
<listitem>
<para>
! The file name that will be used to store the history list. If unset,
! the file name is taken from the <envar>PSQL_HISTORY</envar>
! environment variable. If that is not set either, the default
! is <filename>~/.psql_history</filename>,
! or <filename>%APPDATA%\postgresql\psql_history</filename> on Windows.
! For example, putting:
<programlisting>
\set HISTFILE ~/.psql_history- :DBNAME
</programlisting>
*************** bar
*** 3279,3286 ****
<term><varname>HISTSIZE</varname></term>
<listitem>
<para>
! The number of commands to store in the command history. The
! default value is 500.
</para>
<note>
<para>
--- 3316,3325 ----
<term><varname>HISTSIZE</varname></term>
<listitem>
<para>
! The maximum number of commands to store in the command history.
! If unset, at most 500 commands are stored by default.
! If set to a value that is negative or not an integer, no limit is
! applied.
</para>
<note>
<para>
*************** bar
*** 3297,3303 ****
<para>
The database server host you are currently connected to. This is
set every time you connect to a database (including program
! start-up), but can be unset.
</para>
</listitem>
</varlistentry>
--- 3336,3342 ----
<para>
The database server host you are currently connected to. This is
set every time you connect to a database (including program
! start-up), but can be changed or unset.
</para>
</listitem>
</varlistentry>
*************** bar
*** 3350,3356 ****
generates an error, the error is ignored and the transaction
continues. When set to <literal>interactive</>, such errors are only
ignored in interactive sessions, and not when reading script
! files. When unset or set to <literal>off</>, a statement in a
transaction block that generates an error aborts the entire
transaction. The error rollback mode works by issuing an
implicit <command>SAVEPOINT</> for you, just before each command
--- 3389,3395 ----
generates an error, the error is ignored and the transaction
continues. When set to <literal>interactive</>, such errors are only
ignored in interactive sessions, and not when reading script
! files. When set to <literal>off</> (the default), a statement in a
transaction block that generates an error aborts the entire
transaction. The error rollback mode works by issuing an
implicit <command>SAVEPOINT</> for you, just before each command
*************** bar
*** 3385,3391 ****
<para>
The database server port to which you are currently connected.
This is set every time you connect to a database (including
! program start-up), but can be unset.
</para>
</listitem>
</varlistentry>
--- 3424,3430 ----
<para>
The database server port to which you are currently connected.
This is set every time you connect to a database (including
! program start-up), but can be changed or unset.
</para>
</listitem>
</varlistentry>
*************** bar
*** 3458,3464 ****
<para>
The database user you are currently connected as. This is set
every time you connect to a database (including program
! start-up), but can be unset.
</para>
</listitem>
</varlistentry>
--- 3497,3503 ----
<para>
The database user you are currently connected as. This is set
every time you connect to a database (including program
! start-up), but can be changed or unset.
</para>
</listitem>
</varlistentry>
*************** bar
*** 3481,3487 ****
<listitem>
<para>
This variable is set at program start-up to
! reflect <application>psql</>'s version. It can be unset or changed.
</para>
</listitem>
</varlistentry>
--- 3520,3526 ----
<listitem>
<para>
This variable is set at program start-up to
! reflect <application>psql</>'s version. It can be changed or unset.
</para>
</listitem>
</varlistentry>
*************** PSQL_EDITOR_LINENUMBER_ARG='--line '
*** 4015,4020 ****
--- 4054,4060 ----
</para>
<para>
The location of the history file can be set explicitly via
+ the <varname>HISTFILE</varname> <application>psql</> variable or
the <envar>PSQL_HISTORY</envar> environment variable.
</para>
</listitem>
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index 0574b5b..a3654e6 100644
*** a/src/bin/psql/startup.c
--- b/src/bin/psql/startup.c
*************** main(int argc, char *argv[])
*** 166,175 ****
SetVariable(pset.vars, "VERSION", PG_VERSION_STR);
! /* Default values for variables */
SetVariableBool(pset.vars, "AUTOCOMMIT");
- SetVariable(pset.vars, "VERBOSITY", "default");
- SetVariable(pset.vars, "SHOW_CONTEXT", "errors");
SetVariable(pset.vars, "PROMPT1", DEFAULT_PROMPT1);
SetVariable(pset.vars, "PROMPT2", DEFAULT_PROMPT2);
SetVariable(pset.vars, "PROMPT3", DEFAULT_PROMPT3);
--- 166,173 ----
SetVariable(pset.vars, "VERSION", PG_VERSION_STR);
! /* Default values for variables (that don't match the result of \unset) */
SetVariableBool(pset.vars, "AUTOCOMMIT");
SetVariable(pset.vars, "PROMPT1", DEFAULT_PROMPT1);
SetVariable(pset.vars, "PROMPT2", DEFAULT_PROMPT2);
SetVariable(pset.vars, "PROMPT3", DEFAULT_PROMPT3);
*************** parse_psql_options(int argc, char *argv[
*** 578,594 ****
if (!equal_loc)
{
if (!DeleteVariable(pset.vars, value))
! {
! fprintf(stderr, _("%s: could not delete variable \"%s\"\n"),
! pset.progname, value);
! exit(EXIT_FAILURE);
! }
}
else
{
*equal_loc = '\0';
if (!SetVariable(pset.vars, value, equal_loc + 1))
! exit(EXIT_FAILURE);
}
free(value);
--- 576,588 ----
if (!equal_loc)
{
if (!DeleteVariable(pset.vars, value))
! exit(EXIT_FAILURE); /* error already printed */
}
else
{
*equal_loc = '\0';
if (!SetVariable(pset.vars, value, equal_loc + 1))
! exit(EXIT_FAILURE); /* error already printed */
}
free(value);
*************** showVersion(void)
*** 777,787 ****
/*
! * Assign hooks for psql variables.
*
* This isn't an amazingly good place for them, but neither is anywhere else.
*/
static bool
autocommit_hook(const char *newval)
{
--- 771,798 ----
/*
! * Substitute hooks and assign hooks for psql variables.
*
* This isn't an amazingly good place for them, but neither is anywhere else.
*/
+ static char *
+ bool_substitute_hook(char *newval)
+ {
+ if (newval == NULL)
+ {
+ /* "\unset FOO" becomes "\set FOO off" */
+ newval = pg_strdup("off");
+ }
+ else if (newval[0] == '\0')
+ {
+ /* "\set FOO" becomes "\set FOO on" */
+ pg_free(newval);
+ newval = pg_strdup("on");
+ }
+ return newval;
+ }
+
static bool
autocommit_hook(const char *newval)
{
*************** fetch_count_hook(const char *newval)
*** 822,833 ****
return true;
}
static bool
echo_hook(const char *newval)
{
! if (newval == NULL)
! pset.echo = PSQL_ECHO_NONE;
! else if (pg_strcasecmp(newval, "queries") == 0)
pset.echo = PSQL_ECHO_QUERIES;
else if (pg_strcasecmp(newval, "errors") == 0)
pset.echo = PSQL_ECHO_ERRORS;
--- 833,851 ----
return true;
}
+ static char *
+ echo_substitute_hook(char *newval)
+ {
+ if (newval == NULL)
+ newval = pg_strdup("none");
+ return newval;
+ }
+
static bool
echo_hook(const char *newval)
{
! Assert(newval != NULL); /* else substitute hook messed up */
! if (pg_strcasecmp(newval, "queries") == 0)
pset.echo = PSQL_ECHO_QUERIES;
else if (pg_strcasecmp(newval, "errors") == 0)
pset.echo = PSQL_ECHO_ERRORS;
*************** echo_hook(const char *newval)
*** 846,854 ****
static bool
echo_hidden_hook(const char *newval)
{
! if (newval == NULL)
! pset.echo_hidden = PSQL_ECHO_HIDDEN_OFF;
! else if (pg_strcasecmp(newval, "noexec") == 0)
pset.echo_hidden = PSQL_ECHO_HIDDEN_NOEXEC;
else
{
--- 864,871 ----
static bool
echo_hidden_hook(const char *newval)
{
! Assert(newval != NULL); /* else substitute hook messed up */
! if (pg_strcasecmp(newval, "noexec") == 0)
pset.echo_hidden = PSQL_ECHO_HIDDEN_NOEXEC;
else
{
*************** echo_hidden_hook(const char *newval)
*** 868,876 ****
static bool
on_error_rollback_hook(const char *newval)
{
! if (newval == NULL)
! pset.on_error_rollback = PSQL_ERROR_ROLLBACK_OFF;
! else if (pg_strcasecmp(newval, "interactive") == 0)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_INTERACTIVE;
else
{
--- 885,892 ----
static bool
on_error_rollback_hook(const char *newval)
{
! Assert(newval != NULL); /* else substitute hook messed up */
! if (pg_strcasecmp(newval, "interactive") == 0)
pset.on_error_rollback = PSQL_ERROR_ROLLBACK_INTERACTIVE;
else
{
*************** on_error_rollback_hook(const char *newva
*** 887,898 ****
return true;
}
static bool
comp_keyword_case_hook(const char *newval)
{
! if (newval == NULL)
! pset.comp_case = PSQL_COMP_CASE_PRESERVE_UPPER;
! else if (pg_strcasecmp(newval, "preserve-upper") == 0)
pset.comp_case = PSQL_COMP_CASE_PRESERVE_UPPER;
else if (pg_strcasecmp(newval, "preserve-lower") == 0)
pset.comp_case = PSQL_COMP_CASE_PRESERVE_LOWER;
--- 903,921 ----
return true;
}
+ static char *
+ comp_keyword_case_substitute_hook(char *newval)
+ {
+ if (newval == NULL)
+ newval = pg_strdup("preserve-upper");
+ return newval;
+ }
+
static bool
comp_keyword_case_hook(const char *newval)
{
! Assert(newval != NULL); /* else substitute hook messed up */
! if (pg_strcasecmp(newval, "preserve-upper") == 0)
pset.comp_case = PSQL_COMP_CASE_PRESERVE_UPPER;
else if (pg_strcasecmp(newval, "preserve-lower") == 0)
pset.comp_case = PSQL_COMP_CASE_PRESERVE_LOWER;
*************** comp_keyword_case_hook(const char *newva
*** 909,920 ****
return true;
}
static bool
histcontrol_hook(const char *newval)
{
! if (newval == NULL)
! pset.histcontrol = hctl_none;
! else if (pg_strcasecmp(newval, "ignorespace") == 0)
pset.histcontrol = hctl_ignorespace;
else if (pg_strcasecmp(newval, "ignoredups") == 0)
pset.histcontrol = hctl_ignoredups;
--- 932,950 ----
return true;
}
+ static char *
+ histcontrol_substitute_hook(char *newval)
+ {
+ if (newval == NULL)
+ newval = pg_strdup("none");
+ return newval;
+ }
+
static bool
histcontrol_hook(const char *newval)
{
! Assert(newval != NULL); /* else substitute hook messed up */
! if (pg_strcasecmp(newval, "ignorespace") == 0)
pset.histcontrol = hctl_ignorespace;
else if (pg_strcasecmp(newval, "ignoredups") == 0)
pset.histcontrol = hctl_ignoredups;
*************** prompt3_hook(const char *newval)
*** 952,963 ****
return true;
}
static bool
verbosity_hook(const char *newval)
{
! if (newval == NULL)
! pset.verbosity = PQERRORS_DEFAULT;
! else if (pg_strcasecmp(newval, "default") == 0)
pset.verbosity = PQERRORS_DEFAULT;
else if (pg_strcasecmp(newval, "terse") == 0)
pset.verbosity = PQERRORS_TERSE;
--- 982,1000 ----
return true;
}
+ static char *
+ verbosity_substitute_hook(char *newval)
+ {
+ if (newval == NULL)
+ newval = pg_strdup("default");
+ return newval;
+ }
+
static bool
verbosity_hook(const char *newval)
{
! Assert(newval != NULL); /* else substitute hook messed up */
! if (pg_strcasecmp(newval, "default") == 0)
pset.verbosity = PQERRORS_DEFAULT;
else if (pg_strcasecmp(newval, "terse") == 0)
pset.verbosity = PQERRORS_TERSE;
*************** verbosity_hook(const char *newval)
*** 974,985 ****
return true;
}
static bool
show_context_hook(const char *newval)
{
! if (newval == NULL)
! pset.show_context = PQSHOW_CONTEXT_ERRORS;
! else if (pg_strcasecmp(newval, "never") == 0)
pset.show_context = PQSHOW_CONTEXT_NEVER;
else if (pg_strcasecmp(newval, "errors") == 0)
pset.show_context = PQSHOW_CONTEXT_ERRORS;
--- 1011,1029 ----
return true;
}
+ static char *
+ show_context_substitute_hook(char *newval)
+ {
+ if (newval == NULL)
+ newval = pg_strdup("errors");
+ return newval;
+ }
+
static bool
show_context_hook(const char *newval)
{
! Assert(newval != NULL); /* else substitute hook messed up */
! if (pg_strcasecmp(newval, "never") == 0)
pset.show_context = PQSHOW_CONTEXT_NEVER;
else if (pg_strcasecmp(newval, "errors") == 0)
pset.show_context = PQSHOW_CONTEXT_ERRORS;
*************** EstablishVariableSpace(void)
*** 1002,1021 ****
{
pset.vars = CreateVariableSpace();
! SetVariableAssignHook(pset.vars, "AUTOCOMMIT", autocommit_hook);
! SetVariableAssignHook(pset.vars, "ON_ERROR_STOP", on_error_stop_hook);
! SetVariableAssignHook(pset.vars, "QUIET", quiet_hook);
! SetVariableAssignHook(pset.vars, "SINGLELINE", singleline_hook);
! SetVariableAssignHook(pset.vars, "SINGLESTEP", singlestep_hook);
! SetVariableAssignHook(pset.vars, "FETCH_COUNT", fetch_count_hook);
! SetVariableAssignHook(pset.vars, "ECHO", echo_hook);
! SetVariableAssignHook(pset.vars, "ECHO_HIDDEN", echo_hidden_hook);
! SetVariableAssignHook(pset.vars, "ON_ERROR_ROLLBACK", on_error_rollback_hook);
! SetVariableAssignHook(pset.vars, "COMP_KEYWORD_CASE", comp_keyword_case_hook);
! SetVariableAssignHook(pset.vars, "HISTCONTROL", histcontrol_hook);
! SetVariableAssignHook(pset.vars, "PROMPT1", prompt1_hook);
! SetVariableAssignHook(pset.vars, "PROMPT2", prompt2_hook);
! SetVariableAssignHook(pset.vars, "PROMPT3", prompt3_hook);
! SetVariableAssignHook(pset.vars, "VERBOSITY", verbosity_hook);
! SetVariableAssignHook(pset.vars, "SHOW_CONTEXT", show_context_hook);
}
--- 1046,1097 ----
{
pset.vars = CreateVariableSpace();
! SetVariableHooks(pset.vars, "AUTOCOMMIT",
! bool_substitute_hook,
! autocommit_hook);
! SetVariableHooks(pset.vars, "ON_ERROR_STOP",
! bool_substitute_hook,
! on_error_stop_hook);
! SetVariableHooks(pset.vars, "QUIET",
! bool_substitute_hook,
! quiet_hook);
! SetVariableHooks(pset.vars, "SINGLELINE",
! bool_substitute_hook,
! singleline_hook);
! SetVariableHooks(pset.vars, "SINGLESTEP",
! bool_substitute_hook,
! singlestep_hook);
! SetVariableHooks(pset.vars, "FETCH_COUNT",
! NULL,
! fetch_count_hook);
! SetVariableHooks(pset.vars, "ECHO",
! echo_substitute_hook,
! echo_hook);
! SetVariableHooks(pset.vars, "ECHO_HIDDEN",
! bool_substitute_hook,
! echo_hidden_hook);
! SetVariableHooks(pset.vars, "ON_ERROR_ROLLBACK",
! bool_substitute_hook,
! on_error_rollback_hook);
! SetVariableHooks(pset.vars, "COMP_KEYWORD_CASE",
! comp_keyword_case_substitute_hook,
! comp_keyword_case_hook);
! SetVariableHooks(pset.vars, "HISTCONTROL",
! histcontrol_substitute_hook,
! histcontrol_hook);
! SetVariableHooks(pset.vars, "PROMPT1",
! NULL,
! prompt1_hook);
! SetVariableHooks(pset.vars, "PROMPT2",
! NULL,
! prompt2_hook);
! SetVariableHooks(pset.vars, "PROMPT3",
! NULL,
! prompt3_hook);
! SetVariableHooks(pset.vars, "VERBOSITY",
! verbosity_substitute_hook,
! verbosity_hook);
! SetVariableHooks(pset.vars, "SHOW_CONTEXT",
! show_context_substitute_hook,
! show_context_hook);
}
diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c
index 91e4ae8..b9b8fcb 100644
*** a/src/bin/psql/variables.c
--- b/src/bin/psql/variables.c
*************** CreateVariableSpace(void)
*** 52,57 ****
--- 52,58 ----
ptr = pg_malloc(sizeof *ptr);
ptr->name = NULL;
ptr->value = NULL;
+ ptr->substitute_hook = NULL;
ptr->assign_hook = NULL;
ptr->next = NULL;
*************** ParseVariableBool(const char *value, con
*** 101,111 ****
size_t len;
bool valid = true;
if (value == NULL)
! {
! *result = false; /* not set -> assume "off" */
! return valid;
! }
len = strlen(value);
--- 102,110 ----
size_t len;
bool valid = true;
+ /* Treat "unset" as an empty string, which will lead to error below */
if (value == NULL)
! value = "";
len = strlen(value);
*************** ParseVariableNum(const char *value, cons
*** 152,159 ****
char *end;
long numval;
if (value == NULL)
! return false;
errno = 0;
numval = strtol(value, &end, 0);
if (errno == 0 && *end == '\0' && end != value && numval == (int) numval)
--- 151,160 ----
char *end;
long numval;
+ /* Treat "unset" as an empty string, which will lead to error below */
if (value == NULL)
! value = "";
!
errno = 0;
numval = strtol(value, &end, 0);
if (errno == 0 && *end == '\0' && end != value && numval == (int) numval)
*************** SetVariable(VariableSpace space, const c
*** 235,247 ****
if (!valid_variable_name(name))
{
psql_error("invalid variable name: \"%s\"\n", name);
return false;
}
- if (!value)
- return DeleteVariable(space, name);
-
for (previous = space, current = space->next;
current;
previous = current, current = current->next)
--- 236,248 ----
if (!valid_variable_name(name))
{
+ /* Deletion of non-existent variable is not an error */
+ if (!value)
+ return true;
psql_error("invalid variable name: \"%s\"\n", name);
return false;
}
for (previous = space, current = space->next;
current;
previous = current, current = current->next)
*************** SetVariable(VariableSpace space, const c
*** 249,262 ****
if (strcmp(current->name, name) == 0)
{
/*
! * Found entry, so update, unless hook returns false. The hook
! * may need the passed value to have the same lifespan as the
! * variable, so allocate it right away, even though we'll have to
! * free it again if the hook returns false.
*/
! char *new_value = pg_strdup(value);
bool confirmed;
if (current->assign_hook)
confirmed = (*current->assign_hook) (new_value);
else
--- 250,269 ----
if (strcmp(current->name, name) == 0)
{
/*
! * Found entry, so update, unless assign hook returns false.
! *
! * We must duplicate the passed value to start with. This
! * simplifies the API for substitute hooks. Moreover, some assign
! * hooks assume that the passed value has the same lifespan as the
! * variable. Having to free the string again on failure is a
! * small price to pay for keeping these APIs simple.
*/
! char *new_value = value ? pg_strdup(value) : NULL;
bool confirmed;
+ if (current->substitute_hook)
+ new_value = (*current->substitute_hook) (new_value);
+
if (current->assign_hook)
confirmed = (*current->assign_hook) (new_value);
else
*************** SetVariable(VariableSpace space, const c
*** 267,305 ****
if (current->value)
pg_free(current->value);
current->value = new_value;
}
! else
pg_free(new_value); /* current->value is left unchanged */
return confirmed;
}
}
! /* not present, make new entry */
! current = pg_malloc(sizeof *current);
! current->name = pg_strdup(name);
! current->value = pg_strdup(value);
! current->assign_hook = NULL;
! current->next = NULL;
! previous->next = current;
return true;
}
/*
! * Attach an assign hook function to the named variable.
*
! * If the variable doesn't already exist, create it with value NULL,
! * just so we have a place to store the hook function. (Externally,
! * this isn't different from it not being defined.)
*
! * The hook is immediately called on the variable's current value. This is
! * meant to let it update any derived psql state. If the hook doesn't like
! * the current value, it will print a message to that effect, but we'll ignore
! * it. Generally we do not expect any such failure here, because this should
! * get called before any user-supplied value is assigned.
*/
void
! SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook hook)
{
struct _variable *current,
*previous;
--- 274,334 ----
if (current->value)
pg_free(current->value);
current->value = new_value;
+
+ /*
+ * If we deleted the value, and there are no hooks to
+ * remember, we can discard the variable altogether.
+ */
+ if (new_value == NULL &&
+ current->substitute_hook == NULL &&
+ current->assign_hook == NULL)
+ {
+ previous->next = current->next;
+ free(current->name);
+ free(current);
+ }
}
! else if (new_value)
pg_free(new_value); /* current->value is left unchanged */
return confirmed;
}
}
! /* not present, make new entry ... unless we were asked to delete */
! if (value)
! {
! current = pg_malloc(sizeof *current);
! current->name = pg_strdup(name);
! current->value = pg_strdup(value);
! current->substitute_hook = NULL;
! current->assign_hook = NULL;
! current->next = NULL;
! previous->next = current;
! }
return true;
}
/*
! * Attach substitute and/or assign hook functions to the named variable.
! * If you need only one hook, pass NULL for the other.
*
! * If the variable doesn't already exist, create it with value NULL, just so
! * we have a place to store the hook function(s). (The substitute hook might
! * immediately change the NULL to something else; if not, this state is
! * externally the same as the variable not being defined.)
*
! * The substitute hook, if given, is immediately called on the variable's
! * value. Then the assign hook, if given, is called on the variable's value.
! * This is meant to let it update any derived psql state. If the assign hook
! * doesn't like the current value, it will print a message to that effect,
! * but we'll ignore it. Generally we do not expect any such failure here,
! * because this should get called before any user-supplied value is assigned.
*/
void
! SetVariableHooks(VariableSpace space, const char *name,
! VariableSubstituteHook shook,
! VariableAssignHook ahook)
{
struct _variable *current,
*previous;
*************** SetVariableAssignHook(VariableSpace spac
*** 317,324 ****
if (strcmp(current->name, name) == 0)
{
/* found entry, so update */
! current->assign_hook = hook;
! (void) (*hook) (current->value);
return;
}
}
--- 346,357 ----
if (strcmp(current->name, name) == 0)
{
/* found entry, so update */
! current->substitute_hook = shook;
! current->assign_hook = ahook;
! if (shook)
! current->value = (*shook) (current->value);
! if (ahook)
! (void) (*ahook) (current->value);
return;
}
}
*************** SetVariableAssignHook(VariableSpace spac
*** 327,336 ****
current = pg_malloc(sizeof *current);
current->name = pg_strdup(name);
current->value = NULL;
! current->assign_hook = hook;
current->next = NULL;
previous->next = current;
! (void) (*hook) (NULL);
}
/*
--- 360,373 ----
current = pg_malloc(sizeof *current);
current->name = pg_strdup(name);
current->value = NULL;
! current->substitute_hook = shook;
! current->assign_hook = ahook;
current->next = NULL;
previous->next = current;
! if (shook)
! current->value = (*shook) (current->value);
! if (ahook)
! (void) (*ahook) (current->value);
}
/*
*************** SetVariableBool(VariableSpace space, con
*** 351,392 ****
bool
DeleteVariable(VariableSpace space, const char *name)
{
! struct _variable *current,
! *previous;
!
! if (!space)
! return true;
!
! for (previous = space, current = space->next;
! current;
! previous = current, current = current->next)
! {
! if (strcmp(current->name, name) == 0)
! {
! if (current->assign_hook)
! {
! /* Allow deletion only if hook is okay with NULL value */
! if (!(*current->assign_hook) (NULL))
! return false; /* message printed by hook */
! if (current->value)
! free(current->value);
! current->value = NULL;
! /* Don't delete entry, or we'd forget the hook function */
! }
! else
! {
! /* We can delete the entry as well as its value */
! if (current->value)
! free(current->value);
! previous->next = current->next;
! free(current->name);
! free(current);
! }
! return true;
! }
! }
!
! return true;
}
/*
--- 388,394 ----
bool
DeleteVariable(VariableSpace space, const char *name)
{
! return SetVariable(space, name, NULL);
}
/*
diff --git a/src/bin/psql/variables.h b/src/bin/psql/variables.h
index 274b4af..84be780 100644
*** a/src/bin/psql/variables.h
--- b/src/bin/psql/variables.h
***************
*** 18,29 ****
* prevent invalid values from being assigned, and can update internal C
* variables to keep them in sync with the variable's current value.
*
! * A hook function is called before any attempted assignment, with the
* proposed new value of the variable (or with NULL, if an \unset is being
* attempted). If it returns false, the assignment doesn't occur --- it
* should print an error message with psql_error() to tell the user why.
*
! * When a hook function is installed with SetVariableAssignHook(), it is
* called with the variable's current value (or with NULL, if it wasn't set
* yet). But its return value is ignored in this case. The hook should be
* set before any possibly-invalid value can be assigned.
--- 18,29 ----
* prevent invalid values from being assigned, and can update internal C
* variables to keep them in sync with the variable's current value.
*
! * An assign hook function is called before any attempted assignment, with the
* proposed new value of the variable (or with NULL, if an \unset is being
* attempted). If it returns false, the assignment doesn't occur --- it
* should print an error message with psql_error() to tell the user why.
*
! * When an assign hook function is installed with SetVariableHooks(), it is
* called with the variable's current value (or with NULL, if it wasn't set
* yet). But its return value is ignored in this case. The hook should be
* set before any possibly-invalid value can be assigned.
***************
*** 31,45 ****
typedef bool (*VariableAssignHook) (const char *newval);
/*
* Data structure representing one variable.
*
* Note: if value == NULL then the variable is logically unset, but we are
! * keeping the struct around so as not to forget about its hook function.
*/
struct _variable
{
char *name;
char *value;
VariableAssignHook assign_hook;
struct _variable *next;
};
--- 31,69 ----
typedef bool (*VariableAssignHook) (const char *newval);
/*
+ * Variables can also be given "substitute hook" functions. The substitute
+ * hook can replace values (including NULL) with other values, allowing
+ * normalization of variable contents. For example, for a boolean variable,
+ * we wish to interpret "\unset FOO" as "\set FOO off", and we can do that
+ * by installing a substitute hook. (We can use the same substitute hook
+ * for all bool or nearly-bool variables, which is why this responsibility
+ * isn't part of the assign hook.)
+ *
+ * The substitute hook is called before any attempted assignment, and before
+ * the assign hook if any, passing the proposed new value of the variable as a
+ * malloc'd string (or NULL, if an \unset is being attempted). It can return
+ * the same value, or a different malloc'd string, or modify the string
+ * in-place. It should free the passed-in value if it's not returning it.
+ * The substitute hook generally should not complain about erroneous values;
+ * that's a job for the assign hook.
+ *
+ * When a substitute hook is installed with SetVariableHooks(), it is applied
+ * to the variable's current value (typically NULL, if it wasn't set yet).
+ * That also happens before applying the assign hook.
+ */
+ typedef char *(*VariableSubstituteHook) (char *newval);
+
+ /*
* Data structure representing one variable.
*
* Note: if value == NULL then the variable is logically unset, but we are
! * keeping the struct around so as not to forget about its hook function(s).
*/
struct _variable
{
char *name;
char *value;
+ VariableSubstituteHook substitute_hook;
VariableAssignHook assign_hook;
struct _variable *next;
};
*************** int GetVariableNum(VariableSpace space,
*** 65,74 ****
void PrintVariables(VariableSpace space);
bool SetVariable(VariableSpace space, const char *name, const char *value);
- void SetVariableAssignHook(VariableSpace space, const char *name, VariableAssignHook hook);
bool SetVariableBool(VariableSpace space, const char *name);
bool DeleteVariable(VariableSpace space, const char *name);
void PsqlVarEnumError(const char *name, const char *value, const char *suggestions);
#endif /* VARIABLES_H */
--- 89,101 ----
void PrintVariables(VariableSpace space);
bool SetVariable(VariableSpace space, const char *name, const char *value);
bool SetVariableBool(VariableSpace space, const char *name);
bool DeleteVariable(VariableSpace space, const char *name);
+ void SetVariableHooks(VariableSpace space, const char *name,
+ VariableSubstituteHook shook,
+ VariableAssignHook ahook);
+
void PsqlVarEnumError(const char *name, const char *value, const char *suggestions);
#endif /* VARIABLES_H */
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 420825a..026a4f0 100644
*** a/src/test/regress/expected/psql.out
--- b/src/test/regress/expected/psql.out
*************** invalid variable name: "invalid/name"
*** 11,16 ****
--- 11,33 ----
unrecognized value "foo" for "AUTOCOMMIT": boolean expected
\set FETCH_COUNT foo
invalid value "foo" for "FETCH_COUNT": integer expected
+ -- check handling of built-in boolean variable
+ \echo :ON_ERROR_ROLLBACK
+ off
+ \set ON_ERROR_ROLLBACK
+ \echo :ON_ERROR_ROLLBACK
+ on
+ \set ON_ERROR_ROLLBACK foo
+ unrecognized value "foo" for "ON_ERROR_ROLLBACK"
+ Available values are: on, off, interactive.
+ \echo :ON_ERROR_ROLLBACK
+ on
+ \set ON_ERROR_ROLLBACK on
+ \echo :ON_ERROR_ROLLBACK
+ on
+ \unset ON_ERROR_ROLLBACK
+ \echo :ON_ERROR_ROLLBACK
+ off
-- \gset
select 10 as test01, 20 as test02, 'Hello' as test03 \gset pref01_
\echo :pref01_test01 :pref01_test02 :pref01_test03
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index 79624b9..d823d11 100644
*** a/src/test/regress/sql/psql.sql
--- b/src/test/regress/sql/psql.sql
***************
*** 10,15 ****
--- 10,25 ----
-- fail: invalid value for special variable
\set AUTOCOMMIT foo
\set FETCH_COUNT foo
+ -- check handling of built-in boolean variable
+ \echo :ON_ERROR_ROLLBACK
+ \set ON_ERROR_ROLLBACK
+ \echo :ON_ERROR_ROLLBACK
+ \set ON_ERROR_ROLLBACK foo
+ \echo :ON_ERROR_ROLLBACK
+ \set ON_ERROR_ROLLBACK on
+ \echo :ON_ERROR_ROLLBACK
+ \unset ON_ERROR_ROLLBACK
+ \echo :ON_ERROR_ROLLBACK
-- \gset
BTW ... while I've been fooling with this issue, I've gotten a bit
annoyed at the fact that "\set" prints the variables in, essentially,
creation order. That makes the list ugly and hard to find things in,
and it exposes some psql implementation details to users. I propose
the attached simple patch to keep the list in name order instead.
(This is on top of my previous patch, but it'd be pretty trivial
to modify to apply against HEAD.)
regards, tom lane
Attachments:
keep-psql-variable-list-sorted-by-name.patchtext/x-diff; charset=us-ascii; name=keep-psql-variable-list-sorted-by-name.patchDownload
diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c
index b9b8fcb..9ca1000 100644
*** a/src/bin/psql/variables.c
--- b/src/bin/psql/variables.c
*************** valid_variable_name(const char *name)
*** 43,48 ****
--- 43,51 ----
/*
* A "variable space" is represented by an otherwise-unused struct _variable
* that serves as list header.
+ *
+ * The list entries are kept in name order (according to strcmp). This
+ * is mainly to make the results of PrintVariables() more pleasing.
*/
VariableSpace
CreateVariableSpace(void)
*************** GetVariable(VariableSpace space, const c
*** 74,84 ****
for (current = space->next; current; current = current->next)
{
! if (strcmp(current->name, name) == 0)
{
/* this is correct answer when value is NULL, too */
return current->value;
}
}
return NULL;
--- 77,91 ----
for (current = space->next; current; current = current->next)
{
! int cmp = strcmp(current->name, name);
!
! if (cmp == 0)
{
/* this is correct answer when value is NULL, too */
return current->value;
}
+ if (cmp > 0)
+ break; /* it's not there */
}
return NULL;
*************** SetVariable(VariableSpace space, const c
*** 247,253 ****
current;
previous = current, current = current->next)
{
! if (strcmp(current->name, name) == 0)
{
/*
* Found entry, so update, unless assign hook returns false.
--- 254,262 ----
current;
previous = current, current = current->next)
{
! int cmp = strcmp(current->name, name);
!
! if (cmp == 0)
{
/*
* Found entry, so update, unless assign hook returns false.
*************** SetVariable(VariableSpace space, const c
*** 293,298 ****
--- 302,309 ----
return confirmed;
}
+ if (cmp > 0)
+ break; /* it's not there */
}
/* not present, make new entry ... unless we were asked to delete */
*************** SetVariable(VariableSpace space, const c
*** 303,309 ****
current->value = pg_strdup(value);
current->substitute_hook = NULL;
current->assign_hook = NULL;
! current->next = NULL;
previous->next = current;
}
return true;
--- 314,320 ----
current->value = pg_strdup(value);
current->substitute_hook = NULL;
current->assign_hook = NULL;
! current->next = previous->next;
previous->next = current;
}
return true;
*************** SetVariableHooks(VariableSpace space, co
*** 343,349 ****
current;
previous = current, current = current->next)
{
! if (strcmp(current->name, name) == 0)
{
/* found entry, so update */
current->substitute_hook = shook;
--- 354,362 ----
current;
previous = current, current = current->next)
{
! int cmp = strcmp(current->name, name);
!
! if (cmp == 0)
{
/* found entry, so update */
current->substitute_hook = shook;
*************** SetVariableHooks(VariableSpace space, co
*** 354,359 ****
--- 367,374 ----
(void) (*ahook) (current->value);
return;
}
+ if (cmp > 0)
+ break; /* it's not there */
}
/* not present, make new entry */
*************** SetVariableHooks(VariableSpace space, co
*** 362,368 ****
current->value = NULL;
current->substitute_hook = shook;
current->assign_hook = ahook;
! current->next = NULL;
previous->next = current;
if (shook)
current->value = (*shook) (current->value);
--- 377,383 ----
current->value = NULL;
current->substitute_hook = shook;
current->assign_hook = ahook;
! current->next = previous->next;
previous->next = current;
if (shook)
current->value = (*shook) (current->value);
Tom Lane wrote:
I updated the documentation as well. I think this is committable if
there are not objections.
That works for me. I tested and read it and did not find anything
unexpected or worth objecting.
"\unset var" acting as "\set var off" makes sense considering
that its opposite "\set var" is now an accepted
synonym of "\set var on" for the boolean built-ins.
Best regards,
--
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
"Daniel Verite" <daniel@manitou-mail.org> writes:
That works for me. I tested and read it and did not find anything
unexpected or worth objecting.
"\unset var" acting as "\set var off" makes sense considering
that its opposite "\set var" is now an accepted
synonym of "\set var on" for the boolean built-ins.
Thanks for reviewing! I've pushed this now --- Andrew, you should
be able to revert the RedisFDW test script to the way it was.
regards, tom lane
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
I wrote:
Attached is a finished version that includes hooks for all the variables
for which they were clearly sensible. (FETCH_COUNT doesn't seem to really
need one, and I didn't do anything with HISTSIZE or IGNOREEOF either.
It might be worth bringing the latter two into the hooks paradigm, but
that seems like fit material for a separate patch.)
I got more excited about doing that after noticing that not only would
it clean up the behavior of those particular variables, but we could get
rid of some code. First, we'd not need the rather warty GetVariableNum()
anymore, and second, we'd then be almost at the point where every control
variable has a hook, and therefore we could drop tab-complete.c's private
list of known variable names. That was only ever needed to cover the
possibility of important variables not being present in the variables
list.
So the attached proposed patch does these things:
1. Modify FETCH_COUNT to always have a defined value, like other control
variables, mainly so it will always appear in "\set" output.
2. Add hooks to force HISTSIZE to be defined and require it to have an
integer value. (I don't see any point in allowing it to be set to
non-integral values.)
3. Add hooks to force IGNOREEOF to be defined and require it to have an
integer value. Unlike the other cases, here we're trying to be
bug-compatible with a rather bogus externally-defined behavior, so I think
we need to continue to allow "\set IGNOREEOF whatever". What I propose is
that the substitution hook silently replace non-numeric values with "10",
so that the stored value always reflects what we're really doing.
4. Add a dummy assign hook for HISTFILE, just so it's always in
variables.c's list. We can't require it to be defined always, because
that would break the interaction with the PSQL_HISTORY environment
variable, so there isn't any change in visible behavior here.
5. Remove tab-complete.c's private list of known variable names. Given
the other changes, there are no control variables it won't show anyway.
This does mean that if for some reason you've unset one of the status
variables (DBNAME, HOST, etc), that variable would not appear in tab
completion for \set. But I think that's fine, for at least two reasons:
we shouldn't be encouraging people to use those variables as regular
variables, and if someone does do so anyway, why shouldn't it act just
like a regular variable?
6. Remove no-longer-used-anywhere GetVariableNum().
Any objections?
regards, tom lane
Attachments:
more-psql-variable-cleanup.patchtext/x-diff; charset=us-ascii; name=more-psql-variable-cleanup.patchDownload
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index b9c8fcc..ae58708 100644
*** a/doc/src/sgml/ref/psql-ref.sgml
--- b/doc/src/sgml/ref/psql-ref.sgml
*************** bar
*** 3247,3258 ****
fail after having already displayed some rows.
</para>
- <para>
- <varname>FETCH_COUNT</varname> is ignored if it is unset or does not
- have a positive value. It cannot be set to a value that is not
- syntactically an integer.
- </para>
-
<tip>
<para>
Although you can use any output format with this feature,
--- 3247,3252 ----
*************** bar
*** 3316,3325 ****
<term><varname>HISTSIZE</varname></term>
<listitem>
<para>
! The maximum number of commands to store in the command history.
! If unset, at most 500 commands are stored by default.
! If set to a value that is negative or not an integer, no limit is
! applied.
</para>
<note>
<para>
--- 3310,3317 ----
<term><varname>HISTSIZE</varname></term>
<listitem>
<para>
! The maximum number of commands to store in the command history
! (default 500). If set to a negative value, no limit is applied.
</para>
<note>
<para>
*************** bar
*** 3345,3357 ****
<term><varname>IGNOREEOF</varname></term>
<listitem>
<para>
! If unset, sending an <acronym>EOF</> character (usually
<keycombo action="simul"><keycap>Control</><keycap>D</></>)
to an interactive session of <application>psql</application>
! will terminate the application. If set to a numeric value,
! that many <acronym>EOF</> characters are ignored before the
! application terminates. If the variable is set but not to a
! numeric value, the default is 10.
</para>
<note>
<para>
--- 3337,3349 ----
<term><varname>IGNOREEOF</varname></term>
<listitem>
<para>
! If set to 1 or less, sending an <acronym>EOF</> character (usually
<keycombo action="simul"><keycap>Control</><keycap>D</></>)
to an interactive session of <application>psql</application>
! will terminate the application. If set to a larger numeric value,
! that many consecutive <acronym>EOF</> characters must be typed to
! make an interactive session terminate. If the variable is set to a
! non-numeric value, it is interpreted as 10.
</para>
<note>
<para>
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 5365629..3e3cab4 100644
*** a/src/bin/psql/help.c
--- b/src/bin/psql/help.c
*************** helpVariables(unsigned short int pager)
*** 348,356 ****
" (default: 0=unlimited)\n"));
fprintf(output, _(" HISTCONTROL controls command history [ignorespace, ignoredups, ignoreboth]\n"));
fprintf(output, _(" HISTFILE file name used to store the command history\n"));
! fprintf(output, _(" HISTSIZE the number of commands to store in the command history\n"));
fprintf(output, _(" HOST the currently connected database server host\n"));
! fprintf(output, _(" IGNOREEOF if unset, sending an EOF to interactive session terminates application\n"));
fprintf(output, _(" LASTOID value of the last affected OID\n"));
fprintf(output, _(" ON_ERROR_ROLLBACK if set, an error doesn't stop a transaction (uses implicit savepoints)\n"));
fprintf(output, _(" ON_ERROR_STOP stop batch execution after error\n"));
--- 348,356 ----
" (default: 0=unlimited)\n"));
fprintf(output, _(" HISTCONTROL controls command history [ignorespace, ignoredups, ignoreboth]\n"));
fprintf(output, _(" HISTFILE file name used to store the command history\n"));
! fprintf(output, _(" HISTSIZE max number of commands to store in the command history\n"));
fprintf(output, _(" HOST the currently connected database server host\n"));
! fprintf(output, _(" IGNOREEOF number of EOFs needed to terminate an interactive session\n"));
fprintf(output, _(" LASTOID value of the last affected OID\n"));
fprintf(output, _(" ON_ERROR_ROLLBACK if set, an error doesn't stop a transaction (uses implicit savepoints)\n"));
fprintf(output, _(" ON_ERROR_STOP stop batch execution after error\n"));
diff --git a/src/bin/psql/input.c b/src/bin/psql/input.c
index 3e3e97a..b8c9a00 100644
*** a/src/bin/psql/input.c
--- b/src/bin/psql/input.c
*************** finishInput(void)
*** 539,548 ****
#ifdef USE_READLINE
if (useHistory && psql_history)
{
! int hist_size;
!
! hist_size = GetVariableNum(pset.vars, "HISTSIZE", 500, -1);
! (void) saveHistory(psql_history, hist_size);
free(psql_history);
psql_history = NULL;
}
--- 539,545 ----
#ifdef USE_READLINE
if (useHistory && psql_history)
{
! (void) saveHistory(psql_history, pset.histsize);
free(psql_history);
psql_history = NULL;
}
diff --git a/src/bin/psql/mainloop.c b/src/bin/psql/mainloop.c
index dc25b4b..6e358e2 100644
*** a/src/bin/psql/mainloop.c
--- b/src/bin/psql/mainloop.c
*************** MainLoop(FILE *source)
*** 162,168 ****
/* This tries to mimic bash's IGNOREEOF feature. */
count_eof++;
! if (count_eof < GetVariableNum(pset.vars, "IGNOREEOF", 0, 10))
{
if (!pset.quiet)
printf(_("Use \"\\q\" to leave %s.\n"), pset.progname);
--- 162,168 ----
/* This tries to mimic bash's IGNOREEOF feature. */
count_eof++;
! if (count_eof < pset.ignoreeof)
{
if (!pset.quiet)
printf(_("Use \"\\q\" to leave %s.\n"), pset.progname);
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 4c7c3b1..195f5a1 100644
*** a/src/bin/psql/settings.h
--- b/src/bin/psql/settings.h
*************** typedef struct _psqlSettings
*** 125,130 ****
--- 125,132 ----
bool singleline;
bool singlestep;
int fetch_count;
+ int histsize;
+ int ignoreeof;
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 a3654e6..88d686a 100644
*** a/src/bin/psql/startup.c
--- b/src/bin/psql/startup.c
*************** showVersion(void)
*** 774,779 ****
--- 774,784 ----
* Substitute hooks and assign hooks for psql variables.
*
* This isn't an amazingly good place for them, but neither is anywhere else.
+ *
+ * By policy, every special variable that controls any psql behavior should
+ * have one or both hooks, even if they're just no-ops. This ensures that
+ * the variable will remain present in variables.c's list even when unset,
+ * which ensures that it's known to tab completion.
*/
static char *
*************** singlestep_hook(const char *newval)
*** 823,839 ****
return ParseVariableBool(newval, "SINGLESTEP", &pset.singlestep);
}
static bool
fetch_count_hook(const char *newval)
{
! if (newval == NULL)
! pset.fetch_count = -1; /* default value */
! else if (!ParseVariableNum(newval, "FETCH_COUNT", &pset.fetch_count))
! return false;
return true;
}
static char *
echo_substitute_hook(char *newval)
{
if (newval == NULL)
--- 828,899 ----
return ParseVariableBool(newval, "SINGLESTEP", &pset.singlestep);
}
+ static char *
+ fetch_count_substitute_hook(char *newval)
+ {
+ if (newval == NULL)
+ newval = pg_strdup("0");
+ return newval;
+ }
+
static bool
fetch_count_hook(const char *newval)
{
! return ParseVariableNum(newval, "FETCH_COUNT", &pset.fetch_count);
! }
!
! static bool
! histfile_hook(const char *newval)
! {
! /*
! * Someday we might try to validate the filename, but for now, this is
! * just a placeholder to ensure HISTFILE is known to tab completion.
! */
return true;
}
static char *
+ histsize_substitute_hook(char *newval)
+ {
+ if (newval == NULL)
+ newval = pg_strdup("500");
+ return newval;
+ }
+
+ static bool
+ histsize_hook(const char *newval)
+ {
+ return ParseVariableNum(newval, "HISTSIZE", &pset.histsize);
+ }
+
+ static char *
+ ignoreeof_substitute_hook(char *newval)
+ {
+ int dummy;
+
+ /*
+ * This tries to mimic the behavior of bash, to wit "If set, the value is
+ * the number of consecutive EOF characters which must be typed as the
+ * first characters on an input line before bash exits. If the variable
+ * exists but does not have a numeric value, or has no value, the default
+ * value is 10. If it does not exist, EOF signifies the end of input to
+ * the shell." Unlike bash, however, we insist on the stored value
+ * actually being a valid integer.
+ */
+ if (newval == NULL)
+ newval = pg_strdup("0");
+ else if (!ParseVariableNum(newval, NULL, &dummy))
+ newval = pg_strdup("10");
+ return newval;
+ }
+
+ static bool
+ ignoreeof_hook(const char *newval)
+ {
+ return ParseVariableNum(newval, "IGNOREEOF", &pset.ignoreeof);
+ }
+
+ static char *
echo_substitute_hook(char *newval)
{
if (newval == NULL)
*************** EstablishVariableSpace(void)
*** 1062,1069 ****
bool_substitute_hook,
singlestep_hook);
SetVariableHooks(pset.vars, "FETCH_COUNT",
! NULL,
fetch_count_hook);
SetVariableHooks(pset.vars, "ECHO",
echo_substitute_hook,
echo_hook);
--- 1122,1138 ----
bool_substitute_hook,
singlestep_hook);
SetVariableHooks(pset.vars, "FETCH_COUNT",
! fetch_count_substitute_hook,
fetch_count_hook);
+ SetVariableHooks(pset.vars, "HISTFILE",
+ NULL,
+ histfile_hook);
+ SetVariableHooks(pset.vars, "HISTSIZE",
+ histsize_substitute_hook,
+ histsize_hook);
+ SetVariableHooks(pset.vars, "IGNOREEOF",
+ ignoreeof_substitute_hook,
+ ignoreeof_hook);
SetVariableHooks(pset.vars, "ECHO",
echo_substitute_hook,
echo_hook);
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index d6fffcf..6e759d0 100644
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
*************** append_variable_names(char ***varnames,
*** 3775,3782 ****
/*
* This function supports completion with the name of a psql variable.
* The variable names can be prefixed and suffixed with additional text
! * to support quoting usages. If need_value is true, only the variables
! * that have the set values are picked up.
*/
static char **
complete_from_variables(const char *text, const char *prefix, const char *suffix,
--- 3775,3783 ----
/*
* This function supports completion with the name of a psql variable.
* The variable names can be prefixed and suffixed with additional text
! * to support quoting usages. If need_value is true, only variables
! * that are currently set are included; otherwise, special variables
! * (those that have hooks) are included even if currently unset.
*/
static char **
complete_from_variables(const char *text, const char *prefix, const char *suffix,
*************** complete_from_variables(const char *text
*** 3789,3821 ****
int i;
struct _variable *ptr;
- static const char *const known_varnames[] = {
- "AUTOCOMMIT", "COMP_KEYWORD_CASE", "DBNAME", "ECHO", "ECHO_HIDDEN",
- "ENCODING", "FETCH_COUNT", "HISTCONTROL", "HISTFILE", "HISTSIZE",
- "HOST", "IGNOREEOF", "LASTOID", "ON_ERROR_ROLLBACK", "ON_ERROR_STOP",
- "PORT", "PROMPT1", "PROMPT2", "PROMPT3", "QUIET",
- "SHOW_CONTEXT", "SINGLELINE", "SINGLESTEP",
- "USER", "VERBOSITY", NULL
- };
-
varnames = (char **) pg_malloc((maxvars + 1) * sizeof(char *));
- if (!need_value)
- {
- for (i = 0; known_varnames[i] && nvars < maxvars; i++)
- append_variable_names(&varnames, &nvars, &maxvars,
- known_varnames[i], prefix, suffix);
- }
-
for (ptr = pset.vars->next; ptr; ptr = ptr->next)
{
if (need_value && !(ptr->value))
continue;
- for (i = 0; known_varnames[i]; i++) /* remove duplicate entry */
- {
- if (strcmp(ptr->name, known_varnames[i]) == 0)
- continue;
- }
append_variable_names(&varnames, &nvars, &maxvars, ptr->name,
prefix, suffix);
}
--- 3790,3801 ----
diff --git a/src/bin/psql/variables.c b/src/bin/psql/variables.c
index 9ca1000..d9d0763 100644
*** a/src/bin/psql/variables.c
--- b/src/bin/psql/variables.c
*************** ParseVariableNum(const char *value, cons
*** 180,210 ****
}
/*
- * Read integer value of the numeric variable named "name".
- *
- * Return defaultval if it is not set, or faultval if its value is not a
- * valid integer. (No error message is issued.)
- */
- int
- GetVariableNum(VariableSpace space,
- const char *name,
- int defaultval,
- int faultval)
- {
- const char *val;
- int result;
-
- val = GetVariable(space, name);
- if (!val)
- return defaultval;
-
- if (ParseVariableNum(val, NULL, &result))
- return result;
- else
- return faultval;
- }
-
- /*
* Print values of all variables.
*/
void
--- 180,185 ----
diff --git a/src/bin/psql/variables.h b/src/bin/psql/variables.h
index 84be780..1925793 100644
*** a/src/bin/psql/variables.h
--- b/src/bin/psql/variables.h
*************** bool ParseVariableBool(const char *value
*** 81,91 ****
bool ParseVariableNum(const char *value, const char *name,
int *result);
- int GetVariableNum(VariableSpace space,
- const char *name,
- int defaultval,
- int faultval);
-
void PrintVariables(VariableSpace space);
bool SetVariable(VariableSpace space, const char *name, const char *value);
--- 81,86 ----
On 02/01/2017 11:26 AM, Tom Lane wrote:
"Daniel Verite" <daniel@manitou-mail.org> writes:
That works for me. I tested and read it and did not find anything
unexpected or worth objecting.
"\unset var" acting as "\set var off" makes sense considering
that its opposite "\set var" is now an accepted
synonym of "\set var on" for the boolean built-ins.Thanks for reviewing! I've pushed this now --- Andrew, you should
be able to revert the RedisFDW test script to the way it was.
Done.
cheers
andrew
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers