mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-22 17:01:20 +00:00
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:
1 parent
db5520f70f
commit
e465e1c6c6
3 files changed
+114
-7
No files matched your search
+5
-4
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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, ¬used, &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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user