mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-23 00:41:29 +00:00
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:
1 parent
232588c361
commit
5ded8d925f
321 files changed
+13154
-706
No files matched your search
@@ -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
@@ -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 ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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
Reference in new issue
Block a user