Re: BUG #15060: Row in table not found when using pg function in an expression

Поиск
Список
Период
Сортировка
От Tom Lane
Тема Re: BUG #15060: Row in table not found when using pg function in an expression
Дата
Msg-id 24837.1518543136@sss.pgh.pa.us
обсуждение исходный текст
Ответ на Re: BUG #15060: Row in table not found when using pg function in an expression  (Tom Lane <tgl@sss.pgh.pa.us>)
Список pgsql-bugs
I wrote:
> Hm.  So we could improve on the fix I proposed earlier if there were a
> way to tell GetCachedPlan "take a new snapshot even though there is an
> active snapshot".  It's still expensive, but at least the code path where
> we use an existing generic plan doesn't get any added cost.

Here's a draft patch along those lines.  Because it changes
GetCachedPlan's API, I'd be a bit worried about back-patching it; there
might be third-party code calling that directly.  However, given that it
took so many years for anyone to notice a problem here, I think maybe
just fixing it in HEAD is fine.

            regards, tom lane

diff --git a/src/backend/commands/prepare.c b/src/backend/commands/prepare.c
index b945b15..8991078 100644
*** a/src/backend/commands/prepare.c
--- b/src/backend/commands/prepare.c
*************** ExecuteQuery(ExecuteStmt *stmt, IntoClau
*** 243,249 ****
                                         entry->plansource->query_string);

      /* Replan if needed, and increment plan refcount for portal */
!     cplan = GetCachedPlan(entry->plansource, paramLI, false, NULL);
      plan_list = cplan->stmt_list;

      /*
--- 243,249 ----
                                         entry->plansource->query_string);

      /* Replan if needed, and increment plan refcount for portal */
!     cplan = GetCachedPlan(entry->plansource, paramLI, false, false, NULL);
      plan_list = cplan->stmt_list;

      /*
*************** ExplainExecuteQuery(ExecuteStmt *execstm
*** 670,676 ****
      }

      /* Replan if needed, and acquire a transient refcount */
!     cplan = GetCachedPlan(entry->plansource, paramLI, true, queryEnv);

      INSTR_TIME_SET_CURRENT(planduration);
      INSTR_TIME_SUBTRACT(planduration, planstart);
--- 670,676 ----
      }

      /* Replan if needed, and acquire a transient refcount */
!     cplan = GetCachedPlan(entry->plansource, paramLI, true, false, queryEnv);

      INSTR_TIME_SET_CURRENT(planduration);
      INSTR_TIME_SUBTRACT(planduration, planstart);
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 9fc4431..502b2e6 100644
*** a/src/backend/executor/spi.c
--- b/src/backend/executor/spi.c
*************** SPI_cursor_open_internal(const char *nam
*** 1289,1296 ****
       * plancache refcount.
       */

!     /* Replan if needed, and increment plan refcount for portal */
!     cplan = GetCachedPlan(plansource, paramLI, false, _SPI_current->queryEnv);
      stmt_list = cplan->stmt_list;

      if (!plan->saved)
--- 1289,1301 ----
       * plancache refcount.
       */

!     /*
!      * Replan if needed, and increment plan refcount for portal.  If
!      * read_only, we can use the current snapshot for replanning, same as
!      * we'll do for execution.  Otherwise best get a new one.
!      */
!     cplan = GetCachedPlan(plansource, paramLI, false, !read_only,
!                           _SPI_current->queryEnv);
      stmt_list = cplan->stmt_list;

      if (!plan->saved)
*************** SPI_plan_get_cached_plan(SPIPlanPtr plan
*** 1722,1729 ****
      spierrcontext.previous = error_context_stack;
      error_context_stack = &spierrcontext;

!     /* Get the generic plan for the query */
!     cplan = GetCachedPlan(plansource, NULL, plan->saved,
                            _SPI_current->queryEnv);
      Assert(cplan == plansource->gplan);

--- 1727,1737 ----
      spierrcontext.previous = error_context_stack;
      error_context_stack = &spierrcontext;

!     /*
!      * Get the generic plan for the query.  Be paranoid and force use of a new
!      * snapshot for any replan activity (in current use, that's uncommon).
!      */
!     cplan = GetCachedPlan(plansource, NULL, plan->saved, true,
                            _SPI_current->queryEnv);
      Assert(cplan == plansource->gplan);

*************** _SPI_execute_plan(SPIPlanPtr plan, Param
*** 2010,2015 ****
--- 2018,2024 ----
      Oid            my_lastoid = InvalidOid;
      SPITupleTable *my_tuptable = NULL;
      int            res = 0;
+     bool        useNewSnapshot;
      bool        pushed_active_snap = false;
      ErrorContextCallback spierrcontext;
      CachedPlan *cplan = NULL;
*************** _SPI_execute_plan(SPIPlanPtr plan, Param
*** 2057,2062 ****
--- 2066,2079 ----
          }
      }

+     /*
+      * In the default non-read-only case, we must get a new snapshot for each
+      * plansource, and also tell GetCachedPlan to get new snapshots.  (Without
+      * the latter, we have anomalies if any stable functions that are
+      * speculatively executed by the planner look at the contents of tables.)
+      */
+     useNewSnapshot = (snapshot == InvalidSnapshot && !read_only);
+
      foreach(lc1, plan->plancache_list)
      {
          CachedPlanSource *plansource = (CachedPlanSource *) lfirst(lc1);
*************** _SPI_execute_plan(SPIPlanPtr plan, Param
*** 2114,2127 ****
           * Replan if needed, and increment plan refcount.  If it's a saved
           * plan, the refcount must be backed by the CurrentResourceOwner.
           */
!         cplan = GetCachedPlan(plansource, paramLI, plan->saved, _SPI_current->queryEnv);
          stmt_list = cplan->stmt_list;

          /*
!          * In the default non-read-only case, get a new snapshot, replacing
!          * any that we pushed in a previous cycle.
           */
!         if (snapshot == InvalidSnapshot && !read_only)
          {
              if (pushed_active_snap)
                  PopActiveSnapshot();
--- 2131,2145 ----
           * Replan if needed, and increment plan refcount.  If it's a saved
           * plan, the refcount must be backed by the CurrentResourceOwner.
           */
!         cplan = GetCachedPlan(plansource, paramLI, plan->saved, useNewSnapshot,
!                               _SPI_current->queryEnv);
          stmt_list = cplan->stmt_list;

          /*
!          * If required, get a new snapshot, replacing any that we pushed in a
!          * previous cycle.
           */
!         if (useNewSnapshot)
          {
              if (pushed_active_snap)
                  PopActiveSnapshot();
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 6dc2095..3b8c5c9 100644
*** a/src/backend/tcop/postgres.c
--- b/src/backend/tcop/postgres.c
*************** exec_bind_message(StringInfo input_messa
*** 1794,1800 ****
       * will be generated in MessageContext.  The plan refcount will be
       * assigned to the Portal, so it will be released at portal destruction.
       */
!     cplan = GetCachedPlan(psrc, params, false, NULL);

      /*
       * Now we can define the portal.
--- 1794,1800 ----
       * will be generated in MessageContext.  The plan refcount will be
       * assigned to the Portal, so it will be released at portal destruction.
       */
!     cplan = GetCachedPlan(psrc, params, false, false, NULL);

      /*
       * Now we can define the portal.
diff --git a/src/backend/utils/cache/plancache.c b/src/backend/utils/cache/plancache.c
index 8d7d8e0..b596417 100644
*** a/src/backend/utils/cache/plancache.c
--- b/src/backend/utils/cache/plancache.c
*************** static CachedPlanSource *first_saved_pla
*** 89,98 ****

  static void ReleaseGenericPlan(CachedPlanSource *plansource);
  static List *RevalidateCachedQuery(CachedPlanSource *plansource,
                        QueryEnvironment *queryEnv);
  static bool CheckCachedPlan(CachedPlanSource *plansource);
  static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
!                 ParamListInfo boundParams, QueryEnvironment *queryEnv);
  static bool choose_custom_plan(CachedPlanSource *plansource,
                     ParamListInfo boundParams);
  static double cached_plan_cost(CachedPlan *plan, bool include_planner);
--- 89,100 ----

  static void ReleaseGenericPlan(CachedPlanSource *plansource);
  static List *RevalidateCachedQuery(CachedPlanSource *plansource,
+                       bool useNewSnapshot,
                        QueryEnvironment *queryEnv);
  static bool CheckCachedPlan(CachedPlanSource *plansource);
  static CachedPlan *BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
!                 ParamListInfo boundParams, bool useNewSnapshot,
!                 QueryEnvironment *queryEnv);
  static bool choose_custom_plan(CachedPlanSource *plansource,
                     ParamListInfo boundParams);
  static double cached_plan_cost(CachedPlan *plan, bool include_planner);
*************** ReleaseGenericPlan(CachedPlanSource *pla
*** 547,553 ****
   * planning.
   *
   * If any parse analysis activity is required, the caller's memory context is
!  * used for that work.
   *
   * The result value is the transient analyzed-and-rewritten query tree if we
   * had to do re-analysis, and NIL otherwise.  (This is returned just to save
--- 549,556 ----
   * planning.
   *
   * If any parse analysis activity is required, the caller's memory context is
!  * used for that work.  Also, if useNewSnapshot is true or there's no
!  * ActiveSnapshot, we push a new snapshot for that work.
   *
   * The result value is the transient analyzed-and-rewritten query tree if we
   * had to do re-analysis, and NIL otherwise.  (This is returned just to save
*************** ReleaseGenericPlan(CachedPlanSource *pla
*** 555,560 ****
--- 558,564 ----
   */
  static List *
  RevalidateCachedQuery(CachedPlanSource *plansource,
+                       bool useNewSnapshot,
                        QueryEnvironment *queryEnv)
  {
      bool        snapshot_set;
*************** RevalidateCachedQuery(CachedPlanSource *
*** 663,675 ****

      /*
       * If a snapshot is already set (the normal case), we can just use that
!      * for parsing/planning.  But if it isn't, install one.  Note: no point in
       * checking whether parse analysis requires a snapshot; utility commands
       * don't have invalidatable plans, so we'd not get here for such a
       * command.
       */
      snapshot_set = false;
!     if (!ActiveSnapshotSet())
      {
          PushActiveSnapshot(GetTransactionSnapshot());
          snapshot_set = true;
--- 667,680 ----

      /*
       * If a snapshot is already set (the normal case), we can just use that
!      * for parsing/planning.  But if it isn't (or if the caller thinks it's
!      * possibly not fresh enough), install a new one.  Note: no point in
       * checking whether parse analysis requires a snapshot; utility commands
       * don't have invalidatable plans, so we'd not get here for such a
       * command.
       */
      snapshot_set = false;
!     if (useNewSnapshot || !ActiveSnapshotSet())
      {
          PushActiveSnapshot(GetTransactionSnapshot());
          snapshot_set = true;
*************** CheckCachedPlan(CachedPlanSource *planso
*** 873,885 ****
   * each parameter value; otherwise the planner will treat the value as a
   * hint rather than a hard constant.
   *
   * Planning work is done in the caller's memory context.  The finished plan
   * is in a child memory context, which typically should get reparented
   * (unless this is a one-shot plan, in which case we don't copy the plan).
   */
  static CachedPlan *
  BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
!                 ParamListInfo boundParams, QueryEnvironment *queryEnv)
  {
      CachedPlan *plan;
      List       *plist;
--- 878,895 ----
   * each parameter value; otherwise the planner will treat the value as a
   * hint rather than a hard constant.
   *
+  * Pass useNewSnapshot = true to force a new snapshot to be taken for
+  * planning; otherwise, we'll use the ActiveSnapshot if there is one.
+  *
   * Planning work is done in the caller's memory context.  The finished plan
   * is in a child memory context, which typically should get reparented
   * (unless this is a one-shot plan, in which case we don't copy the plan).
   */
  static CachedPlan *
  BuildCachedPlan(CachedPlanSource *plansource, List *qlist,
!                 ParamListInfo boundParams,
!                 bool useNewSnapshot,
!                 QueryEnvironment *queryEnv)
  {
      CachedPlan *plan;
      List       *plist;
*************** BuildCachedPlan(CachedPlanSource *planso
*** 903,909 ****
       * safety, let's treat it as real and redo the RevalidateCachedQuery call.
       */
      if (!plansource->is_valid)
!         qlist = RevalidateCachedQuery(plansource, queryEnv);

      /*
       * If we don't already have a copy of the querytree list that can be
--- 913,919 ----
       * safety, let's treat it as real and redo the RevalidateCachedQuery call.
       */
      if (!plansource->is_valid)
!         qlist = RevalidateCachedQuery(plansource, useNewSnapshot, queryEnv);

      /*
       * If we don't already have a copy of the querytree list that can be
*************** BuildCachedPlan(CachedPlanSource *planso
*** 920,929 ****

      /*
       * If a snapshot is already set (the normal case), we can just use that
!      * for planning.  But if it isn't, and we need one, install one.
       */
      snapshot_set = false;
!     if (!ActiveSnapshotSet() &&
          plansource->raw_parse_tree &&
          analyze_requires_snapshot(plansource->raw_parse_tree))
      {
--- 930,940 ----

      /*
       * If a snapshot is already set (the normal case), we can just use that
!      * for planning.  But if it isn't (or if the caller thinks it's possibly
!      * not fresh enough), and we need one, install one.
       */
      snapshot_set = false;
!     if ((useNewSnapshot || !ActiveSnapshotSet()) &&
          plansource->raw_parse_tree &&
          analyze_requires_snapshot(plansource->raw_parse_tree))
      {
*************** cached_plan_cost(CachedPlan *plan, bool
*** 1129,1139 ****
   * only be true if it's a "saved" CachedPlanSource).
   *
   * Note: if any replanning activity is required, the caller's memory context
!  * is used for that work.
   */
  CachedPlan *
  GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
!               bool useResOwner, QueryEnvironment *queryEnv)
  {
      CachedPlan *plan = NULL;
      List       *qlist;
--- 1140,1153 ----
   * only be true if it's a "saved" CachedPlanSource).
   *
   * Note: if any replanning activity is required, the caller's memory context
!  * is used for that work.  Also, we will take a new snapshot for that work
!  * if useNewSnapshot is true, and otherwise use the ActiveSnapshot if there
!  * is one.
   */
  CachedPlan *
  GetCachedPlan(CachedPlanSource *plansource, ParamListInfo boundParams,
!               bool useResOwner, bool useNewSnapshot,
!               QueryEnvironment *queryEnv)
  {
      CachedPlan *plan = NULL;
      List       *qlist;
*************** GetCachedPlan(CachedPlanSource *plansour
*** 1147,1153 ****
          elog(ERROR, "cannot apply ResourceOwner to non-saved cached plan");

      /* Make sure the querytree list is valid and we have parse-time locks */
!     qlist = RevalidateCachedQuery(plansource, queryEnv);

      /* Decide whether to use a custom plan */
      customplan = choose_custom_plan(plansource, boundParams);
--- 1161,1167 ----
          elog(ERROR, "cannot apply ResourceOwner to non-saved cached plan");

      /* Make sure the querytree list is valid and we have parse-time locks */
!     qlist = RevalidateCachedQuery(plansource, useNewSnapshot, queryEnv);

      /* Decide whether to use a custom plan */
      customplan = choose_custom_plan(plansource, boundParams);
*************** GetCachedPlan(CachedPlanSource *plansour
*** 1163,1169 ****
          else
          {
              /* Build a new generic plan */
!             plan = BuildCachedPlan(plansource, qlist, NULL, queryEnv);
              /* Just make real sure plansource->gplan is clear */
              ReleaseGenericPlan(plansource);
              /* Link the new generic plan into the plansource */
--- 1177,1184 ----
          else
          {
              /* Build a new generic plan */
!             plan = BuildCachedPlan(plansource, qlist, NULL,
!                                    useNewSnapshot, queryEnv);
              /* Just make real sure plansource->gplan is clear */
              ReleaseGenericPlan(plansource);
              /* Link the new generic plan into the plansource */
*************** GetCachedPlan(CachedPlanSource *plansour
*** 1208,1214 ****
      if (customplan)
      {
          /* Build a custom plan */
!         plan = BuildCachedPlan(plansource, qlist, boundParams, queryEnv);
          /* Accumulate total costs of custom plans, but 'ware overflow */
          if (plansource->num_custom_plans < INT_MAX)
          {
--- 1223,1230 ----
      if (customplan)
      {
          /* Build a custom plan */
!         plan = BuildCachedPlan(plansource, qlist, boundParams,
!                                useNewSnapshot, queryEnv);
          /* Accumulate total costs of custom plans, but 'ware overflow */
          if (plansource->num_custom_plans < INT_MAX)
          {
*************** CachedPlanGetTargetList(CachedPlanSource
*** 1438,1445 ****
      if (plansource->resultDesc == NULL)
          return NIL;

!     /* Make sure the querytree list is valid and we have parse-time locks */
!     RevalidateCachedQuery(plansource, queryEnv);

      /* Get the primary statement and find out what it returns */
      pstmt = QueryListGetPrimaryStmt(plansource->query_list);
--- 1454,1467 ----
      if (plansource->resultDesc == NULL)
          return NIL;

!     /*
!      * Make sure the querytree list is valid and we have parse-time locks.
!      * Force a new snapshot to be taken if we redo parse analysis.  (In some
!      * cases that might be overkill, but existing callers aren't performance
!      * critical, so it seems not worth complicating this function's API by
!      * asking callers to decide whether that's necessary.)
!      */
!     RevalidateCachedQuery(plansource, true, queryEnv);

      /* Get the primary statement and find out what it returns */
      pstmt = QueryListGetPrimaryStmt(plansource->query_list);
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index ab20aa0..4930781 100644
*** a/src/include/utils/plancache.h
--- b/src/include/utils/plancache.h
*************** extern List *CachedPlanGetTargetList(Cac
*** 178,184 ****

  extern CachedPlan *GetCachedPlan(CachedPlanSource *plansource,
                ParamListInfo boundParams,
!               bool useResOwner,
                QueryEnvironment *queryEnv);
  extern void ReleaseCachedPlan(CachedPlan *plan, bool useResOwner);

--- 178,184 ----

  extern CachedPlan *GetCachedPlan(CachedPlanSource *plansource,
                ParamListInfo boundParams,
!               bool useResOwner, bool useNewSnapshot,
                QueryEnvironment *queryEnv);
  extern void ReleaseCachedPlan(CachedPlan *plan, bool useResOwner);

diff --git a/src/test/regress/expected/plpgsql.out b/src/test/regress/expected/plpgsql.out
index 4f9501d..a1ea306 100644
*** a/src/test/regress/expected/plpgsql.out
--- b/src/test/regress/expected/plpgsql.out
*************** select sp_add_user('user3');
*** 2238,2243 ****
--- 2238,2281 ----
  drop function sp_add_user(text);
  drop function sp_id_user(text);
  --
+ -- Variant of above: check for sane behavior during planner's speculative
+ -- execution of stable functions (bug #15060)
+ --
+ create function sp_id_user(a_login text) returns int as $$
+ declare x int;
+ begin
+   select into x id from users where login = a_login;
+   if not found then raise exception '% not found', a_login; end if;
+   return x;
+ end$$ language plpgsql stable;
+ select sp_id_user('user1');
+  sp_id_user
+ ------------
+           1
+ (1 row)
+
+ select sp_id_user('userx');
+ ERROR:  userx not found
+ CONTEXT:  PL/pgSQL function sp_id_user(text) line 5 at RAISE
+ create function sp_add_user(a_login text) returns int as $$
+ declare my_id_user int;
+ begin
+   INSERT INTO users ( login ) VALUES ( a_login );
+   SELECT id INTO my_id_user FROM users WHERE id = sp_id_user( a_login );
+   IF NOT FOUND THEN
+     RETURN -1;  -- error code for insertion failure
+   END IF;
+   RETURN my_id_user;
+ end$$ language plpgsql;
+ select sp_add_user('user4');
+  sp_add_user
+ -------------
+            4
+ (1 row)
+
+ drop function sp_add_user(text);
+ drop function sp_id_user(text);
+ --
  -- tests for refcursors
  --
  create table rc_test (a int, b int);
diff --git a/src/test/regress/sql/plpgsql.sql b/src/test/regress/sql/plpgsql.sql
index 3914651..411b7b5 100644
*** a/src/test/regress/sql/plpgsql.sql
--- b/src/test/regress/sql/plpgsql.sql
*************** drop function sp_add_user(text);
*** 1901,1906 ****
--- 1901,1938 ----
  drop function sp_id_user(text);

  --
+ -- Variant of above: check for sane behavior during planner's speculative
+ -- execution of stable functions (bug #15060)
+ --
+
+ create function sp_id_user(a_login text) returns int as $$
+ declare x int;
+ begin
+   select into x id from users where login = a_login;
+   if not found then raise exception '% not found', a_login; end if;
+   return x;
+ end$$ language plpgsql stable;
+
+ select sp_id_user('user1');
+ select sp_id_user('userx');
+
+ create function sp_add_user(a_login text) returns int as $$
+ declare my_id_user int;
+ begin
+   INSERT INTO users ( login ) VALUES ( a_login );
+   SELECT id INTO my_id_user FROM users WHERE id = sp_id_user( a_login );
+   IF NOT FOUND THEN
+     RETURN -1;  -- error code for insertion failure
+   END IF;
+   RETURN my_id_user;
+ end$$ language plpgsql;
+
+ select sp_add_user('user4');
+
+ drop function sp_add_user(text);
+ drop function sp_id_user(text);
+
+ --
  -- tests for refcursors
  --
  create table rc_test (a int, b int);

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

Предыдущее
От: Tom Lane
Дата:
Сообщение: Re: BUG #15060: Row in table not found when using pg function in an expression
Следующее
От: Tom Lane
Дата:
Сообщение: Re: 答复: response time is very long in PG9.5.5 using psql or jdbc