Changeset 0.22.4 (#146)

This commit is contained in:
2026-03-02 22:16:08 +00:00
parent 9940fb5831
commit 3953dcf364
25 changed files with 2254 additions and 145 deletions

View File

@@ -15,18 +15,19 @@ type AttachmentStore struct{}
func NewAttachmentStore() *AttachmentStore { return &AttachmentStore{} }
// ── columns shared across queries ──────────
const attachmentCols = `id, channel_id, user_id, message_id, filename, content_type,
const attachmentCols = `id, channel_id, user_id, message_id, project_id, filename, content_type,
size_bytes, storage_key, extracted_text, metadata, created_at`
// scanAttachment scans a row into an Attachment struct.
func scanAttachment(row interface{ Scan(dest ...interface{}) error }) (*models.Attachment, error) {
var a models.Attachment
var messageID sql.NullString
var projectID sql.NullString
var extractedText sql.NullString
var metadataJSON []byte
err := row.Scan(
&a.ID, &a.ChannelID, &a.UserID, &messageID,
&a.ID, &a.ChannelID, &a.UserID, &messageID, &projectID,
&a.Filename, &a.ContentType, &a.SizeBytes,
&a.StorageKey, &extractedText, &metadataJSON, st(&a.CreatedAt),
)
@@ -35,6 +36,7 @@ func scanAttachment(row interface{ Scan(dest ...interface{}) error }) (*models.A
}
a.MessageID = NullableStringPtr(messageID)
a.ProjectID = NullableStringPtr(projectID)
if extractedText.Valid {
a.ExtractedText = &extractedText.String
}
@@ -48,10 +50,11 @@ func (s *AttachmentStore) Create(ctx context.Context, a *models.Attachment) erro
a.ID = store.NewID()
a.CreatedAt = time.Now().UTC()
_, err := DB.ExecContext(ctx, `
INSERT INTO attachments (id, channel_id, user_id, message_id, filename, content_type,
INSERT INTO attachments (id, channel_id, user_id, message_id, project_id, filename, content_type,
size_bytes, storage_key, extracted_text, metadata, created_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?)`,
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
a.ID, a.ChannelID, a.UserID, models.NullString(a.MessageID),
models.NullString(a.ProjectID),
a.Filename, a.ContentType, a.SizeBytes,
a.StorageKey, models.NullString(a.ExtractedText),
ToJSON(a.Metadata), a.CreatedAt.Format(timeFmt),
@@ -102,6 +105,26 @@ func (s *AttachmentStore) GetByMessage(ctx context.Context, messageID string) ([
return out, rows.Err()
}
// GetByProject returns all attachments for a project (v0.22.4).
func (s *AttachmentStore) GetByProject(ctx context.Context, projectID string) ([]models.Attachment, error) {
rows, err := DB.QueryContext(ctx,
`SELECT `+attachmentCols+` FROM attachments WHERE project_id = ? ORDER BY created_at`, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []models.Attachment
for rows.Next() {
a, err := scanAttachment(rows)
if err != nil {
return nil, err
}
out = append(out, *a)
}
return out, rows.Err()
}
func (s *AttachmentStore) SetMessageID(ctx context.Context, attachmentID, messageID string) error {
_, err := DB.ExecContext(ctx,
`UPDATE attachments SET message_id = ? WHERE id = ?`,

View File

@@ -18,20 +18,21 @@ func (s *HealthStore) UpsertWindow(ctx context.Context, w *models.ProviderHealth
windowStr := w.WindowStart.Format(timeFmt)
_, err := DB.ExecContext(ctx, `
INSERT INTO provider_health (id, provider_config_id, window_start,
request_count, error_count, timeout_count,
request_count, error_count, timeout_count, rate_limit_count,
total_latency_ms, max_latency_ms, last_error, last_error_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
ON CONFLICT (provider_config_id, window_start) DO UPDATE SET
request_count = provider_health.request_count + excluded.request_count,
error_count = provider_health.error_count + excluded.error_count,
timeout_count = provider_health.timeout_count + excluded.timeout_count,
request_count = provider_health.request_count + excluded.request_count,
error_count = provider_health.error_count + excluded.error_count,
timeout_count = provider_health.timeout_count + excluded.timeout_count,
rate_limit_count = provider_health.rate_limit_count + excluded.rate_limit_count,
total_latency_ms = provider_health.total_latency_ms + excluded.total_latency_ms,
max_latency_ms = MAX(provider_health.max_latency_ms, excluded.max_latency_ms),
last_error = COALESCE(excluded.last_error, provider_health.last_error),
last_error_at = COALESCE(excluded.last_error_at, provider_health.last_error_at),
updated_at = datetime('now')
max_latency_ms = MAX(provider_health.max_latency_ms, excluded.max_latency_ms),
last_error = COALESCE(excluded.last_error, provider_health.last_error),
last_error_at = COALESCE(excluded.last_error_at, provider_health.last_error_at),
updated_at = datetime('now')
`, id, w.ProviderConfigID, windowStr,
w.RequestCount, w.ErrorCount, w.TimeoutCount,
w.RequestCount, w.ErrorCount, w.TimeoutCount, w.RateLimitCount,
w.TotalLatencyMs, w.MaxLatencyMs, w.LastError, w.LastErrorAt,
)
return err
@@ -43,13 +44,13 @@ func (s *HealthStore) GetCurrentWindow(ctx context.Context, providerConfigID str
var windowStartStr string
err := DB.QueryRowContext(ctx, `
SELECT id, provider_config_id, window_start,
request_count, error_count, timeout_count,
request_count, error_count, timeout_count, rate_limit_count,
total_latency_ms, max_latency_ms, last_error, last_error_at
FROM provider_health
WHERE provider_config_id = ? AND window_start = ?
`, providerConfigID, windowStr).Scan(
&w.ID, &w.ProviderConfigID, &windowStartStr,
&w.RequestCount, &w.ErrorCount, &w.TimeoutCount,
&w.RequestCount, &w.ErrorCount, &w.TimeoutCount, &w.RateLimitCount,
&w.TotalLatencyMs, &w.MaxLatencyMs, &w.LastError, &w.LastErrorAt,
)
if err == sql.ErrNoRows {
@@ -65,7 +66,7 @@ func (s *HealthStore) GetCurrentWindow(ctx context.Context, providerConfigID str
func (s *HealthStore) ListWindows(ctx context.Context, providerConfigID string, hours int) ([]models.ProviderHealthWindow, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, provider_config_id, window_start,
request_count, error_count, timeout_count,
request_count, error_count, timeout_count, rate_limit_count,
total_latency_ms, max_latency_ms, last_error, last_error_at
FROM provider_health
WHERE provider_config_id = ?
@@ -83,7 +84,7 @@ func (s *HealthStore) ListAllCurrent(ctx context.Context) ([]models.ProviderHeal
windowStr := time.Now().UTC().Truncate(time.Hour).Format(timeFmt)
rows, err := DB.QueryContext(ctx, `
SELECT id, provider_config_id, window_start,
request_count, error_count, timeout_count,
request_count, error_count, timeout_count, rate_limit_count,
total_latency_ms, max_latency_ms, last_error, last_error_at
FROM provider_health
WHERE window_start = ?
@@ -103,9 +104,72 @@ func (s *HealthStore) Prune(ctx context.Context, before time.Time) (int64, error
if err != nil {
return 0, err
}
DB.ExecContext(ctx, `DELETE FROM tool_health WHERE window_start < ?`, before.Format(timeFmt))
return result.RowsAffected()
}
// ── Tool Health (v0.22.4) ────────────────────
func (s *HealthStore) UpsertToolWindow(ctx context.Context, w *models.ToolHealthWindow) error {
id := store.NewID()
windowStr := w.WindowStart.Format(timeFmt)
_, err := DB.ExecContext(ctx, `
INSERT INTO tool_health (id, tool_name, window_start,
request_count, error_count, total_latency_ms, max_latency_ms,
last_error, last_error_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
ON CONFLICT (tool_name, window_start) DO UPDATE SET
request_count = tool_health.request_count + excluded.request_count,
error_count = tool_health.error_count + excluded.error_count,
total_latency_ms = tool_health.total_latency_ms + excluded.total_latency_ms,
max_latency_ms = MAX(tool_health.max_latency_ms, excluded.max_latency_ms),
last_error = COALESCE(excluded.last_error, tool_health.last_error),
last_error_at = COALESCE(excluded.last_error_at, tool_health.last_error_at),
updated_at = datetime('now')
`, id, w.ToolName, windowStr,
w.RequestCount, w.ErrorCount, w.TotalLatencyMs, w.MaxLatencyMs,
w.LastError, w.LastErrorAt,
)
return err
}
func (s *HealthStore) ListAllToolCurrent(ctx context.Context) ([]models.ToolHealthWindow, error) {
windowStr := time.Now().UTC().Truncate(time.Hour).Format(timeFmt)
rows, err := DB.QueryContext(ctx, `
SELECT id, tool_name, window_start,
request_count, error_count, total_latency_ms, max_latency_ms,
last_error, last_error_at
FROM tool_health
WHERE window_start = ?
ORDER BY tool_name
`, windowStr)
if err != nil {
return nil, err
}
defer rows.Close()
var result []models.ToolHealthWindow
for rows.Next() {
var w models.ToolHealthWindow
var windowStartStr string
if err := rows.Scan(
&w.ID, &w.ToolName, &windowStartStr,
&w.RequestCount, &w.ErrorCount, &w.TotalLatencyMs, &w.MaxLatencyMs,
&w.LastError, &w.LastErrorAt,
); err != nil {
return nil, err
}
w.WindowStart, _ = time.Parse(timeFmt, windowStartStr)
result = append(result, w)
}
return result, rows.Err()
}
func (s *HealthStore) DeactivateProvider(ctx context.Context, configID string) error {
_, err := DB.ExecContext(ctx, `UPDATE provider_configs SET is_active = 0 WHERE id = ?`, configID)
return err
}
func scanHealthRows(rows *sql.Rows) ([]models.ProviderHealthWindow, error) {
var result []models.ProviderHealthWindow
for rows.Next() {
@@ -113,7 +177,7 @@ func scanHealthRows(rows *sql.Rows) ([]models.ProviderHealthWindow, error) {
var windowStartStr string
if err := rows.Scan(
&w.ID, &w.ProviderConfigID, &windowStartStr,
&w.RequestCount, &w.ErrorCount, &w.TimeoutCount,
&w.RequestCount, &w.ErrorCount, &w.TimeoutCount, &w.RateLimitCount,
&w.TotalLatencyMs, &w.MaxLatencyMs, &w.LastError, &w.LastErrorAt,
); err != nil {
return nil, err