plandex-ai/plandex · error

error updating user num_non_draft_plans: %v

Error message

error updating user num_non_draft_plans: %v

What it means

This error wraps a failure to increment the num_non_draft_plans counter for a user inside a transaction. The UPDATE statement on the users table failed, so the transaction caller receives a wrapped sqlx/driver error. It indicates the counter could not be kept in sync when a plan transitions out of draft status.

Source

Thrown at app/server/db/plan_helpers.go:303

	return nil
}

func IncActiveBranches(planId string, inc int, tx *sqlx.Tx) error {
	_, err := tx.Exec("UPDATE plans SET active_branches = active_branches + $1 WHERE id = $2", inc, planId)

	if err != nil {
		return fmt.Errorf("error updating plan active branches: %v", err)
	}

	return nil
}

func IncNumNonDraftPlans(userId string, tx *sqlx.Tx) error {
	_, err := tx.Exec("UPDATE users SET num_non_draft_plans = num_non_draft_plans + 1 WHERE id = $1", userId)

	if err != nil {
		return fmt.Errorf("error updating user num_non_draft_plans: %v", err)
	}

	return nil
}

func StoreDescription(description *ConvoMessageDescription) error {
	descriptionsDir := getPlanDescriptionsDir(description.OrgId, description.PlanId)

	err := os.MkdirAll(descriptionsDir, os.ModePerm)

	if err != nil {
		return fmt.Errorf("error creating convo message descriptions dir: %v", err)
	}

	for _, op := range description.Operations {
		if op.Content != "" {
			quoted := strconv.Quote(op.Content)
			op.Content = quoted[1 : len(quoted)-1]

View on GitHub (pinned to e2d772072e)

Solutions

  1. Check the wrapped %v detail for the underlying Postgres error code and fix the root cause
  2. Ensure the caller did not ignore an earlier error on the same tx (Postgres aborts subsequent statements after one fails)
  3. Verify the users row exists for the userId being incremented
  4. Add connection-pool health checks and reasonable statement_timeout settings
  5. Retry the transaction at the caller level on transient connection errors

Example fix

// before
_, err := tx.Exec("UPDATE users SET num_non_draft_plans = num_non_draft_plans + 1 WHERE id = $1", userId)
if err != nil {
    return fmt.Errorf("error updating user num_non_draft_plans: %v", err)
}
// after
res, err := tx.Exec("UPDATE users SET num_non_draft_plans = num_non_draft_plans + 1 WHERE id = $1", userId)
if err != nil {
    return fmt.Errorf("error updating user num_non_draft_plans: %w", err)
}
if n, _ := res.RowsAffected(); n == 0 {
    return fmt.Errorf("user %s not found", userId)
}
Defensive patterns

Strategy: try-catch

Validate before calling

var exists bool
err := db.Get(&exists, "SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)", userId)
if err != nil || !exists {
    return fmt.Errorf("user %s not found, skipping increment", userId)
}

Try / catch

if err := IncNumNonDraftPlans(userId, tx); err != nil {
    var pqErr *pq.Error
    if errors.As(err, &pqErr) && pqErr.Code == "25P02" {
        // transaction aborted by earlier error; fix caller error handling
    }
    return err
}

Prevention

When it happens

Trigger: tx.Exec("UPDATE users SET num_non_draft_plans = num_non_draft_plans + 1 WHERE id = $1", userId) fails: connection dropped mid-transaction, transaction already aborted by a prior error, invalid user id (no row matched, not an error but worth noting), or Postgres error (lock timeout, dead connection).

Common situations: Long-lived transactions that exceed statement_timeout; a tx passed in after a previous statement failed (Postgres aborts the whole transaction); DB restart or failover during plan creation; passing an empty or stale userId from a deleted user row.

Related errors


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/5dfe7bd199000b1a. Report an issue: GitHub.