perf(#1740): replace idx_tx_last_seen with partial index WHERE last_seen=0 (#1756)

Fixes #1740.

## What

Replace the full `idx_tx_last_seen` with a partial index `WHERE
last_seen=0`. Two ordered, sequential migrations in
`internal/dbschema/dbschema.go::ensureTransmissionsLastSeenColumn`:

- **(a)** `CREATE INDEX IF NOT EXISTS idx_tx_last_seen_zero ON
transmissions(id) WHERE last_seen=0`
- **(b)** `DROP INDEX IF EXISTS idx_tx_last_seen` — gated after (a)
succeeds (sequential `Exec`; (b) never runs if (a) errors)

## Why

The only consumer is `chunkedTxLastSeenBackfill`'s `WHERE last_seen=0`
scan + `MAX(id)` lookup. The full index covers ALL rows including the
long tail where `last_seen != 0` after backfill converges (71K+ rows in
prod per carmack's #1740 note). The partial index degenerates to ~the
count of un-backfilled rows (0 in steady state, bounded by ingest rate
during ops) and stops competing for page cache.

## Migration cost

Both migrations are sync and annotated `PREFLIGHT: async=false`:
- (a) `CREATE INDEX` on partial subset `WHERE last_seen=0` is bounded by
un-backfilled rows — a small superset of the inflight ingest window, not
a full table scan.
- (b) `DROP INDEX` is a metadata-only schema rewrite in SQLite — no row
scan at any size.

`dbschema/` has no access to `Store.RunAsyncMigration` (that helper
lives in `cmd/ingestor/` and is the wrong layer for the schema
source-of-truth per #1321), so sync is the only path here regardless.

## TDD

- **RED** `a529f0f4`: `EXPLAIN QUERY PLAN` test asserting the backfill
`MAX(id) WHERE last_seen=0` query uses `idx_tx_last_seen_zero` + a
second test asserting the legacy `idx_tx_last_seen` is dropped
post-Apply. Both failed on assertion (planner picked the full index;
partial index didn't exist).
- **GREEN** `208fde8a`: add migrations (a) and (b). Both tests pass.

## Files touched

- `internal/dbschema/dbschema.go` — swap the index
- `internal/dbschema/dbschema_test.go` — `EXPLAIN QUERY PLAN` pin + DROP
assertion
- `cmd/ingestor/db.go` — comment refresh only (idx_tx_last_seen →
idx_tx_last_seen_zero)

## Acceptance

- ✅ New partial index created
- ✅ Old full index dropped via gated migration (sequential, order
preserved)
- ✅ Query plan test asserts partial-index usage
- ✅ Existing migration tests still green (`internal/dbschema`,
`cmd/ingestor` TestIssue1690 + applySchema)

---------

Co-authored-by: clawbot <bot@openclaw>
This commit is contained in:
Kpa-clawbotandclawbot authored and GitHub committed 2026-06-20 15:45:28 -07:00
1 parent db5520f70f
commit e465e1c6c6
3 files changed
+114 -7

No files matched your search

+5 -4
View File
@@ -267,10 +267,11 @@ func applySchema(db *sql.DB) error {
CREATE INDEX IF NOT EXISTS idx_transmissions_payload_type ON transmissions(payload_type);
-- idx_transmissions_from_pubkey is created by the from_pubkey_v1
-- migration after the column is added on legacy DBs (#1143).
-- idx_tx_last_seen is created by dbschema.Apply after ensuring
-- the last_seen column exists (#1690) — keep it OUT of this base
-- schema block so legacy DBs (table-exists, column-missing) don't
-- trip on the CREATE INDEX before the ALTER runs.
-- idx_tx_last_seen_zero (partial, WHERE last_seen=0) is created by
-- dbschema.Apply after ensuring the last_seen column exists (#1690,
-- partial-index swap #1740) — keep it OUT of this base schema block
-- so legacy DBs (table-exists, column-missing) don't trip on the
-- CREATE INDEX before the ALTER runs.
-- Mobile client RX coverage: a roaming companion = a mobile observer
-- with a moving GPS position, so it gets its own table rather than
+16 -3
View File
@@ -626,9 +626,22 @@ func ensureTransmissionsLastSeenColumn(rw *sql.DB, logf Logger) error {
}
logf("[dbschema] added last_seen column to transmissions (#1690)")
}
// PREFLIGHT: async=true reason="CREATE INDEX on a single INTEGER column; SQLite scales linearly with row count (~5s for 1.9M rows on prod hardware) and we want the index live before cold-load reads"
if _, err := rw.Exec(`CREATE INDEX IF NOT EXISTS idx_tx_last_seen ON transmissions(last_seen)`); err != nil {
return fmt.Errorf("create idx_tx_last_seen: %w", err)
// #1740 step (a): create the PARTIAL index first. Scans only rows with
// last_seen=0 — i.e. the un-backfilled hot subset the chunked backfill
// MAX(id) lookup walks. Degenerates to near-empty once the backfill
// converges, so it stops competing for page cache.
//
// PREFLIGHT: async=false reason="partial index on `WHERE last_seen=0`; subset is bounded by un-backfilled rows (≤0 in steady state, ≤ingest-rate during ops) — cheap at any scale even on a 1.9M-row prod table"
if _, err := rw.Exec(`CREATE INDEX IF NOT EXISTS idx_tx_last_seen_zero ON transmissions(id) WHERE last_seen=0`); err != nil {
return fmt.Errorf("create idx_tx_last_seen_zero: %w", err)
}
// #1740 step (b): gated DROP of the legacy full index. Only runs AFTER
// step (a) succeeded (sequential — we're past the CREATE above). The
// DROP is a metadata-only schema rewrite in SQLite; no row scan.
//
// PREFLIGHT: async=false reason="DROP INDEX is metadata-only in SQLite (no row scan, no rewrite); safe inline at any DB size"
if _, err := rw.Exec(`DROP INDEX IF EXISTS idx_tx_last_seen`); err != nil {
return fmt.Errorf("drop legacy idx_tx_last_seen: %w", err)
}
return nil
}
+93
View File
@@ -3,6 +3,7 @@ package dbschema
import (
"database/sql"
"path/filepath"
"strings"
"testing"
_ "modernc.org/sqlite"
@@ -160,3 +161,95 @@ func contains(haystack, needle string) bool {
}
return false
}
// TestPartialIdxTxLastSeenZero_BackfillMaxUsesPartialIndex pins the planner
// choice for issue #1740: the backfill MAX lookup
//
// SELECT MAX(id) FROM transmissions WHERE last_seen = 0
//
// (and the chunked-backfill scan that walks WHERE last_seen=0 in id order)
// MUST use the partial index `idx_tx_last_seen_zero` introduced in #1740.
// The pre-fix full `idx_tx_last_seen` covers ALL rows (including the long
// tail where last_seen != 0 after backfill converges), so it grows with
// every transmission ever ingested. The partial index degenerates to the
// "unprocessed" hot subset and stays out of the page cache in steady state.
//
// Failure mode this test guards against:
// - regressing to a full-table SCAN (no index)
// - keeping the legacy full `idx_tx_last_seen` and letting the planner
// reach for it instead of the partial
func TestPartialIdxTxLastSeenZero_BackfillMaxUsesPartialIndex(t *testing.T) {
db := minimalDB(t)
defer db.Close()
if err := Apply(db, nil); err != nil {
t.Fatalf("Apply: %v", err)
}
// Insert a handful of rows so the planner has something to weigh —
// SQLite's planner short-circuits empty tables to "SCAN CONSTANT ROW"
// which would mask a missing/wrong index.
for i := 0; i < 16; i++ {
_, err := db.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, last_seen)
VALUES (?, ?, '2026-01-01T00:00:00Z', ?)`,
"00", "h"+string(rune('a'+i)), int64(i%2)) // half last_seen=0, half last_seen=1
if err != nil {
t.Fatalf("seed insert: %v", err)
}
}
if _, err := db.Exec(`ANALYZE`); err != nil {
t.Fatalf("ANALYZE: %v", err)
}
// The backfill-MAX lookup — the exact shape the chunked backfill uses
// to find the next batch of unprocessed ids.
rows, err := db.Query(`EXPLAIN QUERY PLAN SELECT MAX(id) FROM transmissions WHERE last_seen = 0`)
if err != nil {
t.Fatalf("EXPLAIN QUERY PLAN: %v", err)
}
defer rows.Close()
plan := ""
for rows.Next() {
var id, parent, notused int
var detail string
if err := rows.Scan(&id, &parent, &notused, &detail); err != nil {
t.Fatalf("scan plan: %v", err)
}
plan += detail + "\n"
}
t.Logf("backfill MAX plan:\n%s", plan)
if !strings.Contains(plan, "idx_tx_last_seen_zero") {
t.Fatalf("backfill MAX lookup must use partial index idx_tx_last_seen_zero (#1740); plan was:\n%s", plan)
}
// Defense-in-depth: the legacy full index must NOT be the one used,
// and ideally must not even exist post-migration (the gated DROP
// covers that — see TestPartialIdxTxLastSeenZero_FullIndexDropped).
if strings.Contains(plan, "idx_tx_last_seen ") || strings.HasSuffix(strings.TrimSpace(plan), "idx_tx_last_seen") {
t.Fatalf("backfill MAX lookup should NOT use legacy full idx_tx_last_seen (#1740); plan was:\n%s", plan)
}
}
// TestPartialIdxTxLastSeenZero_FullIndexDropped asserts the gated DROP
// migration ran after the partial index was created (#1740 step b).
func TestPartialIdxTxLastSeenZero_FullIndexDropped(t *testing.T) {
db := minimalDB(t)
defer db.Close()
if err := Apply(db, nil); err != nil {
t.Fatalf("Apply: %v", err)
}
// Confirm the partial exists (precondition for the DROP gate).
var partialName string
err := db.QueryRow(`SELECT name FROM sqlite_master WHERE type='index' AND name='idx_tx_last_seen_zero'`).Scan(&partialName)
if err != nil {
t.Fatalf("idx_tx_last_seen_zero must exist after Apply (#1740): %v", err)
}
// Legacy full index must be gone after Apply (gated DROP).
var legacyName string
err = db.QueryRow(`SELECT name FROM sqlite_master WHERE type='index' AND name='idx_tx_last_seen'`).Scan(&legacyName)
if err == nil {
t.Fatalf("legacy idx_tx_last_seen must be dropped after partial index is in place (#1740); still present as %q", legacyName)
}
}