Re: [HACKERS] Improvements in psql hooks for variables

Поиск
Список
Период
Сортировка
От Tom Lane
Тема Re: [HACKERS] Improvements in psql hooks for variables
Дата
Msg-id 30629.1485881533@sss.pgh.pa.us
обсуждение исходный текст
Ответ на Re: [HACKERS] Improvements in psql hooks for variables  ("Daniel Verite" <daniel@manitou-mail.org>)
Ответы Re: [HACKERS] Improvements in psql hooks for variables  (Tom Lane <tgl@sss.pgh.pa.us>)
Список 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

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


-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

В списке pgsql-hackers по дате отправления:

Предыдущее
От: Mithun Cy
Дата:
Сообщение: Re: [HACKERS] Proposal : For Auto-Prewarm.
Следующее
От: Alvaro Herrera
Дата:
Сообщение: Re: [HACKERS] Patch: Write Amplification Reduction Method (WARM)