fix(store): provider CreateProvider uses UPSERT to handle orphaned duplicates (#295)

CreateProvider now uses ON CONFLICT (tenant_id, name) DO UPDATE instead of
plain INSERT. When a provider with the same name already exists (e.g. orphaned
after agent deletion), it updates the existing record instead of failing with
a unique constraint violation.

Applied to both PG and SQLite implementations.
This commit is contained in:
viettranx committed 2026-03-30 14:29:33 +07:00
1 parent bdfc0fadfb
commit 4ac611530a
2 files changed
+30 -6

No files matched your search

+14 -3
View File
@@ -52,11 +52,22 @@ func (s *PGProviderStore) CreateProvider(ctx context.Context, p *store.LLMProvid
p.UpdatedAt = now
tid := tenantIDForInsert(ctx)
p.TenantID = tid
_, err := s.db.ExecContext(ctx,
// UPSERT: if provider with same (tenant_id, name) exists, update it and return its ID.
// This handles orphaned providers left after agent deletion (#295).
var actualID uuid.UUID
err := s.db.QueryRowContext(ctx,
`INSERT INTO llm_providers (id, name, display_name, provider_type, api_base, api_key, enabled, settings, created_at, updated_at, tenant_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (tenant_id, name) DO UPDATE SET
display_name = EXCLUDED.display_name, provider_type = EXCLUDED.provider_type,
api_base = EXCLUDED.api_base, api_key = EXCLUDED.api_key,
enabled = EXCLUDED.enabled, settings = EXCLUDED.settings, updated_at = EXCLUDED.updated_at
RETURNING id`,
p.ID, p.Name, p.DisplayName, p.ProviderType, p.APIBase, apiKey, p.Enabled, settings, now, now, tid,
)
).Scan(&actualID)
if err == nil {
p.ID = actualID // sync in-memory ID with actual DB row
}
return err
}
+16 -3
View File
@@ -54,11 +54,24 @@ func (s *SQLiteProviderStore) CreateProvider(ctx context.Context, p *store.LLMPr
p.UpdatedAt = now
tid := tenantIDForInsert(ctx)
p.TenantID = tid
_, err := s.db.ExecContext(ctx,
// UPSERT: if provider with same (tenant_id, name) exists, update it and return its ID.
// This handles orphaned providers left after agent deletion (#295).
var actualID string
err := s.db.QueryRowContext(ctx,
`INSERT INTO llm_providers (id, name, display_name, provider_type, api_base, api_key, enabled, settings, created_at, updated_at, tenant_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(tenant_id, name) DO UPDATE SET
display_name = excluded.display_name, provider_type = excluded.provider_type,
api_base = excluded.api_base, api_key = excluded.api_key,
enabled = excluded.enabled, settings = excluded.settings, updated_at = excluded.updated_at
RETURNING id`,
p.ID, p.Name, p.DisplayName, p.ProviderType, p.APIBase, apiKey, p.Enabled, settings, now, now, tid,
)
).Scan(&actualID)
if err == nil {
if parsed, parseErr := uuid.Parse(actualID); parseErr == nil {
p.ID = parsed // sync in-memory ID with actual DB row
}
}
return err
}