Channel messages by site: a read model that serves region and scope pages while trusted (#246)

* feat(db): channel_message_iatas table and indexes

* feat(ingest): record channel message sites at insert and on every later hearing

* feat(db): channel message site backfill and two-range reconcile

* feat(db): serve region and scope message pages from the site read model when trusted

* feat: boot repair, periodic reconcile and first-time verification for the message site read model

* test(planguard): channel message site statements, diff and storediff cases

* feat(background): log when a read-only server trusts the message site read model

* test(planguard): storediff vars in query values, and message cases read from the data

* fix(ingest): record a stored hearing's channel message site right after the observation insert

* fix: bounded message dispatch for filtered all-channel pages, fresher read-only trust, earlier invalidation

* fix: record a hearing's site after its path is marked and skip the backstop when the message was found; read-only trust ends at the writer's deadline
This commit is contained in:
MrAlders0n authored and GitHub committed 2026-10-19 21:20:21 -04:00
1 parent 232588c361
commit 5ded8d925f
321 files changed
+13154 -706

No files matched your search

+7 -2
View File
@@ -69,12 +69,17 @@ jobs:
- name: Verify stats and endpoint queries against PostgreSQL 16
env:
BEACON_TEST_POSTGRES_DSN: postgres://postgres:backup-ci-only@127.0.0.1:5432/postgres?sslmode=disable
run: go test ./db -run '^Test(Signal|Paths|PacketSummaries|PacketEndpointsResolveLive|ObserverMetrics|RouteEvidence|RouteEvidenceIndex|AnalyticsRetention|AnalyticsRetentionConcurrent|DeleteOldPacketsBatches|MeshMapperCatalogue|ChannelMessageScopes|NodeLocationReset|RunMigrations[A-Za-z]*|KnownRoutesIndexSet|PacketsDomainChecks|Reconfirm[A-Za-z]*|BigintCursors|RouteSearchHopOrder|StatsNodeTypesSplit|ListKnownRoutes|ListKnownRoutesMatchesLegacy|KnownRoutesCursorTies|KnownRoutesUnanalyzedRegion|KnownRoutesHopStatementsStayCustom|KnownRoutePaginationTies|RouteRetentionCutoffsExact|DeleteExpiredRoutes|DeleteGraceRoutesSlice|GraceWatermarkHeldByLockedRow|OpenPoolsSettings|PlanRouterClassCUnnamed|APIStoreHopStatementsUnnamed|PgStatStatementsMigrationGuarded|DBStatements|DBStatementsNoExtension|PgStatStatementsMigrationNoCreatableSchema|IndexHealthBaselineOnReindex|UnusedIndexes|PgActivity|RollHourSetsWorkMem|SmallTableIndexSet|NodeReadersAfterIndexDrops|ObserverChannelReadersAfterIndexDrops|UpsertChannelWriteThrough|UpsertChannelHashOnlyWriteThrough|ReadOnlyPoolsRejectWrites|ListPacketsGlobalMatchesLegacy|PacketsGlobalCursorTies|ListPacketsWindowInvertedRows|PacketsLatestObserverTie|NodesLegacyCursorMatchesToday|NodesPageCursorTies|ObserversLegacyCursorMatchesToday|ObserversNextCursorFromLastSeen|ObserverAdvertsPartialIndex|AdvertsWaitForPayloadTypeBackfill|StatsProbeMatchesToday|TraceTagsKeysetMatchesToday|TelemetrySinceMatchesCatchAll|TraceTagPagination|TraceTagPaginationTies|TraceSummarySurvivesMissedHearing)Postgres$' -count=1 -v
run: go test ./db -run '^Test(Signal|Paths|PacketSummaries|PacketEndpointsResolveLive|ObserverMetrics|RouteEvidence|RouteEvidenceIndex|AnalyticsRetention|AnalyticsRetentionConcurrent|DeleteOldPacketsBatches|MeshMapperCatalogue|ChannelMessageScopes|NodeLocationReset|RunMigrations[A-Za-z]*|KnownRoutesIndexSet|PacketsDomainChecks|Reconfirm[A-Za-z]*|BigintCursors|RouteSearchHopOrder|StatsNodeTypesSplit|ListKnownRoutes|ListKnownRoutesMatchesLegacy|KnownRoutesCursorTies|KnownRoutesUnanalyzedRegion|KnownRoutesHopStatementsStayCustom|KnownRoutePaginationTies|RouteRetentionCutoffsExact|DeleteExpiredRoutes|DeleteGraceRoutesSlice|GraceWatermarkHeldByLockedRow|OpenPoolsSettings|PlanRouterClassCUnnamed|APIStoreHopStatementsUnnamed|PgStatStatementsMigrationGuarded|DBStatements|DBStatementsNoExtension|PgStatStatementsMigrationNoCreatableSchema|IndexHealthBaselineOnReindex|UnusedIndexes|PgActivity|RollHourSetsWorkMem|SmallTableIndexSet|NodeReadersAfterIndexDrops|ObserverChannelReadersAfterIndexDrops|UpsertChannelWriteThrough|UpsertChannelHashOnlyWriteThrough|ReadOnlyPoolsRejectWrites|ListPacketsGlobalMatchesLegacy|PacketsGlobalCursorTies|ListPacketsWindowInvertedRows|PacketsLatestObserverTie|NodesLegacyCursorMatchesToday|NodesPageCursorTies|ObserversLegacyCursorMatchesToday|ObserversNextCursorFromLastSeen|ObserverAdvertsPartialIndex|AdvertsWaitForPayloadTypeBackfill|StatsProbeMatchesToday|TraceTagsKeysetMatchesToday|TelemetrySinceMatchesCatchAll|TraceTagPagination|TraceTagPaginationTies|TraceSummarySurvivesMissedHearing|CmiMigrationsIndexSet|InsertChannelMessageSeed[A-Za-z]*|AddChannelMessageIATA|CmiSeedAndAddRace|CmiInitAndBackfill|CmiMessageRangeReconcile|CmiObservationRangeReconcile|CmiBackfillRetries23503|WaitForPreBootWriters|ChannelMessagesByIATAsMatchesLegacy|ChannelMessagesZeroHitRegion|CmiTrustLifecycle|CmiFirstVerification)Postgres$' -count=1 -v
- name: Verify the channel message site rule against PostgreSQL 16
env:
BEACON_TEST_POSTGRES_DSN: postgres://postgres:backup-ci-only@127.0.0.1:5432/postgres?sslmode=disable
run: go test ./internal/ingest -run '^Test(CmiAddOnDecryptFailure|CmiCleanIngestReconcilesNothing)Postgres$' -count=1 -v
- name: Verify route retention task against PostgreSQL 16
env:
BEACON_TEST_POSTGRES_DSN: postgres://postgres:backup-ci-only@127.0.0.1:5432/postgres?sslmode=disable
run: go test ./internal/background -run '^Test(RouteRetentionTask|PayloadTypeBackfill|PayloadTypeBackfillProbe)Postgres$' -count=1 -v
run: go test ./internal/background -run '^Test(RouteRetentionTask|PayloadTypeBackfill|PayloadTypeBackfillProbe|CmiBootRepair|CmiReadOnlyTrust)Postgres$' -count=1 -v
# Builds the fixture once; the planguard tests below reuse it (PLANGUARD_FIXTURE_DSN) after checking that its
# marker records a build of the same files.
+31 -3
View File
@@ -165,6 +165,14 @@ func main() {
os.Exit(1)
}
}
// The read-model boot repair waits for every transaction that began before this instant.
bootTS, err := db.DatabaseNow(ctx, pools.Maint)
if err != nil {
slog.Error("reading the database clock failed", "component", "startup", "error", err)
os.Exit(1)
}
// Trust in each read model is per process: earned by this boot's repair, dropped by any writer's failure.
readModels := &db.ReadModels{Cmi: db.NewTrust("cmi", resolved.ReadModelCmiEnabled, db.CmiTrustPolicy)}
// Three stores: ingest writes, background and operator work, and API reads. The API store
// routes each statement by plan class through the guards; it never runs background jobs.
@@ -172,9 +180,9 @@ func main() {
guard := guards.New(guards.Options{Enforce: enforce, ForceObserve: layout == planpolicy.Single, Budget: resolved.GuardsBudget})
// Ingest and maintenance share one channel id cache; the API never writes channels.
channelIDs := db.NewChannelIDCache()
ingestStore := db.New(pools.Ingest, pools.Ingest, resolved.ClockDriftThreshold, resolved.NodeStaleThreshold, db.WithChannelIDs(channelIDs))
maintStore := db.New(pools.Maint, pools.Maint, resolved.ClockDriftThreshold, resolved.NodeStaleThreshold, db.WithChannelIDs(channelIDs))
apiStore := db.NewAPIStore(pools, layout, guard, resolved.ClockDriftThreshold, resolved.NodeStaleThreshold)
ingestStore := db.New(pools.Ingest, pools.Ingest, resolved.ClockDriftThreshold, resolved.NodeStaleThreshold, db.WithChannelIDs(channelIDs), db.WithReadModels(readModels))
maintStore := db.New(pools.Maint, pools.Maint, resolved.ClockDriftThreshold, resolved.NodeStaleThreshold, db.WithChannelIDs(channelIDs), db.WithReadModels(readModels))
apiStore := db.NewAPIStore(pools, layout, guard, resolved.ClockDriftThreshold, resolved.NodeStaleThreshold, db.WithReadModels(readModels))
if err := maintStore.PreloadChannelIDs(ctx); err != nil {
slog.Warn("channel id preload failed; known channels upsert until cached", "component", "startup", "error", err)
} else {
@@ -483,6 +491,25 @@ func main() {
if repeaterImporter != nil && cfg.MeshMapper.Repeaters.Enabled {
tasks = append(tasks, background.Task{Name: "meshmapper.repeaters", Interval: meshmapper.PollInterval, Run: repeaterImporter.Refresh})
}
cmiJobs := &background.CmiJobs{
Begin: func(ctx context.Context) (background.CmiSession, bool, error) {
s, ok, err := maintStore.BeginCmi(ctx)
if s == nil {
return nil, ok, err
}
return s, ok, err
},
Wait: func(ctx context.Context) error {
return db.WaitForPreBootWriters(ctx, pools.Maint, bootTS, time.Minute)
},
Trust: readModels.Cmi,
Interval: background.CmiInterval,
}
if mode.Jobs {
tasks = append(tasks, cmiJobs.Task())
} else {
go background.CmiReadOnlyWatch(ctx, maintStore.CheckCmiReadOnly, readModels.Cmi, background.CmiInterval)
}
pgSource, pgTask := planguardWiring(ctx, resolved.PlanguardDailyCheck, mode.Jobs, pools.Maint)
if pgTask != nil {
tasks = append(tasks, *pgTask)
@@ -495,6 +522,7 @@ func main() {
if mode.Jobs {
scheduler := background.New(tasks)
go scheduler.Start(ctx)
go cmiJobs.Boot(ctx)
}
// ── HTTP server ──────────────────────────────────────────────────────────
+7
View File
@@ -77,6 +77,13 @@ ratelimit:
#planguard:
# daily_check: false
# Read models. Serve region and scope channel message pages from channel_message_iatas once this process has
# repaired and verified it. false serves every page from the original statements; the table is still maintained,
# so turning it back on needs only a restart.
#readmodels:
# cmi:
# enabled: true
# MeshMapper integrations (optional, require a regional or grouped-region API key).
# Your local MeshMapper regional or grouped-region admin can generate the key.
# A grouped-region key covers multiple regions.
+6 -1
View File
@@ -15,7 +15,12 @@ import (
// LoadState unmarshals the background_state value under key into v. It returns false, nil when
// the key has no row.
func (s *Store) LoadState(ctx context.Context, key string, v any) (bool, error) {
raw, err := s.q.GetBackgroundState(ctx, key)
return loadState(ctx, s.q, key, v)
}
// loadState reads key through q, which may be bound to one connection or a transaction.
func loadState(ctx context.Context, q sqlc.Querier, key string, v any) (bool, error) {
raw, err := q.GetBackgroundState(ctx, key)
if errors.Is(err, pgx.ErrNoRows) {
return false, nil
}
+8
View File
@@ -39,6 +39,14 @@ func applyBaseline(t *testing.T, ctx context.Context, tx pgx.Tx) {
if _, err := tx.Exec(ctx, string(ddl)); err != nil {
t.Fatal(err)
}
// and the read-model table the message writer maintains
cmi, err := migrationFiles.ReadFile("migrations/" + cmiMigrations[0])
if err != nil {
t.Fatal(err)
}
if _, err := tx.Exec(ctx, string(cmi)); err != nil {
t.Fatal(err)
}
if _, err := tx.Exec(ctx, "INSERT INTO iata_codes (iata) VALUES ('YVR'),('YYJ'),('YYZ'),('YOW')"); err != nil {
t.Fatal(err)
}
+1 -1
View File
@@ -46,7 +46,7 @@ func TestChannelMessageScopesPostgres(t *testing.T) {
t.Fatal(err)
}
defer tx.Rollback(context.Background())
for _, table := range []string{"transport_scopes", "packets", "packet_observations", "channels", "channel_messages"} {
for _, table := range []string{"transport_scopes", "packets", "packet_observations", "channels", "channel_messages", "channel_message_iatas"} {
if _, err := tx.Exec(ctx, "CREATE TEMP TABLE "+table+" (LIKE public."+table+" INCLUDING ALL) ON COMMIT DROP"); err != nil {
t.Fatal(err)
}
+144 -2
View File
@@ -7,6 +7,8 @@ import (
"context"
"encoding/hex"
"errors"
"log/slog"
"math"
"time"
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
@@ -217,24 +219,164 @@ func (s *Store) GetChannel(ctx context.Context, channelID int32) (*api.Channel,
return &channel, nil
}
// InsertChannelMessage stores a message and, in the same statement, the site of every stored observation of
// its packet. If that statement fails for any reason but cancellation, the message is stored without its sites
// (the read-model reconcile adds them), so no error path loses a message today's statement would store.
func (s *Store) InsertChannelMessage(ctx context.Context, m ingest.InsertChannelMessageParams) (*ingest.InsertedChannelMessage, error) {
params := sqlc.InsertChannelMessageParams{ChannelID: int32(m.ChannelID), PacketHash: m.PacketHash, SenderName: &m.SenderName, Content: &m.Content, SentAt: pgtype.Timestamptz{Time: m.SentAt, Valid: true}, Column6: m.Historical}
row, err := s.q.InsertChannelMessage(ctx, params)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil // duplicate
}
if err != nil {
// Whatever happens next, a page may miss this packet's sites until the reconcile: the fallback stores the
// message without them, and a failed insert's Add finds no message while another lane's is in flight.
s.readModels.invalidateCmi("a channel message insert failed")
}
if err != nil && ctx.Err() == nil {
slog.Warn("channel message site seed failed; storing the message without it", "component", "db", "packet_hash", hex.EncodeToString(m.PacketHash), "error", err)
unseeded, uerr := s.q.InsertChannelMessageUnseeded(ctx, sqlc.InsertChannelMessageUnseededParams(params))
if errors.Is(uerr, pgx.ErrNoRows) {
return nil, nil // duplicate
}
if uerr != nil {
return nil, uerr
}
row, err = sqlc.InsertChannelMessageRow(unseeded), nil
}
if err != nil {
return nil, err
}
return &ingest.InsertedChannelMessage{ID: row.ID, Scope: row.ScopeName, ScopeStatus: api.RecordedChannelScopeStatus(row.ScopeName, row.TransportCodesPresent)}, nil
}
// AddChannelMessageIATA records iata as a site of the packet's message, when the packet has one.
// found says the packet has a committed message, so the site is recorded. A failure drops cmi trust until the next
// reconcile adds the pair.
func (s *Store) AddChannelMessageIATA(ctx context.Context, packetHash []byte, iata string) (bool, error) {
found, err := s.q.AddChannelMessageIATA(ctx, sqlc.AddChannelMessageIATAParams{PacketHash: packetHash, Iata: iata})
if err != nil {
s.readModels.invalidateCmi("a message site was not recorded")
}
return found, err
}
// ListChannelMessages serves /channels/{id}/messages (channelID set) and /messages.
func (s *Store) ListChannelMessages(ctx context.Context, channelID *int32, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) {
return listChannelMessages(ctx, s.q, s.readModels.cmiTrusted(), channelID, since, limit, iatas, scope, cursor)
}
// listChannelMessages runs one statement per request shape. A zero since is "no since", nil and empty iatas are
// "no region", "" is "no scope". The shapes filtered by site, and those filtered by scope without since, read
// channel_message_iatas; without useReadModel they run today's statement unchanged. The others never read it.
func listChannelMessages(ctx context.Context, q sqlc.Querier, useReadModel bool, channelID *int32, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) {
hasSince, hasSites, hasScope := !since.IsZero(), len(iatas) > 0, scope != ""
// Sites with a since or a scope across every channel have no index that bounds both: each site's range would
// be walked until enough rows pass the filter, so today's statement serves them, trusted or not.
allChannelsFiltered := channelID == nil && hasSites && (hasSince || hasScope)
if allChannelsFiltered || ((hasSites || (hasScope && !hasSince)) && !useReadModel) {
return listChannelMessagesLegacy(ctx, q, channelID, since, limit, iatas, scope, cursor)
}
var scopeID *int32
if hasScope {
id, err := q.GetTransportScopeByName(ctx, scope)
if errors.Is(err, pgx.ErrNoRows) {
return api.Page[api.ChannelMessage]{Items: []api.ChannelMessage{}}, nil
}
if err != nil {
return api.Page[api.ChannelMessage]{}, err
}
scopeID = &id
}
sites := dedupeSites(iatas)
cursorID := cursor
if cursor == 0 {
cursorID = math.MaxInt64
}
pageLimit := limit + 1
ts := pgtype.Timestamptz{Time: since, Valid: true}
var rows []sqlc.ListChannelMessagesRow
var err error
switch {
case channelID != nil && hasSince:
rows, err = convertRows(q.ListChannelMessagesSinceWalk(ctx, sqlc.ListChannelMessagesSinceWalkParams{ChannelID: *channelID, Since: ts, Iatas: sites, ScopeID: scopeID, CursorID: cursorID, PageLimit: pageLimit}))
case channelID != nil && hasSites && hasScope:
rows, err = convertRows(q.ListChannelMessagesByIATAsScoped(ctx, sqlc.ListChannelMessagesByIATAsScopedParams{Iatas: sites, ChannelID: *channelID, ScopeID: *scopeID, CursorID: cursorID, PageLimit: pageLimit}))
case channelID != nil && hasSites:
rows, err = convertRows(q.ListChannelMessagesByIATAs(ctx, sqlc.ListChannelMessagesByIATAsParams{Iatas: sites, ChannelID: *channelID, CursorID: cursorID, PageLimit: pageLimit}))
case channelID != nil && hasScope:
rows, err = convertRows(q.ListChannelMessagesPlainScoped(ctx, sqlc.ListChannelMessagesPlainScopedParams{ChannelID: *channelID, ScopeID: *scopeID, CursorID: cursorID, PageLimit: pageLimit}))
case channelID != nil:
rows, err = convertRows(q.ListChannelMessagesPlain(ctx, sqlc.ListChannelMessagesPlainParams{ChannelID: *channelID, CursorID: cursorID, PageLimit: pageLimit}))
case hasSites:
rows, err = convertRows(q.ListAllChannelMessagesByIATAs(ctx, sqlc.ListAllChannelMessagesByIATAsParams{Iatas: sites, CursorID: cursorID, PageLimit: pageLimit}))
case hasSince:
rows, err = convertRows(q.ListAllChannelMessagesSince(ctx, sqlc.ListAllChannelMessagesSinceParams{Since: ts, ScopeID: scopeID, CursorID: cursorID, PageLimit: pageLimit}))
case hasScope:
rows, err = convertRows(q.ListAllChannelMessagesPlainScoped(ctx, sqlc.ListAllChannelMessagesPlainScopedParams{ScopeID: *scopeID, CursorID: cursorID, PageLimit: pageLimit}))
default:
rows, err = convertRows(q.ListAllChannelMessagesPlain(ctx, sqlc.ListAllChannelMessagesPlainParams{CursorID: cursorID, PageLimit: pageLimit}))
}
if err != nil {
return api.Page[api.ChannelMessage]{}, err
}
hasMore := len(rows) > int(limit)
if hasMore {
rows = rows[:limit]
}
messages := make([]api.ChannelMessage, 0, len(rows))
for _, v := range rows {
messages = append(messages, toChannelMessage(v.ID, v.PacketHashHex, v.ChannelHash, v.SenderName, v.Content, v.SentAt, v.ObservationCount, v.ScopeName, v.TransportCodesPresent))
}
var nextCursor *int64
if hasMore && len(messages) > 0 {
last := messages[len(messages)-1].ID
nextCursor = &last
}
return api.Page[api.ChannelMessage]{Items: messages, NextCursor: nextCursor, HasMore: hasMore}, nil
}
// dedupeSites drops repeated codes in one pass, keeping first-occurrence order; never nil.
func dedupeSites(iatas []string) []string {
sites := make([]string, 0, len(iatas))
seen := make(map[string]struct{}, len(iatas))
for _, iata := range iatas {
if _, dup := seen[iata]; !dup {
seen[iata] = struct{}{}
sites = append(sites, iata)
}
}
return sites
}
// messageRow is every list statement's row: the columns of sqlc.ListChannelMessagesRow.
type messageRow interface {
sqlc.ListChannelMessagesByIATAsRow | sqlc.ListChannelMessagesByIATAsScopedRow | sqlc.ListChannelMessagesPlainRow |
sqlc.ListChannelMessagesPlainScopedRow | sqlc.ListChannelMessagesSinceWalkRow | sqlc.ListAllChannelMessagesByIATAsRow |
sqlc.ListAllChannelMessagesPlainRow |
sqlc.ListAllChannelMessagesPlainScopedRow | sqlc.ListAllChannelMessagesSinceRow
}
func convertRows[R messageRow](rows []R, err error) ([]sqlc.ListChannelMessagesRow, error) {
if err != nil {
return nil, err
}
out := make([]sqlc.ListChannelMessagesRow, len(rows))
for i, r := range rows {
out[i] = sqlc.ListChannelMessagesRow(r)
}
return out, nil
}
// listChannelMessagesLegacy is today's statement pair: one join over every observation, filtered in place. It
// serves the shapes that read the read model while this process does not trust it.
func listChannelMessagesLegacy(ctx context.Context, q sqlc.Querier, channelID *int32, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) {
ts := pgtype.Timestamptz{Time: since, Valid: !since.IsZero()}
var messages []api.ChannelMessage
var hasMore bool
if channelID == nil {
rows, err := s.q.ListAllChannelMessages(ctx, sqlc.ListAllChannelMessagesParams{
rows, err := q.ListAllChannelMessages(ctx, sqlc.ListAllChannelMessagesParams{
Column1: ts,
Column2: iatas,
Column3: scope,
@@ -253,7 +395,7 @@ func (s *Store) ListChannelMessages(ctx context.Context, channelID *int32, since
messages = append(messages, toChannelMessage(v.ID, v.PacketHashHex, v.ChannelHash, v.SenderName, v.Content, v.SentAt, v.ObservationCount, v.ScopeName, v.TransportCodesPresent))
}
} else {
rows, err := s.q.ListChannelMessages(ctx, sqlc.ListChannelMessagesParams{
rows, err := q.ListChannelMessages(ctx, sqlc.ListChannelMessagesParams{
ChannelID: *channelID,
Column2: ts,
Column3: iatas,
+198 -1
View File
@@ -6,6 +6,8 @@ package db
import (
"context"
"errors"
"math"
"strings"
"sync"
"testing"
"time"
@@ -346,7 +348,7 @@ func TestListChannelMessages_DBError(t *testing.T) {
mock := mockdb.NewMockQuerier(ctrl)
mock.EXPECT().
ListAllChannelMessages(gomock.Any(), gomock.Any()).
ListAllChannelMessagesPlain(gomock.Any(), gomock.Any()).
Return(nil, errors.New("db error"))
store := &Store{q: mock}
@@ -690,3 +692,198 @@ func TestUpsertChannelFallbackErrorForgetsKey(t *testing.T) {
t.Fatal("a key whose row is gone stayed cached")
}
}
// earnedCmi returns read models whose cmi trust is earned.
func earnedCmi() *ReadModels {
tr := NewTrust("cmi_dispatch_test", true, CmiTrustPolicy)
tr.Earn()
return &ReadModels{Cmi: tr}
}
func TestListChannelMessagesDispatch(t *testing.T) {
ch := int32(7)
since := time.Date(2026, 10, 18, 6, 48, 0, 0, time.UTC)
ts := pgtype.Timestamptz{Time: since, Valid: true}
scope := int32(3)
const maxCursor = int64(math.MaxInt64)
type call func(m *mockdb.MockQuerier)
scopeLookup := func(m *mockdb.MockQuerier) {
m.EXPECT().GetTransportScopeByName(gomock.Any(), "#fr").Return(scope, nil)
}
cases := []struct {
name string
channel *int32
since time.Time
iatas []string
scope string
trusted bool
expect []call
}{
{"by sites", &ch, time.Time{}, []string{"YUL", "YUL", "YOW"}, "", true, []call{func(m *mockdb.MockQuerier) {
m.EXPECT().ListChannelMessagesByIATAs(gomock.Any(), sqlc.ListChannelMessagesByIATAsParams{Iatas: []string{"YUL", "YOW"}, ChannelID: 7, CursorID: maxCursor, PageLimit: 51}).Return([]sqlc.ListChannelMessagesByIATAsRow{{ID: 1}}, nil)
}}},
{"by sites and scope", &ch, time.Time{}, []string{"YUL"}, "#fr", true, []call{scopeLookup, func(m *mockdb.MockQuerier) {
m.EXPECT().ListChannelMessagesByIATAsScoped(gomock.Any(), sqlc.ListChannelMessagesByIATAsScopedParams{Iatas: []string{"YUL"}, ChannelID: 7, ScopeID: 3, CursorID: maxCursor, PageLimit: 51}).Return(nil, nil)
}}},
{"plain nil sites", &ch, time.Time{}, nil, "", true, []call{func(m *mockdb.MockQuerier) {
m.EXPECT().ListChannelMessagesPlain(gomock.Any(), sqlc.ListChannelMessagesPlainParams{ChannelID: 7, CursorID: maxCursor, PageLimit: 51}).Return(nil, nil)
}}},
{"plain empty sites untrusted", &ch, time.Time{}, []string{}, "", false, []call{func(m *mockdb.MockQuerier) {
m.EXPECT().ListChannelMessagesPlain(gomock.Any(), sqlc.ListChannelMessagesPlainParams{ChannelID: 7, CursorID: maxCursor, PageLimit: 51}).Return(nil, nil)
}}},
{"plain scoped", &ch, time.Time{}, nil, "#fr", true, []call{scopeLookup, func(m *mockdb.MockQuerier) {
m.EXPECT().ListChannelMessagesPlainScoped(gomock.Any(), sqlc.ListChannelMessagesPlainScopedParams{ChannelID: 7, ScopeID: 3, CursorID: maxCursor, PageLimit: 51}).Return(nil, nil)
}}},
{"since walk with sites and scope", &ch, since, []string{"YUL"}, "#fr", true, []call{scopeLookup, func(m *mockdb.MockQuerier) {
m.EXPECT().ListChannelMessagesSinceWalk(gomock.Any(), sqlc.ListChannelMessagesSinceWalkParams{ChannelID: 7, Since: ts, Iatas: []string{"YUL"}, ScopeID: &scope, CursorID: maxCursor, PageLimit: 51}).Return(nil, nil)
}}},
{"since walk untrusted reads no cmi", &ch, since, nil, "", false, []call{func(m *mockdb.MockQuerier) {
m.EXPECT().ListChannelMessagesSinceWalk(gomock.Any(), sqlc.ListChannelMessagesSinceWalkParams{ChannelID: 7, Since: ts, Iatas: []string{}, CursorID: maxCursor, PageLimit: 51}).Return(nil, nil)
}}},
{"all by sites", nil, time.Time{}, []string{"YUL"}, "", true, []call{func(m *mockdb.MockQuerier) {
m.EXPECT().ListAllChannelMessagesByIATAs(gomock.Any(), sqlc.ListAllChannelMessagesByIATAsParams{Iatas: []string{"YUL"}, CursorID: maxCursor, PageLimit: 51}).Return(nil, nil)
}}},
// Sites with a scope or a since across every channel: each site's range has no bound on the scope or since,
// so these run today's statement, trusted or not.
{"all by sites and scope", nil, time.Time{}, []string{"YUL"}, "#fr", true, []call{func(m *mockdb.MockQuerier) {
m.EXPECT().ListAllChannelMessages(gomock.Any(), sqlc.ListAllChannelMessagesParams{Column1: pgtype.Timestamptz{}, Column2: []string{"YUL"}, Column3: "#fr", Column4: 0, Limit: 51}).Return(nil, nil)
}}},
{"all by sites since", nil, since, []string{"YUL"}, "", true, []call{func(m *mockdb.MockQuerier) {
m.EXPECT().ListAllChannelMessages(gomock.Any(), sqlc.ListAllChannelMessagesParams{Column1: ts, Column2: []string{"YUL"}, Column3: "", Column4: 0, Limit: 51}).Return(nil, nil)
}}},
{"all since scoped untrusted reads no cmi", nil, since, nil, "#fr", false, []call{scopeLookup, func(m *mockdb.MockQuerier) {
m.EXPECT().ListAllChannelMessagesSince(gomock.Any(), sqlc.ListAllChannelMessagesSinceParams{Since: ts, ScopeID: &scope, CursorID: maxCursor, PageLimit: 51}).Return(nil, nil)
}}},
{"all plain untrusted", nil, time.Time{}, nil, "", false, []call{func(m *mockdb.MockQuerier) {
m.EXPECT().ListAllChannelMessagesPlain(gomock.Any(), sqlc.ListAllChannelMessagesPlainParams{CursorID: maxCursor, PageLimit: 51}).Return(nil, nil)
}}},
{"all plain scoped", nil, time.Time{}, nil, "#fr", true, []call{scopeLookup, func(m *mockdb.MockQuerier) {
m.EXPECT().ListAllChannelMessagesPlainScoped(gomock.Any(), sqlc.ListAllChannelMessagesPlainScopedParams{ScopeID: 3, CursorID: maxCursor, PageLimit: 51}).Return(nil, nil)
}}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
m := mockdb.NewMockQuerier(ctrl)
for _, e := range c.expect {
e(m)
}
s := &Store{q: m}
if c.trusted {
s.readModels = earnedCmi()
}
page, err := s.ListChannelMessages(context.Background(), c.channel, c.since, 50, c.iatas, c.scope, 0)
if err != nil || page.Items == nil {
t.Fatalf("page = %+v, %v", page, err)
}
})
}
// Every shape that reads cmi runs today's statement, with today's params, when cmi is not trusted.
disabled := NewTrust("cmi_dispatch_disabled", false, CmiTrustPolicy)
disabled.Earn()
dropped := NewTrust("cmi_dispatch_dropped", true, CmiTrustPolicy)
dropped.Earn()
for range 3 {
dropped.ObserveRun(1)
}
untrusted := map[string]*ReadModels{
"nil": nil, "nil trust": {}, "not earned": {Cmi: NewTrust("cmi_dispatch_new", true, CmiTrustPolicy)},
"disabled": {Cmi: disabled}, "dropped": {Cmi: dropped},
}
shapes := []struct {
channel *int32
since time.Time
iatas []string
scope string
}{
{&ch, time.Time{}, []string{"YUL"}, ""}, {&ch, time.Time{}, []string{"YUL"}, "#fr"}, {&ch, time.Time{}, nil, "#fr"},
{&ch, since, []string{"YUL"}, ""}, {nil, time.Time{}, []string{"YUL"}, ""}, {nil, time.Time{}, []string{"YUL"}, "#fr"},
{nil, since, []string{"YUL"}, "#fr"}, {nil, time.Time{}, nil, "#fr"},
}
for name, rm := range untrusted {
for i, sh := range shapes {
ctrl := gomock.NewController(t)
m := mockdb.NewMockQuerier(ctrl)
legacyTS := pgtype.Timestamptz{Time: sh.since, Valid: !sh.since.IsZero()}
if sh.channel != nil {
m.EXPECT().ListChannelMessages(gomock.Any(), sqlc.ListChannelMessagesParams{ChannelID: 7, Column2: legacyTS, Column3: sh.iatas, Column4: sh.scope, Column5: 0, Limit: 51}).Return(nil, nil)
} else {
m.EXPECT().ListAllChannelMessages(gomock.Any(), sqlc.ListAllChannelMessagesParams{Column1: legacyTS, Column2: sh.iatas, Column3: sh.scope, Column4: 0, Limit: 51}).Return(nil, nil)
}
s := &Store{q: m, readModels: rm}
if _, err := s.ListChannelMessages(context.Background(), sh.channel, sh.since, 50, sh.iatas, sh.scope, 0); err != nil {
t.Fatalf("%s shape %d: %v", name, i, err)
}
ctrl.Finish()
}
}
// An unknown scope answers an empty page with no list query; another lookup error is returned.
for _, trusted := range []bool{true, false} {
ctrl := gomock.NewController(t)
m := mockdb.NewMockQuerier(ctrl)
m.EXPECT().GetTransportScopeByName(gomock.Any(), "#nope").Return(int32(0), pgx.ErrNoRows)
s := &Store{q: m}
var shapeChannel *int32
shapeSince := time.Time{}
if trusted {
s.readModels = earnedCmi()
} else {
shapeSince = since // untrusted, only the non-cmi shapes look the scope up
}
page, err := s.ListChannelMessages(context.Background(), shapeChannel, shapeSince, 50, nil, "#nope", 0)
if err != nil || page.Items == nil || len(page.Items) != 0 || page.HasMore || page.NextCursor != nil {
t.Fatalf("unknown scope: %+v, %v", page, err)
}
ctrl.Finish()
}
ctrl := gomock.NewController(t)
m := mockdb.NewMockQuerier(ctrl)
m.EXPECT().GetTransportScopeByName(gomock.Any(), "#fr").Return(int32(0), errors.New("lookup failed"))
if _, err := (&Store{q: m, readModels: earnedCmi()}).ListChannelMessages(context.Background(), &ch, time.Time{}, 50, nil, "#fr", 0); err == nil {
t.Fatal("lookup error not returned")
}
// Cursors: 0 is the newest page, any other value is passed as given; limit+1 rows fetched.
for _, cur := range []int64{-1, 3_000_000_000} {
ctrl := gomock.NewController(t)
m := mockdb.NewMockQuerier(ctrl)
m.EXPECT().ListChannelMessagesPlain(gomock.Any(), sqlc.ListChannelMessagesPlainParams{ChannelID: 7, CursorID: cur, PageLimit: 51}).Return(nil, nil)
if _, err := (&Store{q: m}).ListChannelMessages(context.Background(), &ch, time.Time{}, 50, nil, "", cur); err != nil {
t.Fatal(err)
}
ctrl.Finish()
}
ctrl = gomock.NewController(t)
m = mockdb.NewMockQuerier(ctrl)
rows := make([]sqlc.ListChannelMessagesPlainRow, 51)
for i := range rows {
rows[i] = sqlc.ListChannelMessagesPlainRow{ID: int64(100 - i)}
}
m.EXPECT().ListChannelMessagesPlain(gomock.Any(), gomock.Any()).Return(rows, nil)
page, err := (&Store{q: m}).ListChannelMessages(context.Background(), &ch, time.Time{}, 50, nil, "", 0)
if err != nil || len(page.Items) != 50 || !page.HasMore || page.NextCursor == nil || *page.NextCursor != 51 {
t.Fatalf("page of 51 = %d items, hasMore %v, next %v, %v", len(page.Items), page.HasMore, page.NextCursor, err)
}
}
// Site lists come from the request with no cap: the dedupe is one pass, keeps first-occurrence order and is never nil.
func TestDedupeSitesLargeList(t *testing.T) {
in := make([]string, 200_000)
for i := range in {
in[i] = []string{"YUL", "YOW", "YYZ"}[i%3]
}
in[199_999] = "ZZZ"
start := time.Now()
got := dedupeSites(in)
if d := time.Since(start); d > time.Second {
t.Fatalf("200K codes took %s", d)
}
if strings.Join(got, ",") != "YUL,YOW,YYZ,ZZZ" {
t.Fatalf("dedupe = %v", got)
}
if empty := dedupeSites(nil); empty == nil || len(empty) != 0 {
t.Fatalf("nil input gave %#v", empty)
}
}
+420
View File
@@ -0,0 +1,420 @@
// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package db
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"math/rand/v2"
"reflect"
"time"
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
)
const (
cmiStateKey = "cmi"
// cmiBackfillBatch is the message ids one fill covers, in the backfill and the reconcile's message part.
cmiBackfillBatch int64 = 10_000
// cmiObsSlice is the observation ids one fill of the reconcile's observation part covers.
cmiObsSlice int64 = 500_000
// cmiFillAttempts bounds the retries of a fill that lost a message to a concurrent cascade (23503).
cmiFillAttempts = 3
// cmiVerifyMessages is the width of each message range the verification checks exactly.
cmiVerifyMessages int64 = 2_000
// cmiBatchPause separates backfill batches.
cmiBatchPause = 100 * time.Millisecond
)
// CmiState is background_state['cmi']: the one-off backfill's progress and the reconcile watermarks. Nothing in it
// says the table is exact; each process earns that at boot.
type CmiState struct {
BackfillMaxID int64 `json:"backfill_max_id"`
BackfilledToID int64 `json:"backfilled_to_id"`
Complete bool `json:"complete"`
ObsPrevHi int64 `json:"obs_prev_hi"`
ObsHi int64 `json:"obs_hi"`
MsgPrevHi int64 `json:"msg_prev_hi"`
MsgHi int64 `json:"msg_hi"`
FirstVerifiedAt *time.Time `json:"first_verified_at,omitempty"`
}
// CmiSession holds the cmi lock on one maintenance connection; every statement and state write goes through it.
type CmiSession struct {
conn *pgxpool.Conn
q *sqlc.Queries
state CmiState
// fills and retries count fill statements and 23503 retries, for tests.
fills, retries int
pause time.Duration // between backfill batches; 0 = cmiBatchPause, < 0 = none
}
func (c *CmiSession) batchPause() time.Duration {
switch {
case c.pause < 0:
return 0
case c.pause == 0:
return cmiBatchPause
}
return c.pause
}
// BeginCmi takes the cmi lock for this schema on a dedicated connection. ok is false when another session holds it.
func (s *Store) BeginCmi(ctx context.Context) (*CmiSession, bool, error) {
if s.pool == nil {
return nil, false, errors.New("cmi: store has no dedicated pool")
}
conn, err := s.pool.Acquire(ctx)
if err != nil {
return nil, false, err
}
var ok bool
if err := conn.QueryRow(ctx, "SELECT pg_try_advisory_lock($1, hashtext(current_schema()))", cmiReconcileLockKey).Scan(&ok); err != nil {
conn.Release()
return nil, false, err
}
if !ok {
conn.Release()
return nil, false, nil
}
return &CmiSession{conn: conn, q: sqlc.New(conn)}, true, nil
}
// Close releases the lock and the connection. If the unlock fails, the connection is closed, which drops the lock.
func (c *CmiSession) Close(ctx context.Context) {
if c.conn == nil {
return
}
if _, err := c.conn.Exec(ctx, "SELECT pg_advisory_unlock($1, hashtext(current_schema()))", cmiReconcileLockKey); err != nil {
_ = c.conn.Conn().Close(ctx)
}
c.conn.Release()
c.conn = nil
}
// State returns the state as of the session's last read or write.
func (c *CmiSession) State() CmiState { return c.state }
func (c *CmiSession) save(ctx context.Context) error {
return saveState(ctx, c.q, cmiStateKey, c.state)
}
// Init loads the persisted state or, on first use, records where the backfill must reach and starts both
// reconcile ranges at the current maxima.
func (c *CmiSession) Init(ctx context.Context) (CmiState, error) {
var st CmiState
found, err := loadState(ctx, c.q, cmiStateKey, &st)
if err != nil {
return CmiState{}, fmt.Errorf("cmi state: %w", err)
}
if found {
c.state = st
return st, nil
}
msgHi, err := c.q.MaxChannelMessageID(ctx)
if err != nil {
return CmiState{}, err
}
obsHi, err := c.q.MaxObservationID(ctx)
if err != nil {
return CmiState{}, err
}
first, err := c.q.NextChannelMessageID(ctx, 0)
if err != nil {
return CmiState{}, err
}
c.state = CmiState{
BackfillMaxID: msgHi, BackfilledToID: max(first-1, 0), Complete: msgHi == 0,
ObsPrevHi: obsHi, ObsHi: obsHi, MsgPrevHi: msgHi, MsgHi: msgHi,
}
return c.state, c.save(ctx)
}
// fill runs one idempotent fill, retrying only when a message was cascaded away between the statement's snapshot
// and its foreign key check (SQLSTATE 23503).
func (c *CmiSession) fill(ctx context.Context, run func() (int64, error)) (int64, error) {
var err error
for attempt := 1; attempt <= cmiFillAttempts; attempt++ {
c.fills++
var n int64
if n, err = run(); err == nil {
return n, nil
}
var pgErr *pgconn.PgError
if !errors.As(err, &pgErr) || pgErr.Code != "23503" || attempt == cmiFillAttempts {
break
}
c.retries++
}
return 0, err
}
func (c *CmiSession) fillMessages(ctx context.Context, after, through int64) (int64, error) {
return c.fill(ctx, func() (int64, error) {
return c.q.FillChannelMessageIATAsByMessages(ctx, sqlc.FillChannelMessageIATAsByMessagesParams{AfterID: after, ThroughID: through})
})
}
// Backfill fills cmi oldest to newest in batches of cmiBackfillBatch message ids, jumping id gaps, until it
// reaches BackfillMaxID (then Complete) or budget is spent. Progress is saved after every batch.
func (c *CmiSession) Backfill(ctx context.Context, budget time.Duration) (CmiState, error) {
deadline := time.Now().Add(budget)
for !c.state.Complete && time.Now().Before(deadline) {
if err := ctx.Err(); err != nil {
return c.state, err
}
next, err := c.q.NextChannelMessageID(ctx, c.state.BackfilledToID)
if err != nil {
return c.state, err
}
if next == 0 || next > c.state.BackfillMaxID {
c.state.Complete = true
if err := c.save(ctx); err != nil {
return c.state, err
}
slog.Info("cmi backfill complete", "component", "readmodel", "max_id", c.state.BackfillMaxID)
break
}
hi := min(next-1+cmiBackfillBatch, c.state.BackfillMaxID)
if _, err := c.fillMessages(ctx, next-1, hi); err != nil {
return c.state, fmt.Errorf("cmi backfill (%d, %d]: %w", next-1, hi, err)
}
c.state.BackfilledToID = hi
if err := c.save(ctx); err != nil {
return c.state, err
}
// A short pause between batches leaves room for ingest's writes and WAL on large deployments.
select {
case <-ctx.Done():
return c.state, ctx.Err()
case <-time.After(c.batchPause()):
}
}
return c.state, nil
}
// Reconcile fills the observation ids and the message ids that arrived since the run before last, so each range
// overlaps the previous run by one interval, and advances both watermarks monotonically. It returns the rows the
// two parts inserted: anything above zero was written by a path that bypassed the read model.
func (c *CmiSession) Reconcile(ctx context.Context) (int64, error) {
obsHi, err := c.q.MaxObservationID(ctx)
if err != nil {
return 0, err
}
msgHi, err := c.q.MaxChannelMessageID(ctx)
if err != nil {
return 0, err
}
var rows int64
for lo := c.state.ObsPrevHi; lo < obsHi; lo += cmiObsSlice {
hi := min(lo+cmiObsSlice, obsHi)
n, err := c.fill(ctx, func() (int64, error) {
return c.q.FillChannelMessageIATAsByObservations(ctx, sqlc.FillChannelMessageIATAsByObservationsParams{AfterID: lo, ThroughID: hi})
})
if err != nil {
return rows, fmt.Errorf("cmi reconcile observations (%d, %d]: %w", lo, hi, err)
}
rows += n
}
for after := c.state.MsgPrevHi; ; {
next, err := c.q.NextChannelMessageID(ctx, after)
if err != nil {
return rows, err
}
if next == 0 || next > msgHi {
break
}
hi := min(next-1+cmiBackfillBatch, msgHi)
n, err := c.fillMessages(ctx, next-1, hi)
if err != nil {
return rows, fmt.Errorf("cmi reconcile messages (%d, %d]: %w", next-1, hi, err)
}
rows += n
after = hi
}
st := c.state
st.ObsPrevHi, st.ObsHi = st.ObsHi, max(obsHi, st.ObsHi)
st.MsgPrevHi, st.MsgHi = st.MsgHi, max(msgHi, st.MsgHi)
c.state = st
return rows, c.save(ctx)
}
// CmiVerification is the outcome of one verification pass.
type CmiVerification struct {
Missing, Extra int64 // the exact check over the sampled message ranges
PagesCompared int
PageDiffs []string // "channel 7 iatas [YUL] cursor 0: legacy 51 rows, cmi 50 rows, first difference at 12"
}
// OK reports a clean pass.
func (v CmiVerification) OK() bool { return v.Missing == 0 && v.Extra == 0 && len(v.PageDiffs) == 0 }
// verifyRanges returns the newest cmiVerifyMessages-wide message range below MsgHi and one random range of the same
// width below it, as (after, through] pairs.
func (c *CmiSession) verifyRanges(first int64) [][2]int64 {
hi := c.state.MsgHi
ranges := [][2]int64{{max(hi-cmiVerifyMessages, 0), hi}}
if lo := max(first-1, 0); hi-cmiVerifyMessages-lo > cmiVerifyMessages {
start := lo + rand.Int64N(hi-cmiVerifyMessages-lo-cmiVerifyMessages)
ranges = append(ranges, [2]int64{start, start + cmiVerifyMessages})
}
return ranges
}
// checkRanges runs the exact check over ranges through q, counting observations up to ObsHi.
func (c *CmiSession) checkRanges(ctx context.Context, q sqlc.Querier, ranges [][2]int64) (CmiVerification, error) {
var v CmiVerification
for _, r := range ranges {
row, err := q.CheckChannelMessageIATAs(ctx, sqlc.CheckChannelMessageIATAsParams{ObsThrough: c.state.ObsHi, AfterID: r[0], ThroughID: r[1]})
if err != nil {
return v, err
}
v.Missing += row.Missing
v.Extra += row.Extra
}
return v, nil
}
// Verify is the first-time verification: it reconciles, VACUUMs and ANALYZEs the table (the index-only scans need
// its visibility map), then in one read-only snapshot runs the exact check over the newest messages and one random
// range below them, and compares today's and the read model's pages on the busiest recent sites and scopes, from
// the newest message and from a deeper cursor. The first pass is persisted as FirstVerifiedAt. An in-flight Add can
// make one attempt differ; the caller retries every interval.
func (c *CmiSession) Verify(ctx context.Context) (CmiVerification, error) {
if _, err := c.Reconcile(ctx); err != nil {
return CmiVerification{}, err
}
if _, err := c.conn.Exec(ctx, "VACUUM (ANALYZE) channel_message_iatas"); err != nil {
return CmiVerification{}, fmt.Errorf("cmi vacuum: %w", err)
}
tx, err := c.conn.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.RepeatableRead, AccessMode: pgx.ReadOnly})
if err != nil {
return CmiVerification{}, err
}
defer tx.Rollback(context.Background()) //nolint:errcheck // read-only
q := sqlc.New(tx)
first, err := q.NextChannelMessageID(ctx, 0)
if err != nil {
return CmiVerification{}, err
}
v, err := c.checkRanges(ctx, q, c.verifyRanges(first))
if err != nil {
return v, err
}
if err := comparePages(ctx, q, max(c.state.MsgHi-cmiVerifyMessages, 0), &v); err != nil {
return v, err
}
if err := tx.Rollback(ctx); err != nil {
return v, err
}
if !v.OK() {
slog.Error("cmi verification failed; pages stay on the original statements", "component", "readmodel",
"missing", v.Missing, "extra", v.Extra, "pages", v.PagesCompared, "diffs", len(v.PageDiffs), "first", v.PageDiffs[:min(len(v.PageDiffs), 3)])
return v, nil
}
if c.state.FirstVerifiedAt == nil {
now := time.Now().UTC()
c.state.FirstVerifiedAt = &now
if err := c.save(ctx); err != nil {
return v, err
}
slog.Info("cmi first-time verification passed", "component", "readmodel", "pages", v.PagesCompared)
}
return v, nil
}
const (
cmiVerifySites = 5
cmiVerifyScopes = 3
cmiVerifyLimit = 50
)
// comparePages compares today's statement with the read model's on the busiest sites and scopes among messages
// above after: channel and all-channel pages, from cursor 0 and from page 1's 26th id.
func comparePages(ctx context.Context, q sqlc.Querier, after int64, v *CmiVerification) error {
type request struct {
channel *int32
iatas []string
scope string
}
var requests []request
sites, err := q.ListChannelMessageIATASamples(ctx, sqlc.ListChannelMessageIATASamplesParams{AfterID: after, RowLimit: cmiVerifySites})
if err != nil {
return err
}
for _, s := range sites {
requests = append(requests, request{channel: &s.ChannelID, iatas: []string{s.Iata}}, request{iatas: []string{s.Iata}})
}
scopes, err := q.ListChannelMessageScopeSamples(ctx, sqlc.ListChannelMessageScopeSamplesParams{AfterID: after, RowLimit: cmiVerifyScopes})
if err != nil {
return err
}
for _, s := range scopes {
requests = append(requests, request{channel: &s.ChannelID, scope: s.Name}, request{scope: s.Name})
}
for _, r := range requests {
cursors := []int64{0}
for i, cursor := 0, int64(0); i < len(cursors); i++ {
cursor = cursors[i]
legacy, err := listChannelMessages(ctx, q, false, r.channel, time.Time{}, cmiVerifyLimit, r.iatas, r.scope, cursor)
if err != nil {
return err
}
model, err := listChannelMessages(ctx, q, true, r.channel, time.Time{}, cmiVerifyLimit, r.iatas, r.scope, cursor)
if err != nil {
return err
}
v.PagesCompared++
if !reflect.DeepEqual(legacy, model) {
at := 0
for at < min(len(legacy.Items), len(model.Items)) && reflect.DeepEqual(legacy.Items[at], model.Items[at]) {
at++
}
where := "all channels"
if r.channel != nil {
where = fmt.Sprintf("channel %d", *r.channel)
}
v.PageDiffs = append(v.PageDiffs, fmt.Sprintf("%s iatas %v scope %q cursor %d: legacy %d rows, cmi %d rows, first difference at %d",
where, r.iatas, r.scope, cursor, len(legacy.Items), len(model.Items), at))
}
if cursor == 0 && len(legacy.Items) > cmiVerifyLimit/2 {
cursors = append(cursors, legacy.Items[cmiVerifyLimit/2].ID)
}
}
}
return nil
}
// CmiReadOnlyCheck is what a read-only server reads to decide whether to trust the table another process maintains.
type CmiReadOnlyCheck struct {
State CmiState
Verification CmiVerification // the exact check over the newest messages
Age time.Duration // since the writer last saved the state, on the database clock
Found bool // the state exists
}
// CheckCmiReadOnly reads the persisted cmi state, its age, and runs the exact check over the newest messages. It
// writes nothing, so a read-only server can decide whether to trust the table another process maintains.
func (s *Store) CheckCmiReadOnly(ctx context.Context) (CmiReadOnlyCheck, error) {
row, err := s.q.GetBackgroundStateAge(ctx, cmiStateKey)
if errors.Is(err, pgx.ErrNoRows) {
return CmiReadOnlyCheck{}, nil
}
if err != nil {
return CmiReadOnlyCheck{}, err
}
out := CmiReadOnlyCheck{Found: true, Age: time.Duration(row.AgeSeconds * float64(time.Second))}
if err := json.Unmarshal(row.Value, &out.State); err != nil {
return out, fmt.Errorf("cmi state: %w", err)
}
c := &CmiSession{state: out.State}
out.Verification, err = c.checkRanges(ctx, s.q, [][2]int64{{max(out.State.MsgHi-cmiVerifyMessages, 0), out.State.MsgHi}})
return out, err
}
+428
View File
@@ -0,0 +1,428 @@
// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package db
import (
"context"
"encoding/json"
"fmt"
"math"
"math/rand/v2"
"reflect"
"slices"
"strings"
"testing"
"time"
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/MeshCore-Beacon/beacon-server/internal/ingest"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
var cmiSites = []string{"YUL", "YOW", "YYZ", "YVR", "YQB", "YHZ"}
// cmiMessages is the dispatch fixture: messages on channels 7, 8 and 9, written through the real writers (the
// seeded insert after the first hearings, an Add for each later hearing).
type cmiMessages struct {
ids []int64 // every stored message, newest first
tiedAt time.Time // a sent_at several messages share
channel map[int64]int32
}
// buildCmiMessages fills f with 120 messages: multi-site packets, repeat hearings at one site, untyped hearings,
// a #fr share, equal sent_at on every tenth message, and one message whose packet has no observation.
func buildCmiMessages(f *cmiFixture) cmiMessages {
f.t.Helper()
f.exec("INSERT INTO iata_codes (iata) VALUES ('YHZ') ON CONFLICT DO NOTHING")
observers := map[string][]uuid.UUID{}
for _, site := range cmiSites {
for n := range 2 {
id := uuid.New()
f.exec("INSERT INTO observers (id, public_key) VALUES ($1, $2)", id, []byte(fmt.Sprintf("%s-%d", site, n)))
observers[site] = append(observers[site], id)
}
}
r := rand.New(rand.NewPCG(4, 2))
base := time.Date(2026, 10, 18, 12, 0, 0, 0, time.UTC)
out := cmiMessages{tiedAt: base.Add(-30 * time.Minute), channel: map[int64]int32{}}
scope := int32(3)
for i := range 120 {
hash := []byte(fmt.Sprintf("msg-%03d", i))
params := ingest.UpsertPacketParams{PacketHash: hash, PayloadType: 5, RouteType: 1, RawHeader: []byte{0x15}, RawPayload: []byte{byte(i)}}
if i%4 == 0 {
params.ScopeID = &scope
params.RouteType = 0
params.TransportCodes = []byte{1, 2, 3, 4}
}
if _, err := f.store.UpsertPacket(f.ctx, params); err != nil {
f.t.Fatal(err)
}
channel := []int32{7, 7, 7, 8, 8, 9}[r.IntN(6)]
sites := slices.Clone(cmiSites)
r.Shuffle(len(sites), func(a, b int) { sites[a], sites[b] = sites[b], sites[a] })
sites = sites[:1+r.IntN(3)]
if i == 77 {
sites = nil // a message whose packet was never stored as heard
}
hear := func(site string, n int) {
pt := int16(5)
if r.IntN(10) == 0 {
pt = 0 // stored with no payload type
}
o := ingest.InsertObservationParams{PacketHash: hash, ObserverID: observers[site][n], IATA: site, HeardAt: base, HashSize: 1, PayloadType: pt}
if _, _, err := f.store.InsertObservation(f.ctx, o); err != nil {
f.t.Fatal(err)
}
if pt == 0 {
f.exec("UPDATE packet_observations SET payload_type = NULL WHERE packet_hash = $1 AND observer_id = $2", hash, observers[site][n])
}
}
first := 0
if len(sites) > 0 {
first = 1 + r.IntN(len(sites))
}
for _, site := range sites[:first] {
hear(site, 0)
}
sent := base.Add(-time.Duration(i) * time.Minute)
if i%10 == 0 {
sent = out.tiedAt
}
m, err := f.store.InsertChannelMessage(f.ctx, ingest.InsertChannelMessageParams{ChannelID: int(channel), PacketHash: hash, SenderName: "s", Content: fmt.Sprint(i), SentAt: sent})
if err != nil || m == nil {
f.t.Fatalf("message %d: %+v, %v", i, m, err)
}
out.ids = append(out.ids, m.ID)
out.channel[m.ID] = channel
later := sites[first:]
if len(sites) > 0 && r.IntN(3) == 0 {
later = append(later, sites[0]) // a second observer at a site that already heard it
}
for _, site := range later {
n := 0
if slices.Contains(sites[:first], site) {
n = 1
}
hear(site, n)
if _, err := f.store.AddChannelMessageIATA(f.ctx, hash, site); err != nil {
f.t.Fatal(err)
}
}
}
slices.Reverse(out.ids)
f.exec("ANALYZE")
return out
}
// bogusSites returns n three-letter codes that name no fixture site.
func bogusSites(n int) []string {
out := make([]string, 0, n)
for a := 'A'; a <= 'Z'; a++ {
for b := 'A'; b <= 'Z'; b++ {
for c := 'A'; c <= 'Z'; c++ {
if code := string([]rune{a, b, c}); len(out) < n && !slices.Contains(cmiSites, code) && code != "ZZZ" {
out = append(out, code)
}
}
}
}
return out
}
// legacyPage is today's statement, through the legacy path, for the same request.
func legacyPage(ctx context.Context, q sqlc.Querier, channelID *int32, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) {
return listChannelMessagesLegacy(ctx, q, channelID, since, limit, iatas, scope, cursor)
}
func TestChannelMessagesByIATAsMatchesLegacyPostgres(t *testing.T) {
f := newCmiFixture(t)
fx := buildCmiMessages(f)
if missing, extra := f.definition(); missing != 0 || extra != 0 {
t.Fatalf("fixture: missing %d, extra %d", missing, extra)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
trusted := New(f.pool, nil, 0, 0, WithReadModels(earnedCmi()))
q := sqlc.New(f.pool)
bogus := bogusSites(5000)
ch7, ch8, ch999 := int32(7), int32(8), int32(999)
channels := []*int32{nil, &ch7, &ch8, &ch999}
siteLists := [][]string{nil, {}, {"YUL"}, {"YUL", "YUL", "YOW"}, {"YUL "}, {"yul"}, {"YULX"}, {""}, cmiSites}
scopes := []string{"", "#fr", "#none", "#unknown", " "}
sinces := []time.Time{{}, time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC), fx.tiedAt}
cursors := []int64{0, fx.ids[len(fx.ids)/2], fx.ids[len(fx.ids)-1], math.MaxInt32, math.MaxInt32 + 1, 3_000_000_000, math.MaxInt64, -1}
limits := []int32{1, 2, 3, 50, 200}
cases, nonEmpty := 0, 0
compare := func(ch *int32, iatas []string, scope string, since time.Time, cursor int64, limit int32) {
t.Helper()
got, err := trusted.ListChannelMessages(ctx, ch, since, limit, iatas, scope, cursor)
if err != nil {
t.Fatal(err)
}
want, err := legacyPage(ctx, q, ch, since, limit, iatas, scope, cursor)
if err != nil {
t.Fatal(err)
}
cases++
if len(want.Items) > 0 {
nonEmpty++
}
if !reflect.DeepEqual(got, want) {
chs := "all"
if ch != nil {
chs = fmt.Sprint(*ch)
}
t.Fatalf("channel %s iatas %d %q scope %q since %v cursor %d limit %d:\n got %+v\nwant %+v", chs, len(iatas), iatas[:min(len(iatas), 3)], scope, since, cursor, limit, got, want)
}
}
for _, ch := range channels {
for _, sites := range siteLists {
for _, scope := range scopes {
for _, since := range sinces {
for _, cursor := range cursors {
for _, limit := range limits {
compare(ch, sites, scope, since, cursor, limit)
}
}
}
}
}
for _, scope := range scopes {
for _, since := range sinces {
for _, cursor := range cursors[:2] {
for _, limit := range []int32{1, 50} {
compare(ch, bogus, scope, since, cursor, limit)
}
}
}
}
}
t.Logf("%d cases identical, %d with rows", cases, nonEmpty)
if nonEmpty < 1000 {
t.Fatalf("only %d of %d cases returned rows: the fixture does not exercise the statements", nonEmpty, cases)
}
// Cursor chains: both paths walk the same ids, each once, newest first.
chains := []struct {
ch *int32
iatas []string
scope string
since time.Time
}{
{&ch7, []string{"YUL"}, "", time.Time{}}, {&ch7, []string{"YUL", "YOW"}, "#fr", time.Time{}}, {&ch7, nil, "#fr", time.Time{}},
{nil, []string{"YUL"}, "", time.Time{}}, {nil, nil, "#fr", time.Time{}}, {nil, nil, "", fx.tiedAt},
}
walk := func(page func(cursor int64) (api.Page[api.ChannelMessage], error)) []int64 {
t.Helper()
var ids []int64
cursor := int64(0)
for range 1000 {
p, err := page(cursor)
if err != nil {
t.Fatal(err)
}
for _, m := range p.Items {
ids = append(ids, m.ID)
}
if !p.HasMore {
return ids
}
cursor = *p.NextCursor
}
t.Fatal("chain did not end")
return nil
}
for i, c := range chains {
for _, limit := range []int32{1, 2, 3} {
got := walk(func(cur int64) (api.Page[api.ChannelMessage], error) {
return trusted.ListChannelMessages(ctx, c.ch, c.since, limit, c.iatas, c.scope, cur)
})
want := walk(func(cur int64) (api.Page[api.ChannelMessage], error) {
return legacyPage(ctx, q, c.ch, c.since, limit, c.iatas, c.scope, cur)
})
if !slices.Equal(got, want) || len(got) == 0 {
t.Fatalf("chain %d limit %d: %v, want %v", i, limit, got, want)
}
for j := 1; j < len(got); j++ {
if got[j] >= got[j-1] {
t.Fatalf("chain %d limit %d not strictly descending at %d: %v", i, limit, j, got)
}
}
}
}
}
// recordingDBTX records the sqlc name of every statement, and the text and args of the last one.
type recordingDBTX struct {
sqlc.DBTX
names []string
sql string
args []any
}
func (r *recordingDBTX) record(sql string, args []any) {
r.names = append(r.names, sqlcName(sql))
r.sql, r.args = sql, args
}
func (r *recordingDBTX) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) {
r.record(sql, args)
return r.DBTX.Exec(ctx, sql, args...)
}
func (r *recordingDBTX) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) {
r.record(sql, args)
return r.DBTX.Query(ctx, sql, args...)
}
func (r *recordingDBTX) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row {
r.record(sql, args)
return r.DBTX.QueryRow(ctx, sql, args...)
}
func TestChannelMessagesZeroHitRegionPostgres(t *testing.T) {
f := newCmiFixture(t)
buildCmiMessages(f)
trusted := New(f.pool, nil, 0, 0, WithReadModels(earnedCmi()))
q := sqlc.New(f.pool)
ch := int32(7)
bogus := bogusSites(5000)
cases := []struct {
ch *int32
iatas []string
scope string
}{
{&ch, []string{"ZZZ"}, ""}, {nil, []string{"ZZZ"}, ""}, {&ch, []string{"YOW"}, "#none"},
{&ch, nil, "#none"}, {nil, nil, "#none"}, {&ch, bogus, ""}, {nil, bogus, ""},
}
for i, c := range cases {
got, err := trusted.ListChannelMessages(f.ctx, c.ch, time.Time{}, 50, c.iatas, c.scope, 0)
if err != nil {
t.Fatal(err)
}
want, err := legacyPage(f.ctx, q, c.ch, time.Time{}, 50, c.iatas, c.scope, 0)
if err != nil {
t.Fatal(err)
}
g, _ := json.Marshal(got)
w, _ := json.Marshal(want)
if string(g) != `{"items":[],"hasMore":false}` || string(g) != string(w) {
t.Fatalf("case %d: %s, today %s", i, g, w)
}
}
// The zero-hit site reads a few index pages: no scan, no observations.
f.exec("VACUUM ANALYZE channel_message_iatas")
rec := &recordingDBTX{DBTX: f.pool}
if _, err := New(rec, nil, 0, 0, WithReadModels(earnedCmi())).ListChannelMessages(f.ctx, &ch, time.Time{}, 50, []string{"ZZZ"}, "", 0); err != nil {
t.Fatal(err)
}
if rec.names[len(rec.names)-1] != "ListChannelMessagesByIATAs" {
t.Fatalf("statements %v", rec.names)
}
tx, err := f.pool.Begin(f.ctx)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback(context.Background()) //nolint:errcheck
if _, err := tx.Exec(f.ctx, "SET LOCAL enable_seqscan = off; SET LOCAL jit = off"); err != nil {
t.Fatal(err)
}
var plan string
if err := tx.QueryRow(f.ctx, "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) "+rec.sql, rec.args...).Scan(&plan); err != nil {
t.Fatal(err)
}
var cmiNode map[string]any
var nodes []map[string]any
var visit func(n map[string]any)
visit = func(n map[string]any) {
nodes = append(nodes, n)
if n["Relation Name"] == "channel_message_iatas" {
cmiNode = n
}
if kids, ok := n["Plans"].([]any); ok {
for _, k := range kids {
visit(k.(map[string]any))
}
}
}
var parsed []map[string]any
if err := json.Unmarshal([]byte(plan), &parsed); err != nil {
t.Fatal(err)
}
visit(parsed[0]["Plan"].(map[string]any))
if cmiNode == nil || cmiNode["Node Type"] != "Index Only Scan" || cmiNode["Index Name"] != "idx_cmi_channel_site" || cmiNode["Actual Rows"].(float64) != 0 {
t.Fatalf("cmi node %v\n%s", cmiNode, plan)
}
for _, n := range nodes {
if n["Node Type"] == "Seq Scan" || n["Relation Name"] == "packet_observations" {
t.Fatalf("node %v in\n%s", n, plan)
}
}
root := parsed[0]["Plan"].(map[string]any)
if b := root["Shared Hit Blocks"].(float64) + root["Shared Read Blocks"].(float64); b > 10 {
t.Fatalf("zero-hit page read %v buffers", b)
}
// An unknown scope runs the lookup and nothing else.
rec = &recordingDBTX{DBTX: f.pool}
if _, err := New(rec, nil, 0, 0, WithReadModels(earnedCmi())).ListChannelMessages(f.ctx, nil, time.Time{}, 50, nil, "#unknown", 0); err != nil {
t.Fatal(err)
}
if strings.Join(rec.names, ",") != "GetTransportScopeByName" {
t.Fatalf("an unknown scope ran %v", rec.names)
}
}
func TestCmiTrustLifecyclePostgres(t *testing.T) {
f := newCmiFixture(t)
buildCmiMessages(f)
rec := &recordingDBTX{DBTX: f.pool}
tr := NewTrust("cmi_lifecycle_test", true, CmiTrustPolicy)
s := New(rec, nil, 0, 0, WithReadModels(&ReadModels{Cmi: tr}))
ch := int32(7)
var first api.Page[api.ChannelMessage]
step := func(label, want string) {
t.Helper()
rec.names = nil
page, err := s.ListChannelMessages(f.ctx, &ch, time.Time{}, 50, []string{"YUL"}, "", 0)
if err != nil {
t.Fatal(err)
}
if got := strings.Join(rec.names, ","); got != want {
t.Fatalf("%s: ran %s, want %s", label, got, want)
}
if len(page.Items) == 0 {
t.Fatalf("%s: empty page", label)
}
if first.Items == nil {
first = page
} else if !reflect.DeepEqual(page, first) {
t.Fatalf("%s: page differs", label)
}
}
step("not earned", "ListChannelMessages")
tr.Earn()
step("earned", "ListChannelMessagesByIATAs")
for range 3 {
tr.ObserveRun(1)
}
step("three repairing runs", "ListChannelMessages")
tr.ObserveRun(0)
tr.ObserveRun(0)
step("two clean runs", "ListChannelMessagesByIATAs")
tr.Invalidate("failed add")
step("a writer failure", "ListChannelMessages")
tr.Repaired(tr.Generation())
step("the next reconcile", "ListChannelMessagesByIATAs")
disabled := NewTrust("cmi_lifecycle_disabled", false, CmiTrustPolicy)
disabled.Earn()
s.readModels = &ReadModels{Cmi: disabled}
step("disabled", "ListChannelMessages")
}
+109
View File
@@ -0,0 +1,109 @@
// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package db
import (
"strings"
"testing"
)
// cmiMigrations are the files that build channel_message_iatas, its indexes and the channel walk index.
var cmiMigrations = []string{
"035_channel_message_iatas.sql",
"036_cmi_channel_site.sql",
"037_cmi_site.sql",
"038_cmi_channel_site_scope.sql",
"039_cmi_channel_scope.sql",
"040_cmi_scope.sql",
"041_channel_messages_channel_id.sql",
}
// The migrations leave channel_message_iatas and channel_messages with exactly these indexes, all usable, and a
// message's site rows go with it.
func TestCmiMigrationsIndexSetPostgres(t *testing.T) {
ctx, pool := schemaPool(t)
if err := RunMigrations(ctx, pool); err != nil {
t.Fatal(err)
}
checkRecorded(t, ctx, pool, cmiMigrations...)
var schema string
if err := pool.QueryRow(ctx, "SELECT current_schema()").Scan(&schema); err != nil {
t.Fatal(err)
}
want := map[string]string{
"channel_message_iatas_pkey": "CREATE UNIQUE INDEX channel_message_iatas_pkey ON public.channel_message_iatas USING btree (message_id, iata)",
"idx_cmi_channel_site": "CREATE INDEX idx_cmi_channel_site ON public.channel_message_iatas USING btree (channel_id, iata, message_id DESC)",
"idx_cmi_site": "CREATE INDEX idx_cmi_site ON public.channel_message_iatas USING btree (iata, message_id DESC)",
"idx_cmi_channel_site_scope": "CREATE INDEX idx_cmi_channel_site_scope ON public.channel_message_iatas USING btree (channel_id, iata, scope_id, message_id DESC) WHERE (scope_id IS NOT NULL)",
"idx_cmi_channel_scope": "CREATE INDEX idx_cmi_channel_scope ON public.channel_message_iatas USING btree (channel_id, scope_id, message_id DESC) WHERE (scope_id IS NOT NULL)",
"idx_cmi_scope": "CREATE INDEX idx_cmi_scope ON public.channel_message_iatas USING btree (scope_id, message_id DESC) WHERE (scope_id IS NOT NULL)",
"channel_messages_pkey": "CREATE UNIQUE INDEX channel_messages_pkey ON public.channel_messages USING btree (id)",
"channel_messages_packet_hash_key": "CREATE UNIQUE INDEX channel_messages_packet_hash_key ON public.channel_messages USING btree (packet_hash)",
"idx_channel_messages_channel_id": "CREATE INDEX idx_channel_messages_channel_id ON public.channel_messages USING btree (channel_id, id DESC)",
// Read only by the fallback statements; dropped a release later.
"idx_channel_messages_channel": "CREATE INDEX idx_channel_messages_channel ON public.channel_messages USING btree (channel_id, sent_at DESC)",
"idx_channel_messages_sent_brin": "CREATE INDEX idx_channel_messages_sent_brin ON public.channel_messages USING brin (sent_at)",
}
rows, err := pool.Query(ctx, `SELECT i.indexname, i.indexdef, x.indisvalid AND x.indisready
FROM pg_indexes i JOIN pg_index x ON x.indexrelid = format('%I.%I', i.schemaname, i.indexname)::regclass
WHERE i.schemaname = current_schema() AND i.tablename IN ('channel_message_iatas', 'channel_messages')`)
if err != nil {
t.Fatal(err)
}
got := map[string]string{}
for rows.Next() {
var name, def string
var ok bool
if err := rows.Scan(&name, &def, &ok); err != nil {
t.Fatal(err)
}
if !ok {
t.Errorf("%s is not valid and ready", name)
}
got[name] = strings.Replace(def, " ON "+schema+".", " ON public.", 1)
}
if err := rows.Err(); err != nil {
t.Fatal(err)
}
for name, def := range want {
if got[name] != def {
t.Errorf("%s:\n got %q\nwant %q", name, got[name], def)
}
}
for name := range got {
if _, ok := want[name]; !ok {
t.Errorf("unexpected index %s: %s", name, got[name])
}
}
var cols []string
if err := pool.QueryRow(ctx, `SELECT array_agg(column_name || ' ' || data_type || CASE WHEN is_nullable = 'NO' THEN ' NOT NULL' ELSE ' NULL' END ORDER BY ordinal_position)
FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = 'channel_message_iatas'`).Scan(&cols); err != nil {
t.Fatal(err)
}
wantCols := "message_id bigint NOT NULL,iata character NOT NULL,channel_id integer NOT NULL,scope_id integer NULL"
if strings.Join(cols, ",") != wantCols {
t.Errorf("columns = %v, want %s", cols, wantCols)
}
var iataLen int
if err := pool.QueryRow(ctx, `SELECT character_maximum_length FROM information_schema.columns
WHERE table_schema = current_schema() AND table_name = 'channel_message_iatas' AND column_name = 'iata'`).Scan(&iataLen); err != nil || iataLen != 3 {
t.Errorf("iata length = %d, %v; want character(3)", iataLen, err)
}
// A packet's deletion cascades through its message to the message's site rows.
if _, err := pool.Exec(ctx, `
INSERT INTO channels (id, channel_hash, name) VALUES (7, '\x11', 'fixture');
INSERT INTO packets (packet_hash, payload_type, payload_version, route_type, raw_payload, raw_header, first_heard_at, last_heard_at)
VALUES ('\x01', 5, 0, 1, '\x00', '\x15', now(), now());
INSERT INTO channel_messages (id, channel_id, packet_hash, sent_at) VALUES (3000000001, 7, '\x01', now());
INSERT INTO channel_message_iatas (message_id, iata, channel_id) VALUES (3000000001, 'YUL', 7);
DELETE FROM packets;`); err != nil {
t.Fatal(err)
}
var left int
if err := pool.QueryRow(ctx, "SELECT count(*) FROM channel_message_iatas").Scan(&left); err != nil || left != 0 {
t.Fatalf("cmi rows after the packet delete = %d, %v", left, err)
}
}
+293
View File
@@ -0,0 +1,293 @@
// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package db
import (
"context"
"math"
"testing"
"time"
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
)
const cmiBase = int64(3_000_000_000)
// unseeded stores a message the way an older binary did: no site rows.
func (f *cmiFixture) unseeded(id int64, channel int32, hash []byte) {
f.t.Helper()
f.exec("INSERT INTO channel_messages (id, channel_id, packet_hash, sender_name, content, sent_at) VALUES ($1, $2, $3, 's', 'c', now())", id, channel, hash)
}
// check runs the exact check over every message and observation.
func (f *cmiFixture) check() sqlc.CheckChannelMessageIATAsRow {
f.t.Helper()
row, err := f.store.q.CheckChannelMessageIATAs(f.ctx, sqlc.CheckChannelMessageIATAsParams{ObsThrough: math.MaxInt64, AfterID: 0, ThroughID: math.MaxInt64})
if err != nil {
f.t.Fatal(err)
}
return row
}
func (f *cmiFixture) count() int {
f.t.Helper()
var n int
if err := f.pool.QueryRow(f.ctx, "SELECT count(*) FROM channel_message_iatas").Scan(&n); err != nil {
f.t.Fatal(err)
}
return n
}
func (f *cmiFixture) begin() *CmiSession {
f.t.Helper()
c, ok, err := f.store.BeginCmi(f.ctx)
if err != nil || !ok {
f.t.Fatalf("BeginCmi: ok %v, %v", ok, err)
}
f.t.Cleanup(func() { c.Close(context.Background()) })
return c
}
func (f *cmiFixture) reconcile(c *CmiSession) int64 {
f.t.Helper()
n, err := c.Reconcile(f.ctx)
if err != nil {
f.t.Fatal(err)
}
return n
}
func TestCmiInitAndBackfillPostgres(t *testing.T) {
t.Run("empty", func(t *testing.T) {
f := newCmiFixture(t)
c := f.begin()
st, err := c.Init(f.ctx)
if err != nil || st != (CmiState{Complete: true}) {
t.Fatalf("Init = %+v, %v", st, err)
}
if st, err = c.Backfill(f.ctx, time.Minute); err != nil || !st.Complete {
t.Fatalf("Backfill = %+v, %v", st, err)
}
if got := f.check(); got.Missing != 0 || got.Extra != 0 {
t.Fatalf("check = %+v", got)
}
})
f := newCmiFixture(t)
var lastObs int64
ids := []int64{1, 2, 3, 4, 5, 40_001, 40_002, 40_003}
for i, off := range ids {
h := f.packet(string(rune('a'+i)), 3)
f.obs(f.pool, h, "YUL", true)
lastObs = f.obs(f.pool, h, []string{"YOW", "YYZ"}[i%2], true)
f.unseeded(cmiBase+off, 7, h)
}
c := f.begin()
st, err := c.Init(f.ctx)
if err != nil {
t.Fatal(err)
}
maxID := cmiBase + 40_003
want := CmiState{BackfillMaxID: maxID, BackfilledToID: cmiBase, ObsPrevHi: lastObs, ObsHi: lastObs, MsgPrevHi: maxID, MsgHi: maxID}
if st != want || c.State() != want {
t.Fatalf("Init = %+v, want %+v", st, want)
}
var loaded CmiState
if ok, err := f.store.LoadState(f.ctx, cmiStateKey, &loaded); !ok || err != nil || loaded != want {
t.Fatalf("persisted %+v (%v, %v), want %+v", loaded, ok, err, want)
}
var raw string
if err := f.pool.QueryRow(f.ctx, "SELECT value->>'backfill_max_id' FROM background_state WHERE key = 'cmi'").Scan(&raw); err != nil || raw != "3000040003" {
t.Fatalf("backfill_max_id stored as %q, %v", raw, err)
}
if missing := f.check().Missing; missing != 16 {
t.Fatalf("missing before the backfill = %d, want 16", missing)
}
st, err = c.Backfill(f.ctx, time.Minute)
if err != nil || !st.Complete || st.BackfilledToID != maxID {
t.Fatalf("Backfill = %+v, %v", st, err)
}
if c.fills != 2 {
t.Fatalf("the backfill ran %d fills, want 2 (the id gap is jumped)", c.fills)
}
if got := f.check(); got.Missing != 0 || got.Extra != 0 {
t.Fatalf("check = %+v", got)
}
before := f.count()
if _, err := c.Backfill(f.ctx, time.Minute); err != nil || f.count() != before || c.fills != 2 {
t.Fatalf("a second backfill: %v, rows %d -> %d, fills %d", err, before, f.count(), c.fills)
}
// One session per schema at a time; schemas do not contend.
if other, ok, err := f.store.BeginCmi(f.ctx); ok || err != nil {
if other != nil {
other.Close(f.ctx)
}
t.Fatalf("second session in the schema: ok %v, %v", ok, err)
}
g := newCmiFixture(t)
g.begin()
}
func TestCmiMessageRangeReconcilePostgres(t *testing.T) {
f := newCmiFixture(t)
old := f.packet("old", 3)
f.obs(f.pool, old, "YUL", true)
f.obs(f.pool, old, "YOW", true)
f.obs(f.pool, old, "YYZ", false)
live := f.packet("live", 0)
f.obs(f.pool, live, "YUL", true)
f.message(f.store, 7, live, false)
c := f.begin()
if _, err := c.Init(f.ctx); err != nil {
t.Fatal(err)
}
if _, err := c.Backfill(f.ctx, time.Minute); err != nil {
t.Fatal(err)
}
if n := f.reconcile(c); n != 0 {
t.Fatalf("clean reconcile repaired %d", n)
}
st := c.State()
// An older binary imports a key and stores a message for the old packet, without sites.
var m int64
if err := f.pool.QueryRow(f.ctx, "INSERT INTO channel_messages (channel_id, packet_hash, sent_at) VALUES (8, $1, now()) RETURNING id", old).Scan(&m); err != nil {
t.Fatal(err)
}
obsMax, err := f.store.q.MaxObservationID(f.ctx)
if err != nil {
t.Fatal(err)
}
if _, err := f.store.q.FillChannelMessageIATAsByObservations(f.ctx, sqlc.FillChannelMessageIATAsByObservationsParams{AfterID: st.ObsPrevHi, ThroughID: obsMax}); err != nil {
t.Fatal(err)
}
if missing := f.check().Missing; missing != 3 {
t.Fatalf("the observation range alone left %d missing, want 3", missing)
}
if n := f.reconcile(c); n != 3 {
t.Fatalf("reconcile repaired %d, want 3", n)
}
if got := f.check(); got.Missing != 0 || got.Extra != 0 {
t.Fatalf("check = %+v", got)
}
after := c.State()
if after.MsgHi != m || after.MsgPrevHi != st.MsgHi {
t.Fatalf("message watermarks %d/%d, want %d/%d", after.MsgPrevHi, after.MsgHi, st.MsgHi, m)
}
if n := f.reconcile(c); n != 0 {
t.Fatalf("next run repaired %d", n)
}
}
func TestCmiObservationRangeReconcilePostgres(t *testing.T) {
f := newCmiFixture(t)
p := f.packet("P", 3)
f.obs(f.pool, p, "YUL", true)
f.message(f.store, 7, p, false)
c := f.begin()
if _, err := c.Init(f.ctx); err != nil {
t.Fatal(err)
}
if _, err := c.Backfill(f.ctx, time.Minute); err != nil {
t.Fatal(err)
}
f.reconcile(c)
prev := c.State()
// Hearings stored without their Add: a new site, and an untyped observation at another.
f.obs(f.pool, p, "YOW", true)
f.obs(f.pool, p, "YYZ", false)
if n := f.reconcile(c); n != 2 {
t.Fatalf("reconcile repaired %d, want 2", n)
}
if got := f.check(); got.Missing != 0 || got.Extra != 0 {
t.Fatalf("check = %+v", got)
}
st := c.State()
if st.ObsPrevHi != prev.ObsHi || st.ObsHi <= prev.ObsHi {
t.Fatalf("observation watermarks %+v after %+v", st, prev)
}
for range 2 {
before := c.State()
if n := f.reconcile(c); n != 0 {
t.Fatalf("idle run repaired %d", n)
}
after := c.State()
if after.ObsHi < before.ObsHi || after.ObsPrevHi < before.ObsPrevHi || after.MsgHi < before.MsgHi || after.MsgPrevHi < before.MsgPrevHi {
t.Fatalf("watermarks moved back: %+v -> %+v", before, after)
}
}
}
func TestCmiBackfillRetries23503Postgres(t *testing.T) {
f := newCmiFixture(t)
var hashes [][]byte
for i, site := range []string{"YUL", "YOW", "YYZ"} {
h := f.packet(site, 0)
f.obs(f.pool, h, site, true)
f.unseeded(cmiBase+int64(i)+1, 7, h)
hashes = append(hashes, h)
}
c := f.begin()
if _, err := c.Init(f.ctx); err != nil {
t.Fatal(err)
}
conn, err := f.pool.Acquire(f.ctx)
if err != nil {
t.Fatal(err)
}
defer conn.Release()
tx, err := conn.Begin(f.ctx)
if err != nil {
t.Fatal(err)
}
defer tx.Rollback(context.Background()) //nolint:errcheck
if _, err := tx.Exec(f.ctx, "DELETE FROM packets WHERE packet_hash = $1", hashes[0]); err != nil {
t.Fatal(err)
}
type result struct {
st CmiState
err error
}
done := make(chan result, 1)
pid := c.conn.Conn().PgConn().PID()
go func() {
st, err := c.Backfill(f.ctx, time.Minute)
done <- result{st, err}
}()
deadline := time.Now().Add(10 * time.Second)
for {
var waiting bool
if err := f.pool.QueryRow(f.ctx, "SELECT EXISTS (SELECT 1 FROM pg_locks WHERE pid = $1 AND NOT granted)", pid).Scan(&waiting); err != nil {
t.Fatal(err)
}
if waiting {
break
}
if time.Now().After(deadline) {
t.Fatal("the backfill never waited on the deleting transaction")
}
time.Sleep(5 * time.Millisecond)
}
if err := tx.Commit(f.ctx); err != nil {
t.Fatal(err)
}
r := <-done
if r.err != nil || !r.st.Complete {
t.Fatalf("Backfill = %+v, %v", r.st, r.err)
}
if c.retries != 1 {
t.Fatalf("retries = %d, want 1 (the first attempt fails with 23503)", c.retries)
}
want := []string{"3000000002/YOW/7/0", "3000000003/YYZ/7/0"}
if got := f.rows(); len(got) != 2 || got[0] != want[0] || got[1] != want[1] {
t.Fatalf("rows = %v, want %v", got, want)
}
if got := f.check(); got.Missing != 0 || got.Extra != 0 {
t.Fatalf("check = %+v", got)
}
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package db
import (
"context"
"testing"
"time"
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestCmiFirstVerificationPostgres(t *testing.T) {
t.Run("empty", func(t *testing.T) {
f := newCmiFixture(t)
c := f.begin()
if _, err := c.Init(f.ctx); err != nil {
t.Fatal(err)
}
v, err := c.Verify(f.ctx)
if err != nil || !v.OK() || v.PagesCompared != 0 {
t.Fatalf("Verify = %+v, %v", v, err)
}
})
f := newCmiFixture(t)
buildCmiMessages(f)
f.exec("TRUNCATE channel_message_iatas") // as on a first install: the backfill fills it
c := f.begin()
if _, err := c.Init(f.ctx); err != nil {
t.Fatal(err)
}
if _, err := c.Backfill(f.ctx, time.Minute); err != nil {
t.Fatal(err)
}
v, err := c.Verify(f.ctx)
if err != nil || !v.OK() || v.PagesCompared < 10 {
t.Fatalf("Verify = %+v, %v", v, err)
}
first := c.State().FirstVerifiedAt
var loaded CmiState
if _, err := f.store.LoadState(f.ctx, cmiStateKey, &loaded); err != nil || first == nil || loaded.FirstVerifiedAt == nil || !loaded.FirstVerifiedAt.Equal(*first) {
t.Fatalf("first_verified_at %v persisted as %+v (%v)", first, loaded, err)
}
var reltuples float64
if err := f.pool.QueryRow(f.ctx, "SELECT reltuples FROM pg_class WHERE oid = 'channel_message_iatas'::regclass").Scan(&reltuples); err != nil || reltuples <= 0 {
t.Fatalf("reltuples = %v (%v): no ANALYZE", reltuples, err)
}
// Damage below the watermarks, where no reconcile looks: the newest message of the busiest sampled pair loses
// a site.
samples, err := f.store.q.ListChannelMessageIATASamples(f.ctx, sqlc.ListChannelMessageIATASamplesParams{AfterID: 0, RowLimit: 1})
if err != nil || len(samples) != 1 {
t.Fatalf("samples %v, %v", samples, err)
}
f.exec(`DELETE FROM channel_message_iatas WHERE (message_id, iata) = (
SELECT message_id, iata FROM channel_message_iatas WHERE channel_id = $1 AND iata = $2 ORDER BY message_id DESC LIMIT 1)`, samples[0].ChannelID, samples[0].Iata)
v, err = c.Verify(f.ctx)
if err != nil || v.Missing != 1 || v.Extra != 0 || len(v.PageDiffs) == 0 || v.OK() {
t.Fatalf("damaged Verify = %+v, %v", v, err)
}
if got := c.State().FirstVerifiedAt; got == nil || !got.Equal(*first) {
t.Fatalf("first_verified_at moved: %v -> %v", first, got)
}
t.Logf("diffs: %v", v.PageDiffs)
// Repair, then the read-only check on a read-only pool.
f.exec("TRUNCATE channel_message_iatas")
f.exec("DELETE FROM background_state WHERE key = 'cmi'")
c.Close(f.ctx)
c = f.begin()
if _, err := c.Init(f.ctx); err != nil {
t.Fatal(err)
}
if _, err := c.Backfill(f.ctx, time.Minute); err != nil {
t.Fatal(err)
}
if v, err := c.Verify(f.ctx); err != nil || !v.OK() {
t.Fatalf("Verify after the rebuild = %+v, %v", v, err)
}
cfg := f.pool.Config().Copy()
cfg.ConnConfig.RuntimeParams["default_transaction_read_only"] = "on"
ro, err := pgxpool.NewWithConfig(context.Background(), cfg)
if err != nil {
t.Fatal(err)
}
defer ro.Close()
rc, err := New(ro, ro, 0, 0).CheckCmiReadOnly(f.ctx)
if err != nil || !rc.Found || !rc.Verification.OK() || !rc.State.Complete || rc.State.FirstVerifiedAt == nil || rc.Age < 0 || rc.Age > time.Minute {
t.Fatalf("CheckCmiReadOnly = %+v, %v", rc, err)
}
f.exec("UPDATE background_state SET updated_at = now() - interval '11 minutes' WHERE key = 'cmi'")
if rc, err = New(ro, ro, 0, 0).CheckCmiReadOnly(f.ctx); err != nil || rc.Age < 11*time.Minute || rc.Age > 12*time.Minute {
t.Fatalf("age after 11 minutes = %v, %v", rc.Age, err)
}
}
+455
View File
@@ -0,0 +1,455 @@
// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package db
import (
"context"
"errors"
"fmt"
"slices"
"testing"
"time"
sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
"github.com/MeshCore-Beacon/beacon-server/internal/ingest"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool"
)
// cmiFixture is a migrated schema whose message and observation ids start above 3,000,000,000, with sites,
// scopes #fr (3) and #none (4), and channels 7, 8 and 9.
type cmiFixture struct {
t *testing.T
ctx context.Context
pool *pgxpool.Pool
store *Store
}
func newCmiFixture(t *testing.T) *cmiFixture {
t.Helper()
ctx, pool := schemaPool(t)
if err := RunMigrations(ctx, pool); err != nil {
t.Fatal(err)
}
f := &cmiFixture{t: t, ctx: ctx, pool: pool, store: New(pool, pool, 0, 0)}
f.exec(`SELECT setval('channel_messages_id_seq', 3000000000);
SELECT setval('packet_observations_id_seq', 3000000000);
INSERT INTO iata_codes (iata) VALUES ('YUL'),('YOW'),('YYZ'),('YVR'),('YQB'),('BAD'),('ZZZ') ON CONFLICT DO NOTHING;
INSERT INTO transport_scopes (id, name, transport_key, key_fingerprint) VALUES (3, '#fr', '\x00', '\x01'), (4, '#none', '\x02', '\x03');
INSERT INTO channels (id, channel_hash, name) VALUES (7, '\x11', 'ch7'), (8, '\x12', 'ch8'), (9, '\x13', 'ch9');`)
return f
}
func (f *cmiFixture) exec(sql string, args ...any) {
f.t.Helper()
if _, err := f.pool.Exec(f.ctx, sql, args...); err != nil {
f.t.Fatal(err)
}
}
// packet stores a group-text packet with the given scope id (0 = none).
func (f *cmiFixture) packet(hash string, scope int32) []byte {
f.t.Helper()
h := []byte(hash)
var scopeID *int32
if scope != 0 {
scopeID = &scope
}
f.exec(`INSERT INTO packets (packet_hash, payload_type, payload_version, route_type, scope_id, raw_payload, raw_header, first_heard_at, last_heard_at, observation_count)
VALUES ($1, 5, 0, 1, $2, '\x00', '\x15', now(), now(), 0)`, h, scopeID)
return h
}
// obs stores one observation of hash at iata by a new observer and returns its id; typed=false leaves payload_type NULL.
func (f *cmiFixture) obs(db execQuerier, hash []byte, iata string, typed bool) int64 {
f.t.Helper()
var pt *int16
if typed {
v := int16(5)
pt = &v
}
var id int64
if err := db.QueryRow(f.ctx, `WITH o AS (INSERT INTO observers (public_key) VALUES (decode(md5(random()::text), 'hex')) RETURNING id)
INSERT INTO packet_observations (packet_hash, observer_id, iata, heard_at, path_length_byte, hash_size, hop_count, payload_type)
SELECT $1, o.id, $2, now(), 0, 1, 0, $3 FROM o RETURNING id`, hash, iata, pt).Scan(&id); err != nil {
f.t.Fatal(err)
}
return id
}
func (f *cmiFixture) message(s *Store, channel int, hash []byte, historical bool) *ingest.InsertedChannelMessage {
f.t.Helper()
m, err := s.InsertChannelMessage(f.ctx, ingest.InsertChannelMessageParams{ChannelID: channel, PacketHash: hash, SenderName: "s", Content: "c", SentAt: time.Now(), Historical: historical})
if err != nil {
f.t.Fatal(err)
}
return m
}
// rows lists the cmi rows as "message/iata/channel/scope", in key order.
func (f *cmiFixture) rows() []string {
f.t.Helper()
rows, err := f.pool.Query(f.ctx, "SELECT message_id, iata, channel_id, coalesce(scope_id, 0) FROM channel_message_iatas ORDER BY 1, 2")
if err != nil {
f.t.Fatal(err)
}
out, err := pgx.CollectRows(rows, func(r pgx.CollectableRow) (string, error) {
var m int64
var iata string
var ch, sc int32
err := r.Scan(&m, &iata, &ch, &sc)
return fmt.Sprintf("%d/%s/%d/%d", m, iata, ch, sc), err
})
if err != nil {
f.t.Fatal(err)
}
return out
}
// definition counts the pairs the read model's definition has and the table lacks, and table pairs no stored
// observation backs, over every message.
func (f *cmiFixture) definition() (missing, extra int64) {
f.t.Helper()
if err := f.pool.QueryRow(f.ctx, `WITH def AS (
SELECT DISTINCT cm.id AS message_id, po.iata, cm.channel_id, p.scope_id
FROM channel_messages cm JOIN packets p ON p.packet_hash = cm.packet_hash JOIN packet_observations po ON po.packet_hash = cm.packet_hash
), have AS (SELECT message_id, iata, channel_id, scope_id FROM channel_message_iatas)
SELECT (SELECT count(*) FROM (SELECT * FROM def EXCEPT SELECT * FROM have) m), (SELECT count(*) FROM (SELECT * FROM have EXCEPT SELECT * FROM def) x)`).Scan(&missing, &extra); err != nil {
f.t.Fatal(err)
}
return missing, extra
}
func (f *cmiFixture) checkDefinition(label string) {
f.t.Helper()
if missing, extra := f.definition(); missing != 0 || extra != 0 {
f.t.Fatalf("%s: missing %d, extra %d; rows %v", label, missing, extra, f.rows())
}
}
func TestInsertChannelMessageSeedsSitesPostgres(t *testing.T) {
f := newCmiFixture(t)
p := f.packet("P", 3)
f.obs(f.pool, p, "YUL", true)
f.obs(f.pool, p, "YOW", true)
f.obs(f.pool, p, "YUL", true)
m := f.message(f.store, 7, p, true)
if m == nil || m.ID <= 3000000000 {
t.Fatalf("message = %+v", m)
}
want := []string{fmt.Sprintf("%d/YOW/7/3", m.ID), fmt.Sprintf("%d/YUL/7/3", m.ID)}
if got := f.rows(); !slices.Equal(got, want) {
t.Fatalf("rows = %v, want %v", got, want)
}
if again := f.message(f.store, 7, p, true); again != nil {
t.Fatalf("duplicate returned %+v", again)
}
if got := f.rows(); !slices.Equal(got, want) {
t.Fatalf("rows after the duplicate = %v, want %v", got, want)
}
var dirty int
if err := f.pool.QueryRow(f.ctx, "SELECT count(*) FROM analytics_dirty_hours WHERE hour = date_trunc('hour', now(), 'UTC')").Scan(&dirty); err != nil || dirty != 1 {
t.Fatalf("historical message queued %d hours: %v", dirty, err)
}
// A packet with no observation: the message is stored, with no site.
q := f.packet("Q", 0)
if mq := f.message(f.store, 8, q, false); mq == nil {
t.Fatal("message without observations not stored")
}
if got := f.rows(); !slices.Equal(got, want) {
t.Fatalf("rows = %v, want %v", got, want)
}
f.checkDefinition("seed")
}
func TestInsertChannelMessageSeedFallbackPostgres(t *testing.T) {
f := newCmiFixture(t)
f.exec("ALTER TABLE channel_message_iatas ADD CONSTRAINT reject_bad CHECK (iata <> 'BAD')")
p := f.packet("P", 0)
f.obs(f.pool, p, "BAD", true)
m := f.message(f.store, 7, p, false)
if m == nil {
t.Fatal("the unseeded statement did not store the message")
}
if got := f.rows(); len(got) != 0 {
t.Fatalf("rows = %v, want none", got)
}
ch, err := f.store.GetChannel(f.ctx, 7)
if err != nil || ch.MessageCount != 1 {
t.Fatalf("message count = %+v, %v; want 1", ch, err)
}
if again := f.message(f.store, 7, p, false); again != nil {
t.Fatalf("duplicate through the fallback returned %+v", again)
}
// A packet missing from packets: both statements return today's foreign key error.
_, err = f.store.InsertChannelMessage(f.ctx, ingest.InsertChannelMessageParams{ChannelID: 7, PacketHash: []byte("missing"), SentAt: time.Now()})
var pgErr *pgconn.PgError
if !errors.As(err, &pgErr) || pgErr.Code != "23503" {
t.Fatalf("missing packet: %v, want 23503", err)
}
_, err = f.store.q.InsertChannelMessageUnseeded(f.ctx, sqlc.InsertChannelMessageUnseededParams{ChannelID: 7, PacketHash: []byte("missing"), SentAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}})
if !errors.As(err, &pgErr) || pgErr.Code != "23503" {
t.Fatalf("missing packet, unseeded: %v, want 23503", err)
}
}
func TestAddChannelMessageIATAPostgres(t *testing.T) {
f := newCmiFixture(t)
p := f.packet("P", 3)
if found, err := f.store.AddChannelMessageIATA(f.ctx, p, "YOW"); err != nil || found {
t.Fatalf("Add with no message: found %v, %v", found, err)
}
if got := f.rows(); len(got) != 0 {
t.Fatalf("Add with no message wrote %v", got)
}
m := f.message(f.store, 9, p, false)
for range 2 {
if found, err := f.store.AddChannelMessageIATA(f.ctx, p, "YOW"); err != nil || !found {
t.Fatalf("Add with a message: found %v, %v", found, err)
}
}
if got, want := f.rows(), []string{fmt.Sprintf("%d/YOW/9/3", m.ID)}; !slices.Equal(got, want) {
t.Fatalf("rows = %v, want %v", got, want)
}
}
// cmiLane is one ingest worker on its own connection: an observation commit, then the message insert, then the
// Add when the insert returned no row.
type cmiLane struct {
f *cmiFixture
conn *pgxpool.Conn
tx pgx.Tx
site string
hash []byte
}
func (f *cmiFixture) lane(site string, hash []byte) *cmiLane {
f.t.Helper()
conn, err := f.pool.Acquire(f.ctx)
if err != nil {
f.t.Fatal(err)
}
f.t.Cleanup(conn.Release)
return &cmiLane{f: f, conn: conn, site: site, hash: hash}
}
func (l *cmiLane) obs() { l.f.obs(l.conn, l.hash, l.site, true) }
// icm runs the insert (inside the open transaction when there is one) and, when it returned no row outside a
// transaction, the Add.
func (l *cmiLane) icm(historical bool) *ingest.InsertedChannelMessage {
l.f.t.Helper()
var db sqlc.DBTX = l.conn
if l.tx != nil {
db = l.tx
}
m, err := New(db, nil, 0, 0).InsertChannelMessage(l.f.ctx, ingest.InsertChannelMessageParams{ChannelID: 7, PacketHash: l.hash, SenderName: "s", Content: "c", SentAt: time.Now(), Historical: historical})
if err != nil {
l.f.t.Fatal(err)
}
if m == nil && l.tx == nil {
l.add()
}
return m
}
func (l *cmiLane) add() {
l.f.t.Helper()
if _, err := New(l.conn, nil, 0, 0).AddChannelMessageIATA(l.f.ctx, l.hash, l.site); err != nil {
l.f.t.Fatal(err)
}
}
func (l *cmiLane) open(historical bool) {
l.f.t.Helper()
tx, err := l.conn.Begin(l.f.ctx)
if err != nil {
l.f.t.Fatal(err)
}
l.tx = tx
l.icm(historical)
}
func (l *cmiLane) end(commit bool) {
l.f.t.Helper()
var err error
if commit {
err = l.tx.Commit(l.f.ctx)
} else {
err = l.tx.Rollback(l.f.ctx)
}
l.tx = nil
if err != nil {
l.f.t.Fatal(err)
}
}
// icmWaiting starts the lane's insert, waits until Postgres shows it blocked on a lock, and returns a function
// that waits for it (and its Add) to finish.
func (l *cmiLane) icmWaiting() func() *ingest.InsertedChannelMessage {
l.f.t.Helper()
pid := l.conn.Conn().PgConn().PID()
done := make(chan *ingest.InsertedChannelMessage, 1)
failed := make(chan error, 1)
go func() {
m, err := New(l.conn, nil, 0, 0).InsertChannelMessage(l.f.ctx, ingest.InsertChannelMessageParams{ChannelID: 7, PacketHash: l.hash, SenderName: "s", Content: "c", SentAt: time.Now()})
if err == nil && m == nil {
_, err = New(l.conn, nil, 0, 0).AddChannelMessageIATA(l.f.ctx, l.hash, l.site)
}
if err != nil {
failed <- err
return
}
done <- m
}()
deadline := time.Now().Add(10 * time.Second)
for {
var waiting bool
if err := l.f.pool.QueryRow(l.f.ctx, "SELECT EXISTS (SELECT 1 FROM pg_locks WHERE pid = $1 AND NOT granted)", pid).Scan(&waiting); err != nil {
l.f.t.Fatal(err)
}
if waiting {
break
}
select {
case err := <-failed:
l.f.t.Fatalf("insert did not wait: %v", err)
case m := <-done:
l.f.t.Fatalf("insert did not wait: returned %+v", m)
default:
}
if time.Now().After(deadline) {
l.f.t.Fatal("insert never waited")
}
time.Sleep(5 * time.Millisecond)
}
return func() *ingest.InsertedChannelMessage {
select {
case err := <-failed:
l.f.t.Fatal(err)
case m := <-done:
return m
case <-time.After(10 * time.Second):
l.f.t.Fatal("insert never finished")
}
return nil
}
}
// Every interleaving of two or three writers of one packet leaves exactly the definition's pairs, with no
// reconcile: the seed covers what its snapshot saw, and every lane that did not create the message adds its site.
func TestCmiSeedAndAddRacePostgres(t *testing.T) {
type sched struct {
name string
run func(f *cmiFixture, hash []byte)
}
schedules := []sched{
{"sequential", func(f *cmiFixture, h []byte) {
a, b := f.lane("YUL", h), f.lane("YOW", h)
a.obs()
a.icm(false)
b.obs()
b.icm(false)
}},
{"both_heard_then_a_inserts", func(f *cmiFixture, h []byte) {
a, b := f.lane("YUL", h), f.lane("YOW", h)
a.obs()
b.obs()
a.icm(false)
b.icm(false)
}},
{"both_heard_then_b_inserts", func(f *cmiFixture, h []byte) {
a, b := f.lane("YUL", h), f.lane("YOW", h)
a.obs()
b.obs()
b.icm(false)
a.icm(false)
}},
{"b_heard_during_a_insert_and_waits", func(f *cmiFixture, h []byte) {
a, b := f.lane("YUL", h), f.lane("YOW", h)
a.obs()
a.open(false)
b.obs()
wait := b.icmWaiting()
a.end(true)
if m := wait(); m != nil {
f.t.Fatalf("B created %+v", m)
}
}},
{"b_heard_during_a_insert_inserts_after", func(f *cmiFixture, h []byte) {
a, b := f.lane("YUL", h), f.lane("YOW", h)
a.obs()
a.open(false)
b.obs()
a.end(true)
b.icm(false)
}},
{"both_heard_b_waits_on_a", func(f *cmiFixture, h []byte) {
a, b := f.lane("YUL", h), f.lane("YOW", h)
a.obs()
b.obs()
a.open(false)
wait := b.icmWaiting()
a.end(true)
wait()
}},
{"live_waits_on_historical_insert", func(f *cmiFixture, h []byte) {
old, b := f.lane("YYZ", h), f.lane("YOW", h)
old.obs()
old.open(true)
b.obs()
wait := b.icmWaiting()
old.end(true)
wait()
}},
{"a_rolls_back_while_b_waits", func(f *cmiFixture, h []byte) {
a, b := f.lane("YUL", h), f.lane("YOW", h)
a.obs()
a.open(false)
b.obs()
wait := b.icmWaiting()
a.end(false)
if m := wait(); m == nil {
f.t.Fatal("B did not create the message after A rolled back")
}
}},
{"two_wait_on_a", func(f *cmiFixture, h []byte) {
a, b, c := f.lane("YUL", h), f.lane("YOW", h), f.lane("YYZ", h)
a.obs()
a.open(false)
b.obs()
c.obs()
waitB := b.icmWaiting()
waitC := c.icmWaiting()
a.end(true)
waitB()
waitC()
}},
{"later_hearings_same_and_new_site", func(f *cmiFixture, h []byte) {
a := f.lane("YUL", h)
a.obs()
a.icm(false)
a.obs()
a.icm(false)
a.site = "YQB"
a.obs()
a.icm(false)
}},
}
for _, s := range schedules {
t.Run(s.name, func(t *testing.T) {
f := newCmiFixture(t)
s.run(f, f.packet("race", 3))
var messages int
if err := f.pool.QueryRow(f.ctx, "SELECT count(*) FROM channel_messages").Scan(&messages); err != nil || messages != 1 {
t.Fatalf("%s: %d messages, %v", s.name, messages, err)
}
f.checkDefinition(s.name)
})
}
}
@@ -0,0 +1,11 @@
-- Copyright 2026 Beacon Contributors
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- One row per (channel message, site that heard it); region message pages read this instead of every observation.
CREATE TABLE channel_message_iatas (
message_id bigint NOT NULL REFERENCES channel_messages(id) ON DELETE CASCADE,
iata character(3) NOT NULL,
channel_id integer NOT NULL,
scope_id integer,
PRIMARY KEY (message_id, iata)
);
+5
View File
@@ -0,0 +1,5 @@
-- Copyright 2026 Beacon Contributors
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- Channel pages filtered by site walk one (channel, site) range each.
CREATE INDEX CONCURRENTLY idx_cmi_channel_site ON channel_message_iatas USING btree (channel_id, iata, message_id DESC);
+5
View File
@@ -0,0 +1,5 @@
-- Copyright 2026 Beacon Contributors
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- All-channel pages filtered by site walk one site range each.
CREATE INDEX CONCURRENTLY idx_cmi_site ON channel_message_iatas USING btree (iata, message_id DESC);
@@ -0,0 +1,5 @@
-- Copyright 2026 Beacon Contributors
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- Channel pages filtered by site and scope.
CREATE INDEX CONCURRENTLY idx_cmi_channel_site_scope ON channel_message_iatas USING btree (channel_id, iata, scope_id, message_id DESC) WHERE (scope_id IS NOT NULL);
+5
View File
@@ -0,0 +1,5 @@
-- Copyright 2026 Beacon Contributors
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- Channel pages filtered by scope alone: rare and absent scopes read a few entries instead of the channel.
CREATE INDEX CONCURRENTLY idx_cmi_channel_scope ON channel_message_iatas USING btree (channel_id, scope_id, message_id DESC) WHERE (scope_id IS NOT NULL);
+5
View File
@@ -0,0 +1,5 @@
-- Copyright 2026 Beacon Contributors
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- All-channel pages filtered by scope alone.
CREATE INDEX CONCURRENTLY idx_cmi_scope ON channel_message_iatas USING btree (scope_id, message_id DESC) WHERE (scope_id IS NOT NULL);
@@ -0,0 +1,5 @@
-- Copyright 2026 Beacon Contributors
-- SPDX-License-Identifier: AGPL-3.0-or-later
-- Unfiltered channel pages walk the channel by id, the cursor's own column.
CREATE INDEX CONCURRENTLY idx_channel_messages_channel_id ON channel_messages USING btree (channel_id, id DESC);
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop (Semi)",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop (Semi)",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop (Semi)",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop (Semi)",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop (Semi)",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop (Semi)",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop (Semi)",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop (Semi)",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop (Semi)",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop (Semi)",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop (Semi)",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop (Semi)",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,16 @@
[
"Sort",
" Limit [InitPlan]",
" Result",
" Unique",
" Index Only Scan on channel_message_iatas i using idx_cmi_scope scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,16 @@
[
"Sort",
" Limit [InitPlan]",
" Result",
" Unique",
" Index Only Scan on channel_message_iatas i using idx_cmi_scope scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,16 @@
[
"Sort",
" Limit [InitPlan]",
" Result",
" Unique",
" Index Only Scan on channel_message_iatas i using idx_cmi_scope scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,16 @@
[
"Sort",
" Limit [InitPlan]",
" Result",
" Unique",
" Index Only Scan on channel_message_iatas i using idx_cmi_scope scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,16 @@
[
"Sort",
" Limit [InitPlan]",
" Result",
" Unique",
" Index Only Scan on channel_message_iatas i using idx_cmi_scope scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,18 @@
[
"Sort",
" Limit [InitPlan]",
" Result",
" Unique",
" Index Only Scan on channel_message_iatas i using idx_cmi_scope scan=Forward",
" Hash Join (Left)",
" Nested Loop",
" Merge Join",
" Index Scan on channels c using channels_pkey scan=Forward",
" Sort",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Hash",
" Seq Scan on transport_scopes ts"
]
@@ -0,0 +1,16 @@
[
"Sort",
" Limit [InitPlan]",
" Result",
" Unique",
" Index Only Scan on channel_message_iatas i using idx_cmi_scope scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,16 @@
[
"Sort",
" Limit [InitPlan]",
" Result",
" Unique",
" Index Only Scan on channel_message_iatas i using idx_cmi_scope scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop (Semi)",
" Nested Loop",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop (Semi)",
" Nested Loop",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,13 @@
[
"Limit",
" Sort",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop (Semi)",
" Nested Loop",
" Index Scan on channel_messages cm using idx_channel_messages_channel scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,14 @@
[
"Limit",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop (Semi)",
" Nested Loop",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Backward",
" Memoize",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Memoize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,16 @@
[
"Limit",
" Sort",
" Nested Loop (Semi)",
" Nested Loop (Left)",
" Nested Loop",
" Hash Join",
" Bitmap Heap Scan on packets p",
" Bitmap Index Scan using idx_packets_scope",
" Hash",
" Seq Scan on channel_messages cm",
" Index Scan on channels c using channels_pkey scan=Forward",
" Materialize",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward",
" Index Only Scan on packet_observations po using idx_observations_packet scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Group",
" Sort",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Group",
" Sort",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Group",
" Sort",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Group",
" Sort",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Group",
" Sort",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Group",
" Sort",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Group",
" Sort",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Group",
" Sort",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Group",
" Sort",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Group",
" Sort",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Group",
" Sort",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Sort",
" Aggregate partial=Simple {Hashed}",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
@@ -0,0 +1,19 @@
[
"Sort",
" Limit [InitPlan]",
" Group",
" Sort",
" Nested Loop",
" Function Scan",
" Limit",
" Index Only Scan on channel_message_iatas i using idx_cmi_channel_site scan=Forward",
" Nested Loop (Left)",
" Nested Loop",
" Nested Loop",
" Nested Loop",
" CTE Scan",
" Index Scan on channel_messages cm using channel_messages_pkey scan=Forward",
" Index Scan on channels c using channels_pkey scan=Forward",
" Index Scan on packets p using packets_pkey scan=Forward",
" Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward"
]
Loaded 100 of 321 files, more files were not shown because too many files have changed in this diff. Show more