{"record":{"id":"5ae4e03364d3366c","repo":"gastownhall/beads","slug":"journal-compute-retain-days-floor-w","errorCode":null,"errorMessage":"journal: compute retain-days floor: %w","messagePattern":"journal: compute retain-days floor: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/journal_prune.go","lineNumber":398,"sourceCode":"\t\tvar ceil int64\n\t\tscanErr := tx.QueryRowContext(ctx, EventsPruneRowsCeilQuery(), retainRows).Scan(&ceil)\n\t\tif errors.Is(scanErr, sql.ErrNoRows) {\n\t\t\treturn 0, false, nil\n\t\t}\n\t\tif scanErr != nil {\n\t\t\treturn 0, false, fmt.Errorf(\"journal: compute retain-rows floor: %w\", scanErr)\n\t\t}\n\t\treturn ceil, true, nil\n\t}\n\treadDaysFloor := func(cutoff time.Time) (int64, bool, error) {\n\t\t// MIN over no matching rows yields one NULL row, not ErrNoRows.\n\t\tvar floorSeq sql.NullInt64\n\t\tscanErr := tx.QueryRowContext(ctx, EventsPruneDaysFloorQuery(), cutoff).Scan(&floorSeq)\n\t\tif errors.Is(scanErr, sql.ErrNoRows) {\n\t\t\treturn 0, false, nil\n\t\t}\n\t\tif scanErr != nil {\n\t\t\treturn 0, false, fmt.Errorf(\"journal: compute retain-days floor: %w\", scanErr)\n\t\t}\n\t\tif !floorSeq.Valid {\n\t\t\treturn 0, false, nil\n\t\t}\n\t\treturn floorSeq.Int64, true, nil\n\t}\n\treturn readRowsCeil, readDaysFloor\n}\n\n// PruneEventsInTx deletes journal rows with seq below before, honoring the\n// retain-days and retain-rows floors (0 disables a floor), and returns the\n// number of rows deleted. It runs inside the caller's transaction.\nfunc PruneEventsInTx(ctx context.Context, tx DBTX, before int64, retainDays, retainRows int, now time.Time) (int64, error) {\n\treadRowsCeil, readDaysFloor := eventsPruneFloorReadersInTx(ctx, tx, retainRows)\n\twhere, args, skip, err := ComputeEventsPruneWhere(before, retainDays, retainRows, now, readRowsCeil, readDaysFloor)\n\tif err != nil {\n\t\treturn 0, err\n\t}","sourceCodeStart":380,"sourceCodeEnd":416,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/journal_prune.go#L380-L416","documentation":"In the retain-days pruning strategy, the cutoff time is converted to a floor seq via EventsPruneDaysFloorQuery (MIN(seq) over rows older than cutoff). This error wraps a Scan failure of that query. ErrNoRows and a NULL MIN (no matching rows) are both treated as 'no bound', so this is always a real driver error.","triggerScenarios":"ComputeEventsAutoPruneBoundInTx with a RetainDays policy failing at the floor query: type mismatch comparing the ts column against the cutoff parameter, missing table, connection failure, or driver incompatibility with the date comparison expression.","commonSituations":"Driver/dialect mismatch where the cutoff time cannot be bound or compared (e.g. older Dolt not supporting the DATETIME comparison form); clock/cutoff values far outside stored range triggering edge-case casts; pre-migration DB.","solutions":["Retry; transient connection or lock errors are common wrapped causes.","Verify migrations created bd_events_journal with a normalized ts column (CAST(ts AS CHAR) normalization exists for driver variance).","Check the Dolt/driver version supports the date comparison used by EventsPruneDaysFloorQuery; upgrade if not.","Inspect server logs for the exact underlying SQL error."],"exampleFix":"// before\nstore.ComputeEventsAutoPruneBoundInTx(ctx, tx, PruneOpts{RetainDays: 0}) // nonsensical cutoff\n// after\nstore.ComputeEventsAutoPruneBoundInTx(ctx, tx, PruneOpts{RetainDays: 30}) // sane retention window","handlingStrategy":"validation","validationCode":"if opts.RetainDays <= 0 { return errors.New(\"RetainDays must be positive\") }\nif _, err := db.Query(\"SELECT MIN(seq) FROM bd_events_journal WHERE ts < NOW()\"); err != nil {\n    return fmt.Errorf(\"day-floor query unsupported on this driver: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"journal: compute retain-days floor\") {\n    if isDialectError(err) { return fallbackToRetainRowsPolicy(opts) }\n    return err\n}","preventionTips":["Use a driver/Dolt version tested against the app's date-comparison SQL.","Sanity-check retention settings before pruning.","Prefer retain-rows policy if your driver has date-comparison quirks.","Run a dry-run bound computation before enabling scheduled pruning."],"tags":["storage","database","journal","pruning"],"backgroundTag":"prune-bound-computation-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}