tools/createuser: report which field collided on insert failure

ON CONFLICT (id) DO NOTHING only catches the id itself, so every other
failure (duplicate username/phone/signup_email) was reported as a generic
"already exists (or insert failed)" - not useful for telling apart the four
distinct causes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Astra 2026-09-13 22:07:28 +01:00
parent 3ae27c435d
commit 61f6685603

View file

@ -27,11 +27,14 @@ import (
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/binary" "encoding/binary"
"errors"
"flag" "flag"
"fmt" "fmt"
"os" "os"
"time" "time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"telesrv/internal/config" "telesrv/internal/config"
@ -124,7 +127,7 @@ func main() {
var createdID int64 var createdID int64
if err := row.Scan(&createdID); err != nil { if err := row.Scan(&createdID); err != nil {
fmt.Fprintf(os.Stderr, "createuser: id %d already exists (or insert failed): %v\n", *id, err) fmt.Fprintln(os.Stderr, describeInsertFailure(*id, *username, displayPhone, signupEmail, err))
os.Exit(1) os.Exit(1)
} }
@ -132,6 +135,31 @@ func main() {
createdID, accessHash, *firstName, *lastName, *username, displayPhone, signupEmail) createdID, accessHash, *firstName, *lastName, *username, displayPhone, signupEmail)
} }
// describeInsertFailure turns the INSERT's failure into a message naming the
// actual thing that collided, instead of "id already exists" for every case:
// ON CONFLICT (id) DO NOTHING only covers the id itself, so a duplicate
// username/phone/signup_email surfaces here as a distinct unique-violation
// error (pgx.ErrNoRows only means the id itself was the conflict).
func describeInsertFailure(id int64, username, phone, signupEmail string, err error) string {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
switch pgErr.ConstraintName {
case "users_username_lower_unique_idx":
return fmt.Sprintf("createuser: username %q is already taken", username)
case "users_phone_unique_idx":
return fmt.Sprintf("createuser: phone %q is already in use", phone)
case "users_signup_email_lower_unique_idx":
return fmt.Sprintf("createuser: email %q is already in use by another account", signupEmail)
default:
return fmt.Sprintf("createuser: unique constraint %q violated: %v", pgErr.ConstraintName, err)
}
}
if errors.Is(err, pgx.ErrNoRows) {
return fmt.Sprintf("createuser: id %d already exists", id)
}
return fmt.Sprintf("createuser: insert failed: %v", err)
}
// assignEmailSignupDisplayPhone mirrors internal/app/auth/service.go's method // assignEmailSignupDisplayPhone mirrors internal/app/auth/service.go's method
// of the same name: pick a random "888"-prefixed display phone and re-roll on // of the same name: pick a random "888"-prefixed display phone and re-roll on
// the astronomically unlikely collision with an existing account's phone. // the astronomically unlikely collision with an existing account's phone.