From 3a73207abd9dc27f2962b712dbd9649618a71b0b Mon Sep 17 00:00:00 2001 From: Repinoid Date: Sat, 10 Oct 2026 19:21:47 +0300 Subject: [PATCH] =?UTF-8?q?fix(iot):=20409=20=D1=80=D0=B0=D0=B7=D0=BB?= =?UTF-8?q?=D0=B8=D1=87=D0=B0=D0=B5=D1=82=20=D0=BA=D0=BE=D0=BD=D1=84=D0=BB?= =?UTF-8?q?=D0=B8=D0=BA=D1=82=20name=20vs=20device=5Fid=20(=D1=80=D0=B5?= =?UTF-8?q?=D0=B2=D1=8C=D1=8E)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit store.Create классифицирует 23505 по имени constraint: (namespace,device_id) → ErrDeviceIDExists, иначе ErrAlreadyExists. Handler отдаёт разные 409-сообщения. Помогает Terraform понять, какое поле конфликтует. --- internal/service/api/handler/devices.go | 4 ++++ internal/service/store/device.go | 4 ++++ internal/service/store/devices.go | 10 ++++++++++ 3 files changed, 18 insertions(+) diff --git a/internal/service/api/handler/devices.go b/internal/service/api/handler/devices.go index 1e88604..389b343 100644 --- a/internal/service/api/handler/devices.go +++ b/internal/service/api/handler/devices.go @@ -215,6 +215,10 @@ func (h *Handler) CreateIoTDevice(w http.ResponseWriter, r *http.Request) { } if err := h.Devices.Create(r.Context(), device); err != nil { + if errors.Is(err, store.ErrDeviceIDExists) { + writeJSON(w, http.StatusConflict, errResp("device_id already exists in this namespace")) + return + } if errors.Is(err, store.ErrAlreadyExists) { writeJSON(w, http.StatusConflict, errResp("iot device already exists")) return diff --git a/internal/service/store/device.go b/internal/service/store/device.go index bfd4b34..c0c049e 100644 --- a/internal/service/store/device.go +++ b/internal/service/store/device.go @@ -15,6 +15,10 @@ var ErrNotFound = errors.New("device not found") // ErrAlreadyExists — конфликт уникальности (namespace, name) или (namespace, device_id). var ErrAlreadyExists = errors.New("device already exists") +// ErrDeviceIDExists — конфликт именно по (namespace, device_id) (отличается от +// конфликта по name для понятной ошибки API/Terraform, ревью 2026-10-10). +var ErrDeviceIDExists = errors.New("device_id already exists") + // Device — устройство IoT из таблицы iot_devices. type Device struct { Namespace string diff --git a/internal/service/store/devices.go b/internal/service/store/devices.go index 73cd358..1a1ba87 100644 --- a/internal/service/store/devices.go +++ b/internal/service/store/devices.go @@ -6,6 +6,9 @@ import ( "encoding/json" "errors" "fmt" + "strings" + + "github.com/lib/pq" ) // Create вставляет новое устройство. Конфликт (namespace, name) или @@ -26,6 +29,13 @@ VALUES ($1, $2, $3, $4, $5, $6, $7)`, ) if err != nil { if isUniqueViolation(err) { + // Различаем конфликт name и device_id по имени constraint (ревью + // 2026-10-10): PK — iot_devices_pkey (namespace,name), уникальный + // индекс — idx_iot_devices_ns_device (namespace,device_id). + var pqErr *pq.Error + if errors.As(err, &pqErr) && strings.Contains(pqErr.Constraint, "ns_device") { + return ErrDeviceIDExists + } return ErrAlreadyExists } return fmt.Errorf("store: insert device: %w", err)