package mailstore import ( "context" "database/sql" "errors" "fmt" "strings" "time" pgtable "galaxy/mail/internal/adapters/postgres/jet/mail/table" "galaxy/mail/internal/domain/attempt" "galaxy/mail/internal/domain/common" deliverydomain "galaxy/mail/internal/domain/delivery" "galaxy/mail/internal/domain/idempotency" pg "github.com/go-jet/jet/v2/postgres" ) // queryable is satisfied by both *sql.DB and *sql.Tx so the row read/write // helpers below run inside or outside an explicit transaction. type queryable interface { ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row } // recipientKind enumerates the supported delivery_recipients.kind values. const ( recipientKindTo = "to" recipientKindCc = "cc" recipientKindBcc = "bcc" recipientKindReplyTo = "reply_to" ) // nextAttemptStatuses lists the delivery statuses for which next_attempt_at is // kept populated. Other statuses store NULL so the partial scheduler index // stays small. var nextAttemptStatuses = map[deliverydomain.Status]struct{}{ deliverydomain.StatusQueued: {}, deliverydomain.StatusRendered: {}, } // deliverySelectColumns is the canonical SELECT list for the deliveries // table, matching scanDelivery's column order. var deliverySelectColumns = pg.ColumnList{ pgtable.Deliveries.DeliveryID, pgtable.Deliveries.ResendParentDeliveryID, pgtable.Deliveries.Source, pgtable.Deliveries.Status, pgtable.Deliveries.PayloadMode, pgtable.Deliveries.TemplateID, pgtable.Deliveries.Locale, pgtable.Deliveries.LocaleFallbackUsed, pgtable.Deliveries.TemplateVariables, pgtable.Deliveries.Attachments, pgtable.Deliveries.Subject, pgtable.Deliveries.TextBody, pgtable.Deliveries.HTMLBody, pgtable.Deliveries.IdempotencyKey, pgtable.Deliveries.RequestFingerprint, pgtable.Deliveries.IdempotencyExpiresAt, pgtable.Deliveries.AttemptCount, pgtable.Deliveries.LastAttemptStatus, pgtable.Deliveries.ProviderSummary, pgtable.Deliveries.NextAttemptAt, pgtable.Deliveries.CreatedAt, pgtable.Deliveries.UpdatedAt, pgtable.Deliveries.SentAt, pgtable.Deliveries.SuppressedAt, pgtable.Deliveries.FailedAt, pgtable.Deliveries.DeadLetteredAt, } // insertDelivery writes one delivery record together with its recipient rows. // idem supplies the request_fingerprint and idempotency_expires_at fields; if // zero-valued (resend), the helper stores an empty fingerprint and uses // fallbackExpiresAt for the idempotency expiry. activeAttempt — when non-nil // and the delivery is queued/rendered — drives the initial next_attempt_at. func insertDelivery(ctx context.Context, q queryable, record deliverydomain.Delivery, idem idempotency.Record, fallbackExpiresAt time.Time, activeAttempt *attempt.Attempt) error { templateVariables, err := marshalTemplateVariables(record.TemplateVariables) if err != nil { return err } attachments, err := marshalAttachments(record.Attachments) if err != nil { return err } requestFingerprint := idem.RequestFingerprint idemExpires := idem.ExpiresAt if idem.IdempotencyKey.IsZero() && idem.Source == "" { requestFingerprint = "" idemExpires = fallbackExpiresAt } stmt := pgtable.Deliveries.INSERT( pgtable.Deliveries.DeliveryID, pgtable.Deliveries.ResendParentDeliveryID, pgtable.Deliveries.Source, pgtable.Deliveries.Status, pgtable.Deliveries.PayloadMode, pgtable.Deliveries.TemplateID, pgtable.Deliveries.Locale, pgtable.Deliveries.LocaleFallbackUsed, pgtable.Deliveries.TemplateVariables, pgtable.Deliveries.Attachments, pgtable.Deliveries.Subject, pgtable.Deliveries.TextBody, pgtable.Deliveries.HTMLBody, pgtable.Deliveries.IdempotencyKey, pgtable.Deliveries.RequestFingerprint, pgtable.Deliveries.IdempotencyExpiresAt, pgtable.Deliveries.AttemptCount, pgtable.Deliveries.LastAttemptStatus, pgtable.Deliveries.ProviderSummary, pgtable.Deliveries.NextAttemptAt, pgtable.Deliveries.CreatedAt, pgtable.Deliveries.UpdatedAt, pgtable.Deliveries.SentAt, pgtable.Deliveries.SuppressedAt, pgtable.Deliveries.FailedAt, pgtable.Deliveries.DeadLetteredAt, ).VALUES( record.DeliveryID.String(), record.ResendParentDeliveryID.String(), string(record.Source), string(record.Status), string(record.PayloadMode), record.TemplateID.String(), record.Locale.String(), record.LocaleFallbackUsed, templateVariables, attachments, record.Content.Subject, record.Content.TextBody, record.Content.HTMLBody, record.IdempotencyKey.String(), requestFingerprint, idemExpires.UTC(), record.AttemptCount, string(record.LastAttemptStatus), record.ProviderSummary, nextAttemptValue(record, activeAttempt), record.CreatedAt.UTC(), record.UpdatedAt.UTC(), nullableTime(record.SentAt), nullableTime(record.SuppressedAt), nullableTime(record.FailedAt), nullableTime(record.DeadLetteredAt), ) query, args := stmt.Sql() if _, err := q.ExecContext(ctx, query, args...); err != nil { return err } return insertRecipients(ctx, q, record.DeliveryID, record.Envelope) } // insertRecipients writes one row per envelope address, preserving the // caller's slice ordering through the position column. func insertRecipients(ctx context.Context, q queryable, deliveryID common.DeliveryID, envelope deliverydomain.Envelope) error { groups := []struct { kind string emails []common.Email }{ {recipientKindTo, envelope.To}, {recipientKindCc, envelope.Cc}, {recipientKindBcc, envelope.Bcc}, {recipientKindReplyTo, envelope.ReplyTo}, } for _, group := range groups { for index, email := range group.emails { stmt := pgtable.DeliveryRecipients.INSERT( pgtable.DeliveryRecipients.DeliveryID, pgtable.DeliveryRecipients.Kind, pgtable.DeliveryRecipients.Position, pgtable.DeliveryRecipients.Email, ).VALUES( deliveryID.String(), group.kind, index, email.String(), ) query, args := stmt.Sql() if _, err := q.ExecContext(ctx, query, args...); err != nil { return fmt.Errorf("insert delivery recipient (%s[%d]): %w", group.kind, index, err) } } } return nil } // updateDelivery writes mutated delivery columns. The set of columns covers // every field that the domain model can change after acceptance: status, // rendered content, attempt metadata, terminal timestamps, plus // next_attempt_at. activeAttempt — when non-nil and the delivery is // queued/rendered — drives the next_attempt_at column; otherwise NULL. func updateDelivery(ctx context.Context, q queryable, record deliverydomain.Delivery, activeAttempt *attempt.Attempt) error { templateVariables, err := marshalTemplateVariables(record.TemplateVariables) if err != nil { return err } attachments, err := marshalAttachments(record.Attachments) if err != nil { return err } stmt := pgtable.Deliveries.UPDATE( pgtable.Deliveries.Status, pgtable.Deliveries.TemplateVariables, pgtable.Deliveries.Attachments, pgtable.Deliveries.Subject, pgtable.Deliveries.TextBody, pgtable.Deliveries.HTMLBody, pgtable.Deliveries.Locale, pgtable.Deliveries.LocaleFallbackUsed, pgtable.Deliveries.AttemptCount, pgtable.Deliveries.LastAttemptStatus, pgtable.Deliveries.ProviderSummary, pgtable.Deliveries.NextAttemptAt, pgtable.Deliveries.UpdatedAt, pgtable.Deliveries.SentAt, pgtable.Deliveries.SuppressedAt, pgtable.Deliveries.FailedAt, pgtable.Deliveries.DeadLetteredAt, ).SET( string(record.Status), templateVariables, attachments, record.Content.Subject, record.Content.TextBody, record.Content.HTMLBody, record.Locale.String(), record.LocaleFallbackUsed, record.AttemptCount, string(record.LastAttemptStatus), record.ProviderSummary, nextAttemptValue(record, activeAttempt), record.UpdatedAt.UTC(), nullableTime(record.SentAt), nullableTime(record.SuppressedAt), nullableTime(record.FailedAt), nullableTime(record.DeadLetteredAt), ).WHERE(pgtable.Deliveries.DeliveryID.EQ(pg.String(record.DeliveryID.String()))) query, args := stmt.Sql() result, err := q.ExecContext(ctx, query, args...) if err != nil { return err } rows, err := result.RowsAffected() if err != nil { return err } if rows == 0 { return fmt.Errorf("update delivery %q: row not found", record.DeliveryID) } return nil } // nextAttemptValue resolves the next_attempt_at column value: the active // attempt's scheduled_for when the delivery is queued/rendered, otherwise // NULL. Other statuses (sending/sent/suppressed/failed/dead_letter/accepted) // store NULL so the partial scheduler index excludes the row. func nextAttemptValue(record deliverydomain.Delivery, activeAttempt *attempt.Attempt) any { if activeAttempt == nil { return nil } if _, ok := nextAttemptStatuses[record.Status]; !ok { return nil } if activeAttempt.Status != attempt.StatusScheduled { return nil } return activeAttempt.ScheduledFor.UTC() } // insertAttempt writes one attempt row. func insertAttempt(ctx context.Context, q queryable, record attempt.Attempt) error { stmt := pgtable.Attempts.INSERT( pgtable.Attempts.DeliveryID, pgtable.Attempts.AttemptNo, pgtable.Attempts.Status, pgtable.Attempts.ScheduledFor, pgtable.Attempts.StartedAt, pgtable.Attempts.FinishedAt, pgtable.Attempts.ProviderClassification, pgtable.Attempts.ProviderSummary, ).VALUES( record.DeliveryID.String(), record.AttemptNo, string(record.Status), record.ScheduledFor.UTC(), nullableTime(record.StartedAt), nullableTime(record.FinishedAt), record.ProviderClassification, record.ProviderSummary, ) query, args := stmt.Sql() _, err := q.ExecContext(ctx, query, args...) return err } // updateAttempt writes mutated attempt fields keyed by (delivery_id, // attempt_no). func updateAttempt(ctx context.Context, q queryable, record attempt.Attempt) error { stmt := pgtable.Attempts.UPDATE( pgtable.Attempts.Status, pgtable.Attempts.ScheduledFor, pgtable.Attempts.StartedAt, pgtable.Attempts.FinishedAt, pgtable.Attempts.ProviderClassification, pgtable.Attempts.ProviderSummary, ).SET( string(record.Status), record.ScheduledFor.UTC(), nullableTime(record.StartedAt), nullableTime(record.FinishedAt), record.ProviderClassification, record.ProviderSummary, ).WHERE(pg.AND( pgtable.Attempts.DeliveryID.EQ(pg.String(record.DeliveryID.String())), pgtable.Attempts.AttemptNo.EQ(pg.Int(int64(record.AttemptNo))), )) query, args := stmt.Sql() result, err := q.ExecContext(ctx, query, args...) if err != nil { return err } rows, err := result.RowsAffected() if err != nil { return err } if rows == 0 { return fmt.Errorf("update attempt %q/%d: row not found", record.DeliveryID, record.AttemptNo) } return nil } // insertDeadLetter writes the dead_letters row for a delivery that exhausted // retries. func insertDeadLetter(ctx context.Context, q queryable, entry deliverydomain.DeadLetterEntry) error { stmt := pgtable.DeadLetters.INSERT( pgtable.DeadLetters.DeliveryID, pgtable.DeadLetters.FinalAttemptNo, pgtable.DeadLetters.FailureClassification, pgtable.DeadLetters.ProviderSummary, pgtable.DeadLetters.RecoveryHint, pgtable.DeadLetters.CreatedAt, ).VALUES( entry.DeliveryID.String(), entry.FinalAttemptNo, entry.FailureClassification, entry.ProviderSummary, entry.RecoveryHint, entry.CreatedAt.UTC(), ) query, args := stmt.Sql() _, err := q.ExecContext(ctx, query, args...) return err } // scanDeliveryRow scans the columns produced by selectColumns into a // deliverydomain.Delivery + the auxiliary idempotency fingerprint/expiry // values. The auxiliary fields are returned alongside so callers can // translate them into idempotency.Record where needed. type deliveryAux struct { RequestFingerprint string IdempotencyExpiresAt time.Time NextAttemptAt *time.Time } func scanDelivery(row interface { Scan(dest ...any) error }) (deliverydomain.Delivery, deliveryAux, error) { var ( record deliverydomain.Delivery resendParent string source string status string payloadMode string templateID string locale string templateVariables []byte attachments []byte idempotencyKey string lastAttemptStatusStr string nextAttemptAt *time.Time sentAt *time.Time suppressedAt *time.Time failedAt *time.Time deadLetteredAt *time.Time idemExpiresAt time.Time requestFingerprint string ) if err := row.Scan( (*string)(&record.DeliveryID), &resendParent, &source, &status, &payloadMode, &templateID, &locale, &record.LocaleFallbackUsed, &templateVariables, &attachments, &record.Content.Subject, &record.Content.TextBody, &record.Content.HTMLBody, &idempotencyKey, &requestFingerprint, &idemExpiresAt, &record.AttemptCount, &lastAttemptStatusStr, &record.ProviderSummary, &nextAttemptAt, &record.CreatedAt, &record.UpdatedAt, &sentAt, &suppressedAt, &failedAt, &deadLetteredAt, ); err != nil { return deliverydomain.Delivery{}, deliveryAux{}, err } record.ResendParentDeliveryID = common.DeliveryID(resendParent) record.Source = deliverydomain.Source(source) record.Status = deliverydomain.Status(status) record.PayloadMode = deliverydomain.PayloadMode(payloadMode) record.TemplateID = common.TemplateID(templateID) record.Locale = common.Locale(locale) record.IdempotencyKey = common.IdempotencyKey(idempotencyKey) record.LastAttemptStatus = attempt.Status(lastAttemptStatusStr) record.CreatedAt = record.CreatedAt.UTC() record.UpdatedAt = record.UpdatedAt.UTC() record.SentAt = timeFromNullable(sentAt) record.SuppressedAt = timeFromNullable(suppressedAt) record.FailedAt = timeFromNullable(failedAt) record.DeadLetteredAt = timeFromNullable(deadLetteredAt) if templateVariables != nil { variables, err := unmarshalTemplateVariables(templateVariables) if err != nil { return deliverydomain.Delivery{}, deliveryAux{}, err } record.TemplateVariables = variables } atts, err := unmarshalAttachments(attachments) if err != nil { return deliverydomain.Delivery{}, deliveryAux{}, err } record.Attachments = atts return record, deliveryAux{ RequestFingerprint: requestFingerprint, IdempotencyExpiresAt: idemExpiresAt.UTC(), NextAttemptAt: timeFromNullable(nextAttemptAt), }, nil } // loadEnvelope materialises the four envelope groups for one delivery. func loadEnvelope(ctx context.Context, q queryable, deliveryID common.DeliveryID) (deliverydomain.Envelope, error) { stmt := pg.SELECT( pgtable.DeliveryRecipients.Kind, pgtable.DeliveryRecipients.Position, pgtable.DeliveryRecipients.Email, ).FROM(pgtable.DeliveryRecipients). WHERE(pgtable.DeliveryRecipients.DeliveryID.EQ(pg.String(deliveryID.String()))). ORDER_BY(pgtable.DeliveryRecipients.Kind.ASC(), pgtable.DeliveryRecipients.Position.ASC()) query, args := stmt.Sql() rows, err := q.QueryContext(ctx, query, args...) if err != nil { return deliverydomain.Envelope{}, err } defer rows.Close() var envelope deliverydomain.Envelope for rows.Next() { var ( kind string position int email string ) if err := rows.Scan(&kind, &position, &email); err != nil { return deliverydomain.Envelope{}, err } switch kind { case recipientKindTo: envelope.To = append(envelope.To, common.Email(email)) case recipientKindCc: envelope.Cc = append(envelope.Cc, common.Email(email)) case recipientKindBcc: envelope.Bcc = append(envelope.Bcc, common.Email(email)) case recipientKindReplyTo: envelope.ReplyTo = append(envelope.ReplyTo, common.Email(email)) default: return deliverydomain.Envelope{}, fmt.Errorf("load envelope: unknown recipient kind %q", kind) } } if err := rows.Err(); err != nil { return deliverydomain.Envelope{}, err } return envelope, nil } // loadDeliveryByID returns the delivery referenced by deliveryID along with // its full envelope. Returns (Delivery{}, false, nil) when the row does not // exist. func loadDeliveryByID(ctx context.Context, q queryable, deliveryID common.DeliveryID) (deliverydomain.Delivery, bool, error) { stmt := pg.SELECT(deliverySelectColumns). FROM(pgtable.Deliveries). WHERE(pgtable.Deliveries.DeliveryID.EQ(pg.String(deliveryID.String()))) query, args := stmt.Sql() row := q.QueryRowContext(ctx, query, args...) record, _, err := scanDelivery(row) switch { case errors.Is(err, sql.ErrNoRows): return deliverydomain.Delivery{}, false, nil case err != nil: return deliverydomain.Delivery{}, false, err } envelope, err := loadEnvelope(ctx, q, deliveryID) if err != nil { return deliverydomain.Delivery{}, false, err } record.Envelope = envelope return record, true, nil } // loadIdempotencyByScope returns the idempotency.Record for (source, key). // Returns (Record{}, false, nil) when no delivery owns the scope. func loadIdempotencyByScope(ctx context.Context, q queryable, source deliverydomain.Source, key common.IdempotencyKey) (idempotency.Record, bool, error) { stmt := pg.SELECT( pgtable.Deliveries.DeliveryID, pgtable.Deliveries.RequestFingerprint, pgtable.Deliveries.IdempotencyExpiresAt, pgtable.Deliveries.CreatedAt, ).FROM(pgtable.Deliveries). WHERE(pg.AND( pgtable.Deliveries.Source.EQ(pg.String(string(source))), pgtable.Deliveries.IdempotencyKey.EQ(pg.String(key.String())), )) query, args := stmt.Sql() row := q.QueryRowContext(ctx, query, args...) var ( deliveryID string requestFingerprint string expiresAt time.Time createdAt time.Time ) if err := row.Scan(&deliveryID, &requestFingerprint, &expiresAt, &createdAt); err != nil { if errors.Is(err, sql.ErrNoRows) { return idempotency.Record{}, false, nil } return idempotency.Record{}, false, err } if strings.TrimSpace(requestFingerprint) == "" { // Resend / non-idempotent rows expose an empty fingerprint; the // reservation is not idempotency-scoped and must not surface as a hit. return idempotency.Record{}, false, nil } return idempotency.Record{ Source: source, IdempotencyKey: key, DeliveryID: common.DeliveryID(deliveryID), RequestFingerprint: requestFingerprint, CreatedAt: createdAt.UTC(), ExpiresAt: expiresAt.UTC(), }, true, nil } // loadAttempts returns the attempts of deliveryID in attempt_no ASC order. // expectedCount lets the caller fail closed when the stored sequence has a // gap. func loadAttempts(ctx context.Context, q queryable, deliveryID common.DeliveryID, expectedCount int) ([]attempt.Attempt, error) { stmt := pg.SELECT( pgtable.Attempts.AttemptNo, pgtable.Attempts.Status, pgtable.Attempts.ScheduledFor, pgtable.Attempts.StartedAt, pgtable.Attempts.FinishedAt, pgtable.Attempts.ProviderClassification, pgtable.Attempts.ProviderSummary, ).FROM(pgtable.Attempts). WHERE(pgtable.Attempts.DeliveryID.EQ(pg.String(deliveryID.String()))). ORDER_BY(pgtable.Attempts.AttemptNo.ASC()) query, args := stmt.Sql() rows, err := q.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() out := make([]attempt.Attempt, 0, expectedCount) for rows.Next() { var ( attemptNo int status string scheduledFor time.Time startedAt *time.Time finishedAt *time.Time providerClassification string providerSummary string ) if err := rows.Scan( &attemptNo, &status, &scheduledFor, &startedAt, &finishedAt, &providerClassification, &providerSummary, ); err != nil { return nil, err } out = append(out, attempt.Attempt{ DeliveryID: deliveryID, AttemptNo: attemptNo, Status: attempt.Status(status), ScheduledFor: scheduledFor.UTC(), StartedAt: timeFromNullable(startedAt), FinishedAt: timeFromNullable(finishedAt), ProviderClassification: providerClassification, ProviderSummary: providerSummary, }) } if err := rows.Err(); err != nil { return nil, err } if expectedCount >= 0 && len(out) != expectedCount { return nil, fmt.Errorf("load attempts %q: expected %d, got %d", deliveryID, expectedCount, len(out)) } for index, record := range out { if record.AttemptNo != index+1 { return nil, fmt.Errorf("load attempts %q: gap at attempt %d", deliveryID, index+1) } } return out, nil } // loadDeadLetter returns the dead_letters row keyed by deliveryID. func loadDeadLetter(ctx context.Context, q queryable, deliveryID common.DeliveryID) (deliverydomain.DeadLetterEntry, bool, error) { stmt := pg.SELECT( pgtable.DeadLetters.FinalAttemptNo, pgtable.DeadLetters.FailureClassification, pgtable.DeadLetters.ProviderSummary, pgtable.DeadLetters.RecoveryHint, pgtable.DeadLetters.CreatedAt, ).FROM(pgtable.DeadLetters). WHERE(pgtable.DeadLetters.DeliveryID.EQ(pg.String(deliveryID.String()))) query, args := stmt.Sql() row := q.QueryRowContext(ctx, query, args...) var ( finalAttemptNo int failureClassification string providerSummary string recoveryHint string createdAt time.Time ) if err := row.Scan(&finalAttemptNo, &failureClassification, &providerSummary, &recoveryHint, &createdAt); err != nil { if errors.Is(err, sql.ErrNoRows) { return deliverydomain.DeadLetterEntry{}, false, nil } return deliverydomain.DeadLetterEntry{}, false, err } return deliverydomain.DeadLetterEntry{ DeliveryID: deliveryID, FinalAttemptNo: finalAttemptNo, FailureClassification: failureClassification, ProviderSummary: providerSummary, RecoveryHint: recoveryHint, CreatedAt: createdAt.UTC(), }, true, nil } // lockDelivery acquires a row-level lock on the deliveries row keyed by // deliveryID for the lifetime of the surrounding transaction. func lockDelivery(ctx context.Context, q queryable, deliveryID common.DeliveryID) error { stmt := pg.SELECT(pgtable.Deliveries.DeliveryID). FROM(pgtable.Deliveries). WHERE(pgtable.Deliveries.DeliveryID.EQ(pg.String(deliveryID.String()))). FOR(pg.UPDATE()) query, args := stmt.Sql() row := q.QueryRowContext(ctx, query, args...) var ignored string if err := row.Scan(&ignored); err != nil { if errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("lock delivery %q: not found", deliveryID) } return fmt.Errorf("lock delivery %q: %w", deliveryID, err) } return nil } // loadActiveAttempt returns the attempt row identified by expectedAttemptNo. // When expectedAttemptNo is zero, the helper falls back to the most-recent // attempt (used by call sites that do not yet know the count). func loadActiveAttempt(ctx context.Context, q queryable, deliveryID common.DeliveryID, expectedAttemptNo int) (attempt.Attempt, error) { selectColumns := []pg.Projection{ pgtable.Attempts.AttemptNo, pgtable.Attempts.Status, pgtable.Attempts.ScheduledFor, pgtable.Attempts.StartedAt, pgtable.Attempts.FinishedAt, pgtable.Attempts.ProviderClassification, pgtable.Attempts.ProviderSummary, } var stmt pg.SelectStatement if expectedAttemptNo > 0 { stmt = pg.SELECT(selectColumns[0], selectColumns[1:]...). FROM(pgtable.Attempts). WHERE(pg.AND( pgtable.Attempts.DeliveryID.EQ(pg.String(deliveryID.String())), pgtable.Attempts.AttemptNo.EQ(pg.Int(int64(expectedAttemptNo))), )) } else { stmt = pg.SELECT(selectColumns[0], selectColumns[1:]...). FROM(pgtable.Attempts). WHERE(pgtable.Attempts.DeliveryID.EQ(pg.String(deliveryID.String()))). ORDER_BY(pgtable.Attempts.AttemptNo.DESC()). LIMIT(1) } query, args := stmt.Sql() row := q.QueryRowContext(ctx, query, args...) var ( attemptNo int status string scheduledFor time.Time startedAt *time.Time finishedAt *time.Time providerClassification string providerSummary string ) if err := row.Scan(&attemptNo, &status, &scheduledFor, &startedAt, &finishedAt, &providerClassification, &providerSummary); err != nil { return attempt.Attempt{}, err } return attempt.Attempt{ DeliveryID: deliveryID, AttemptNo: attemptNo, Status: attempt.Status(status), ScheduledFor: scheduledFor.UTC(), StartedAt: timeFromNullable(startedAt), FinishedAt: timeFromNullable(finishedAt), ProviderClassification: providerClassification, ProviderSummary: providerSummary, }, nil } // DeleteDeliveriesOlderThan removes deliveries whose created_at predates // cutoff. Cascading FKs drop the related attempts/dead_letters/payloads/ // recipients automatically. The helper satisfies SQLRetentionStore. func (store *Store) DeleteDeliveriesOlderThan(ctx context.Context, cutoff time.Time) (int64, error) { if store == nil { return 0, errors.New("delete deliveries: nil store") } operationCtx, cancel, err := store.operationContext(ctx, "delete deliveries") if err != nil { return 0, err } defer cancel() stmt := pgtable.Deliveries.DELETE(). WHERE(pgtable.Deliveries.CreatedAt.LT(pg.TimestampzT(cutoff.UTC()))) query, args := stmt.Sql() result, err := store.db.ExecContext(operationCtx, query, args...) if err != nil { return 0, fmt.Errorf("delete deliveries: %w", err) } rows, err := result.RowsAffected() if err != nil { return 0, fmt.Errorf("delete deliveries: rows affected: %w", err) } return rows, nil } // loadDeliveryPayload returns the payload bundle for deliveryID. func loadDeliveryPayload(ctx context.Context, q queryable, deliveryID common.DeliveryID) ([]byte, bool, error) { stmt := pg.SELECT(pgtable.DeliveryPayloads.Payload). FROM(pgtable.DeliveryPayloads). WHERE(pgtable.DeliveryPayloads.DeliveryID.EQ(pg.String(deliveryID.String()))) query, args := stmt.Sql() row := q.QueryRowContext(ctx, query, args...) var payload []byte if err := row.Scan(&payload); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, false, nil } return nil, false, err } return payload, true, nil }