package sqlite import ( "database/sql" "fmt" "time" "github.com/google/uuid" ) // Attachment is metadata for a file attached to a ticket. type Attachment struct { ID string `json:"id"` TicketID string `json:"ticketId"` ProjectID string `json:"projectId"` Filename string `json:"filename"` ContentType string `json:"contentType"` SizeBytes int64 `json:"sizeBytes"` CreatedBy string `json:"createdBy"` CreatedAt time.Time `json:"createdAt"` } // AttachmentStore persists attachment metadata. type AttachmentStore struct { db *DB } // NewAttachmentStore returns an attachment metadata store. func NewAttachmentStore(db *DB) *AttachmentStore { return &AttachmentStore{db: db} } const attachmentCols = `id, ticket_id, project_id, filename, content_type, size_bytes, created_by, created_at` func scanAttachment(scanner interface{ Scan(dest ...any) error }) (Attachment, error) { var a Attachment var created string err := scanner.Scan( &a.ID, &a.TicketID, &a.ProjectID, &a.Filename, &a.ContentType, &a.SizeBytes, &a.CreatedBy, &created, ) if err != nil { return Attachment{}, err } a.CreatedAt = parseTime(created) return a, nil } // ListByTicket returns attachments for a ticket, newest first. func (s *AttachmentStore) ListByTicket(ticketID string) ([]Attachment, error) { rows, err := s.db.SQL.Query( `SELECT `+attachmentCols+` FROM ticket_attachments WHERE ticket_id = ? ORDER BY created_at DESC`, ticketID, ) if err != nil { return nil, err } defer rows.Close() var out []Attachment for rows.Next() { a, err := scanAttachment(rows) if err != nil { return nil, err } out = append(out, a) } if out == nil { out = []Attachment{} } return out, rows.Err() } // Get returns one attachment by id. func (s *AttachmentStore) Get(id string) (Attachment, error) { row := s.db.SQL.QueryRow(`SELECT `+attachmentCols+` FROM ticket_attachments WHERE id = ?`, id) a, err := scanAttachment(row) if err == sql.ErrNoRows { return Attachment{}, fmt.Errorf("attachment not found") } return a, err } // Create inserts attachment metadata. func (s *AttachmentStore) Create(a Attachment) (Attachment, error) { if a.ID == "" { a.ID = uuid.NewString() } if a.Filename == "" { return Attachment{}, fmt.Errorf("filename required") } if a.ContentType == "" { a.ContentType = "application/octet-stream" } if a.CreatedAt.IsZero() { a.CreatedAt = time.Now().UTC() } _, err := s.db.SQL.Exec( `INSERT INTO ticket_attachments (`+attachmentCols+`) VALUES (?,?,?,?,?,?,?,?)`, a.ID, a.TicketID, a.ProjectID, a.Filename, a.ContentType, a.SizeBytes, a.CreatedBy, formatTime(a.CreatedAt), ) if err != nil { return Attachment{}, err } return a, nil } // UpdateSize updates size after a replace/overwrite. func (s *AttachmentStore) UpdateSize(id string, sizeBytes int64) error { res, err := s.db.SQL.Exec(`UPDATE ticket_attachments SET size_bytes = ? WHERE id = ?`, sizeBytes, id) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("attachment not found") } return nil } // Delete removes attachment metadata. func (s *AttachmentStore) Delete(id string) error { res, err := s.db.SQL.Exec(`DELETE FROM ticket_attachments WHERE id = ?`, id) if err != nil { return err } n, _ := res.RowsAffected() if n == 0 { return fmt.Errorf("attachment not found") } return nil }