diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b54010a..7132321 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ 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|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)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)Postgres$' -count=1 -v - name: Verify route retention task against PostgreSQL 16 env: diff --git a/cmd/beacon/main.go b/cmd/beacon/main.go index 1ca09a8..c6b772c 100644 --- a/cmd/beacon/main.go +++ b/cmd/beacon/main.go @@ -50,7 +50,7 @@ import ( var version = "dev" // @title MeshCore Beacon API -// @version 2.0.8 +// @version 2.1.0 // @description MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket. // @description REST requests share a configurable per-client rate limit (default 600/minute and a 600-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget. // @description WebSocket upgrade attempts at /ws have a configurable per-client limit (default 10/minute, including failed handshakes). Rate exhaustion returns HTTP 429 with Retry-After before upgrade. An accepted socket exceeding the concurrent cap closes with code 1013 before hello; established connections remain open. diff --git a/config.yaml.example b/config.yaml.example index ef3b84e..33d8592 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -58,8 +58,8 @@ ratelimit: # Keep max_connections well above that. #database: # # custom_all (default): every API statement gets a custom plan. -# # split: statements admitted to the generic-plan class run on cached generic plans. No statement -# # is admitted yet, so split has no effect; keep custom_all. +# # split: statements admitted to the generic-plan class (db/planpolicy/cached_gen.go) run on cached +# # generic plans on api_cached; every other API statement keeps custom plans. # # single: rollback. One API pool on today's settings; every guard family observes. # api_plan_layout: custom_all diff --git a/db/migrate_integration_test.go b/db/migrate_integration_test.go index a7fdd89..007e251 100644 --- a/db/migrate_integration_test.go +++ b/db/migrate_integration_test.go @@ -198,3 +198,41 @@ FROM pg_index WHERE indrelid = 'known_routes'::regclass`).Scan(&got, &bad); err t.Fatalf("known_routes indexes = %v, want %v; not valid and ready: %v", got, want, bad) } } + +// The operator pre-builds the four packet indexes; the migrations record them, not fail or rebuild them. +func TestRunMigrationsPacketsIndexesPrebuiltPostgres(t *testing.T) { + ctx, pool := schemaPool(t) + if err := RunMigrations(ctx, pool); err != nil { + t.Fatal(err) + } + oids := map[string]uint32{} + for _, idx := range packetsEndStateIndexes { + var oid uint32 + if err := pool.QueryRow(ctx, "SELECT $1::regclass::oid", idx).Scan(&oid); err != nil { + t.Fatalf("%s: %v", idx, err) + } + oids[idx] = oid + } + files := []string{ + "029_packets_last_heard_hash.sql", + "030_packets_type_last_heard.sql", + "031_packets_scope_last_heard.sql", + "032_packets_heard_inverted.sql", + } + if _, err := pool.Exec(ctx, "DELETE FROM schema_migrations WHERE filename = ANY($1)", files); err != nil { + t.Fatal(err) + } + if err := RunMigrations(ctx, pool); err != nil { + t.Fatalf("029-032 over pre-built indexes: %v", err) + } + checkRecorded(t, ctx, pool, files...) + for idx, before := range oids { + var after uint32 + if err := pool.QueryRow(ctx, "SELECT $1::regclass::oid", idx).Scan(&after); err != nil { + t.Fatalf("%s: %v", idx, err) + } + if after != before { + t.Errorf("%s rebuilt: oid %d -> %d", idx, before, after) + } + } +} diff --git a/db/migrations/025_packets_payload_type_domain.sql b/db/migrations/025_packets_payload_type_domain.sql new file mode 100644 index 0000000..0f5ed5e --- /dev/null +++ b/db/migrations/025_packets_payload_type_domain.sql @@ -0,0 +1,5 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- Decoded headers carry 4 payload-type bits; an absent type filter is passed as the full domain. +ALTER TABLE packets ADD CONSTRAINT packets_payload_type_domain CHECK (payload_type BETWEEN 0 AND 15) NOT VALID; diff --git a/db/migrations/026_validate_packets_payload_type_domain.sql b/db/migrations/026_validate_packets_payload_type_domain.sql new file mode 100644 index 0000000..515c03a --- /dev/null +++ b/db/migrations/026_validate_packets_payload_type_domain.sql @@ -0,0 +1,5 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- Scans packets under SHARE UPDATE EXCLUSIVE; writes continue. +ALTER TABLE packets VALIDATE CONSTRAINT packets_payload_type_domain; diff --git a/db/migrations/027_packets_route_type_domain.sql b/db/migrations/027_packets_route_type_domain.sql new file mode 100644 index 0000000..cd6f6df --- /dev/null +++ b/db/migrations/027_packets_route_type_domain.sql @@ -0,0 +1,5 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- Decoded headers carry 2 route-type bits; an absent type filter is passed as the full domain. +ALTER TABLE packets ADD CONSTRAINT packets_route_type_domain CHECK (route_type BETWEEN 0 AND 3) NOT VALID; diff --git a/db/migrations/028_validate_packets_route_type_domain.sql b/db/migrations/028_validate_packets_route_type_domain.sql new file mode 100644 index 0000000..bc7f482 --- /dev/null +++ b/db/migrations/028_validate_packets_route_type_domain.sql @@ -0,0 +1,5 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- Scans packets under SHARE UPDATE EXCLUSIVE; writes continue. +ALTER TABLE packets VALIDATE CONSTRAINT packets_route_type_domain; diff --git a/db/migrations/029_packets_last_heard_hash.sql b/db/migrations/029_packets_last_heard_hash.sql new file mode 100644 index 0000000..1609c42 --- /dev/null +++ b/db/migrations/029_packets_last_heard_hash.sql @@ -0,0 +1,5 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- Packet pages walk (last_heard_at, packet_hash); the hash breaks millisecond ties. +CREATE INDEX CONCURRENTLY idx_packets_last_heard_hash ON packets USING btree (last_heard_at DESC, packet_hash DESC); diff --git a/db/migrations/030_packets_type_last_heard.sql b/db/migrations/030_packets_type_last_heard.sql new file mode 100644 index 0000000..c7772bc --- /dev/null +++ b/db/migrations/030_packets_type_last_heard.sql @@ -0,0 +1,5 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- Typed pages read one range per (payload type, route type) pair. +CREATE INDEX CONCURRENTLY idx_packets_type_last_heard ON packets USING btree (payload_type, route_type, last_heard_at DESC, packet_hash DESC); diff --git a/db/migrations/031_packets_scope_last_heard.sql b/db/migrations/031_packets_scope_last_heard.sql new file mode 100644 index 0000000..5b009f4 --- /dev/null +++ b/db/migrations/031_packets_scope_last_heard.sql @@ -0,0 +1,5 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- Scoped pages read one range per scope; the included columns keep typed and windowed scope pages index-only. +CREATE INDEX CONCURRENTLY idx_packets_scope_last_heard ON packets USING btree (scope_id, last_heard_at DESC, packet_hash DESC) INCLUDE (payload_type, route_type, first_heard_at) WHERE (scope_id IS NOT NULL); diff --git a/db/migrations/032_packets_heard_inverted.sql b/db/migrations/032_packets_heard_inverted.sql new file mode 100644 index 0000000..028fa28 --- /dev/null +++ b/db/migrations/032_packets_heard_inverted.sql @@ -0,0 +1,5 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- Concurrent upserts can leave last_heard_at below first_heard_at; window pages read those rows here. +CREATE INDEX CONCURRENTLY idx_packets_heard_inverted ON packets USING btree (first_heard_at) INCLUDE (last_heard_at, packet_hash, payload_type, route_type, scope_id) WHERE ((first_heard_at > last_heard_at) IS TRUE); diff --git a/db/migrations/033_packets_heard_inverted_stat.sql b/db/migrations/033_packets_heard_inverted_stat.sql new file mode 100644 index 0000000..2b2a6e0 --- /dev/null +++ b/db/migrations/033_packets_heard_inverted_stat.sql @@ -0,0 +1,6 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- Without these statistics the planner assumes a third of packets are inverted and misprices the window's second branch. +CREATE STATISTICS packets_heard_inverted_stat ON ((first_heard_at > last_heard_at)) FROM packets; +ANALYZE packets; diff --git a/db/nodes.go b/db/nodes.go index ee248cd..436389e 100644 --- a/db/nodes.go +++ b/db/nodes.go @@ -90,12 +90,32 @@ func (s *Store) SetNodeDefaultScope(ctx context.Context, nodeID uuid.UUID, scope }) } -func (s *Store) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, limit int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) { - var cursorTS pgtype.Timestamptz - if cursor > 0 { - cursorTS = pgtype.Timestamptz{Time: time.UnixMilli(cursor), Valid: true} +// uuidKeysetCursor maps the cursors of a (last_seen DESC, id DESC) list to the +// statement's row-cursor pair. A pageCursor (at, id) wins. A numeric cursor +// pairs its millisecond with the nil uuid, which sorts below every id, so the +// row comparison keeps today's "last_seen < ms". Page 1 starts at infinity. +func uuidKeysetCursor(cursor int64, at *time.Time, id uuid.UUID) (pgtype.Timestamptz, uuid.UUID) { + switch { + case at != nil: + return pgtype.Timestamptz{Time: *at, Valid: true}, id + case cursor > 0: + return pgtype.Timestamptz{Time: time.UnixMilli(cursor), Valid: true}, uuid.Nil + default: + return pgtype.Timestamptz{InfinityModifier: pgtype.Infinity, Valid: true}, uuid.Nil } - rows, err := s.q.ListNodes(ctx, sqlc.ListNodesParams{ +} + +// listNodesParams builds the statement's arguments for one ListNodes request. +func listNodesParams(nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, pageCursor *api.NodeCursor, limit int32, includeNeighbors bool) sqlc.ListNodesParams { + var ( + at *time.Time + id uuid.UUID + ) + if pageCursor != nil { + at, id = &pageCursor.LastSeen, pageCursor.ID + } + cursorTS, cursorID := uuidKeysetCursor(cursor, at, id) + return sqlc.ListNodesParams{ Column1: nodeType, Column2: iatas, Column3: tristate(supportsMultibytePaths), @@ -107,9 +127,14 @@ func (s *Store) ListNodes(ctx context.Context, nodeType int16, iatas []string, s Column9: scope, Column10: includeNeighbors, Column11: pubkeyPrefix, - }) + Column12: cursorID, + } +} + +func (s *Store) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, pageCursor *api.NodeCursor, limit int32, includeNeighbors bool) (api.NodePage, error) { + rows, err := s.q.ListNodes(ctx, listNodesParams(nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, pubkeyPrefix, name, scope, cursor, pageCursor, limit, includeNeighbors)) if err != nil { - return api.Page[api.NodeSummary]{}, err + return api.NodePage{}, err } hasMore := len(rows) > int(limit) if hasMore { @@ -143,16 +168,15 @@ func (s *Store) ListNodes(ctx context.Context, nodeType int16, iatas []string, s } items = append(items, node) } - var nextCursor *int64 - if hasMore && len(items) > 0 { - ms := rows[len(rows)-1].LastSeen.Time.UnixMilli() - nextCursor = &ms + page := api.NodePage{Page: api.Page[api.NodeSummary]{Items: items, HasMore: hasMore}} + if hasMore && len(rows) > 0 { + last := rows[len(rows)-1] + ms := last.LastSeen.Time.UnixMilli() + next := api.NodeCursor{LastSeen: last.LastSeen.Time, ID: last.ID}.String() + page.NextCursor = &ms + page.NextPageCursor = &next } - return api.Page[api.NodeSummary]{ - Items: items, - NextCursor: nextCursor, - HasMore: hasMore, - }, nil + return page, nil } func (s *Store) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error) { diff --git a/db/nodes_integration_test.go b/db/nodes_integration_test.go index 4a45f8a..ea64730 100644 --- a/db/nodes_integration_test.go +++ b/db/nodes_integration_test.go @@ -15,6 +15,7 @@ import ( "time" sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc" + "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) @@ -70,7 +71,8 @@ ANALYZE nodes; ANALYZE node_iatas; ANALYZE node_neighbors; ANALYZE observers; AN t.Fatal(err) } query := sqlc.New(tx) - base := sqlc.ListNodesParams{Column1: int16(0), Column3: "any", Column4: "any", Column6: "", Limit: 51} + page1 := pgtype.Timestamptz{InfinityModifier: pgtype.Infinity, Valid: true} + base := sqlc.ListNodesParams{Column1: int16(0), Column3: "any", Column4: "any", Column6: "", Column7: page1, Limit: 51, Column12: uuid.Nil} for _, tc := range []struct { name string change func(*sqlc.ListNodesParams) @@ -154,7 +156,7 @@ ANALYZE nodes; ANALYZE node_iatas; ANALYZE node_neighbors; ANALYZE observers; AN sql := strings.Split(strings.Split(queries, "-- name: ListNodes :many\n")[1], "\n-- name:")[0] var planJSON []byte err = tx.QueryRow(ctx, "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) "+sql, - int16(0), []string(nil), "any", "any", []byte(nil), "", nil, int32(51), "", false, "").Scan(&planJSON) + int16(0), []string(nil), "any", "any", []byte(nil), "", page1, int32(51), "", false, "", uuid.Nil).Scan(&planJSON) if err != nil { t.Fatal(err) } diff --git a/db/nodes_observers_cursor_integration_test.go b/db/nodes_observers_cursor_integration_test.go new file mode 100644 index 0000000..fbfcb3e --- /dev/null +++ b/db/nodes_observers_cursor_integration_test.go @@ -0,0 +1,517 @@ +// Copyright 2026 Beacon Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +package db + +import ( + "context" + "fmt" + "slices" + "testing" + "time" + + sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc" + "github.com/MeshCore-Beacon/beacon-server/internal/api" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" +) + +// keysetFixture: 300 nodes and 120 observers. Row i sits in block (i-1)/size at position r = (i-1)%size; rows +// r = 0..2 of every block share one millisecond (12 tied groups per table), r = 0 and r = 1 also share one +// microsecond in blocks 0..2 (3 exact pairs). Observers are within minutes of now() so status splits online and +// offline; 20 (i % 6 = 0) have NULL last_status_at, the rest have last_status_at 60-120 s before last_seen. +const keysetFixture = ` +INSERT INTO transport_scopes (id, name, transport_key, key_fingerprint) VALUES + (1, '#test', '\x01', '\x02'), (2, '#other', '\x03', '\x04'); +INSERT INTO nodes (id, public_key, node_type, name, last_seen, default_scope_id) +SELECT md5('node' || i)::uuid, + CASE WHEN i % 10 = 0 THEN '\x0a'::bytea || sha256(int4send(i)) ELSE sha256(int4send(i)) END, + 1 + i % 4, 'node-' || i, + '2026-03-01'::timestamptz - (b * 25 + CASE WHEN r < 3 THEN 0 ELSE r END) * interval '1 second' + + CASE WHEN r < 3 AND NOT (b < 3 AND r = 1) THEN r ELSE 0 END * interval '1 microsecond', + CASE WHEN i % 3 = 0 THEN 1 END +FROM generate_series(1, 300) i, LATERAL (SELECT (i - 1) / 25 AS b, (i - 1) % 25 AS r) k; +INSERT INTO node_iatas (node_id, iata) SELECT md5('node' || i)::uuid, 'YVR' FROM generate_series(1, 300) i WHERE i % 2 = 0; +INSERT INTO node_iatas (node_id, iata) SELECT md5('node' || i)::uuid, 'YYJ' FROM generate_series(1, 300) i WHERE i % 5 = 0; +INSERT INTO observers (id, public_key, display_name, observer_type, last_iata, last_seen, last_status_at) +SELECT md5('observer' || i)::uuid, sha256(int4send(100000 + i)), 'obs-' || i, + CASE WHEN i % 3 = 0 THEN 'repeater' ELSE 'companion' END, + CASE WHEN i % 2 = 0 THEN 'YVR' ELSE 'YYZ' END, + ls, CASE WHEN i % 6 = 0 THEN NULL ELSE ls - interval '1 minute' - (i % 4) * interval '20 seconds' END +FROM generate_series(1, 120) i, + LATERAL (SELECT (i - 1) / 10 AS b, (i - 1) % 10 AS r) k, + LATERAL (SELECT date_trunc('second', now()) - interval '1 second' + - (b * 10 + CASE WHEN r < 3 THEN 0 ELSE r END) * interval '5 seconds' + + CASE WHEN r < 3 AND NOT (b < 3 AND r = 1) THEN r ELSE 0 END * interval '1 microsecond') t(ls); +INSERT INTO observer_brokers (observer_id, broker_name) SELECT md5('observer' || i)::uuid, 'b1' FROM generate_series(1, 120) i; +INSERT INTO observer_brokers (observer_id, broker_name) SELECT md5('observer' || i)::uuid, 'b2' FROM generate_series(1, 120) i WHERE i % 4 = 0; +INSERT INTO observer_scopes (observer_id, scope_id) SELECT md5('observer' || i)::uuid, 1 FROM generate_series(1, 120) i WHERE i % 5 = 0; +INSERT INTO observer_scopes (observer_id, scope_id) SELECT md5('observer' || i)::uuid, 2 FROM generate_series(1, 120) i WHERE i % 7 = 0; +ANALYZE nodes, node_iatas, observers, observer_brokers, observer_scopes, transport_scopes;` + +// keysetTx is a rolled-back transaction holding the schema after the small-table index drops and keysetFixture. +func keysetTx(t *testing.T) (context.Context, pgx.Tx, *Store) { + t.Helper() + ctx, tx := retentionTx(t) + applyBaseline(t, ctx, tx) + applyMigrationsTx(t, ctx, tx, smallTableMigrations...) + mustExec(t, ctx, tx, keysetFixture) + // enable_seqscan = off pushes every plan past jit_above_cost; JIT-compiling thousands of queries times out on + // servers built with LLVM. + mustExec(t, ctx, tx, "SET LOCAL jit = off") + return ctx, tx, &Store{q: sqlc.New(tx)} +} + +// keysetRow is one row of a table in (last_seen DESC, id DESC) order. +type keysetRow struct { + ID uuid.UUID + LastSeen time.Time + LastStatusAt pgtype.Timestamptz +} + +func keysetOrder(t *testing.T, ctx context.Context, tx pgx.Tx, sql string) []keysetRow { + t.Helper() + return legacyRows[keysetRow](t, ctx, tx, sql) +} + +// tiedMillis are the epoch milliseconds that hold more than one row, newest first. +func tiedMillis(rows []keysetRow) []int64 { + var out []int64 + for i := 1; i < len(rows); i++ { + ms := rows[i].LastSeen.UnixMilli() + if ms == rows[i-1].LastSeen.UnixMilli() && !slices.Contains(out, ms) { + out = append(out, ms) + } + } + return out +} + +func keysetIDs(rows []keysetRow) []uuid.UUID { + out := make([]uuid.UUID, len(rows)) + for i, r := range rows { + out[i] = r.ID + } + return out +} + +func legacyCursorTS(cursor int64) pgtype.Timestamptz { + if cursor > 0 { + return pgtype.Timestamptz{Time: time.UnixMilli(cursor), Valid: true} + } + return pgtype.Timestamptz{} +} + +type nodeShape struct { + name string + nodeType int16 + iatas []string + nameFilter string + scope string + prefix string + withNeighbours bool +} + +func nodeShapes() []nodeShape { + return []nodeShape{ + {name: "unfiltered"}, + {name: "unfiltered neighbours", withNeighbours: true}, + {name: "type 2", nodeType: 2}, + {name: "iatas YVR", iatas: []string{"YVR"}}, + {name: "name node-1", nameFilter: "node-1"}, + {name: "scope #test", scope: "#test"}, + {name: "pubkeyPrefix 0a", prefix: "0a"}, + } +} + +func legacyNodeRows(t *testing.T, ctx context.Context, tx pgx.Tx, sql string, sh nodeShape, cursor int64, pageLimit int32) []sqlc.ListNodesRow { + t.Helper() + return legacyRows[sqlc.ListNodesRow](t, ctx, tx, sql, sh.nodeType, sh.iatas, "any", "any", []byte(nil), sh.nameFilter, + legacyCursorTS(cursor), pageLimit, sh.scope, sh.withNeighbours, sh.prefix) +} + +func newNodeRows(t *testing.T, ctx context.Context, s *Store, sh nodeShape, cursor int64, pc *api.NodeCursor, limit int32) []sqlc.ListNodesRow { + t.Helper() + rows, err := s.q.ListNodes(ctx, listNodesParams(sh.nodeType, sh.iatas, nil, nil, nil, sh.prefix, sh.nameFilter, sh.scope, cursor, pc, limit, sh.withNeighbours)) + if err != nil { + t.Fatalf("%s: %v", sh.name, err) + } + return rows +} + +func nodeKeys(rows []sqlc.ListNodesRow) []string { + out := make([]string, len(rows)) + for i, r := range rows { + out[i] = fmt.Sprintf("%s/%s", r.LastSeen.Time.Format("15:04:05.000000"), r.ID.String()[:4]) + } + return out +} + +func nodeLastSeen(rows []sqlc.ListNodesRow) []time.Time { + out := make([]time.Time, len(rows)) + for i, r := range rows { + out[i] = r.LastSeen.Time + } + return out +} + +// Legacy cursors return today's rows: the nil uuid sorts below every id, so (ms, nil) is today's "< ms". +func TestNodesLegacyCursorMatchesTodayPostgres(t *testing.T) { + ctx, tx, s := keysetTx(t) + all := keysetOrder(t, ctx, tx, "SELECT id, last_seen, NULL::timestamptz FROM nodes ORDER BY last_seen DESC, id DESC") + tied := tiedMillis(all) + if len(tied) != 12 { + t.Fatalf("fixture: %d tied milliseconds, want 12", len(tied)) + } + cursors := append([]int64{0, -5, all[3].LastSeen.Add(-500 * time.Millisecond).UnixMilli()}, tied...) + nonEmpty := 0 + for _, setting := range planSettings { + mustExec(t, ctx, tx, setting) + for _, sh := range nodeShapes() { + for _, c := range cursors { + for _, limit := range []int32{1, 2, 51} { + want := legacyNodeRows(t, ctx, tx, legacyListNodesDet, sh, c, limit+1) + got := newNodeRows(t, ctx, s, sh, c, nil, limit) + if !sameRows(got, want) { + t.Fatalf("%q %s cursor=%d limit=%d:\n got %v\n want %v", setting, sh.name, c, limit, nodeKeys(got), nodeKeys(want)) + } + raw := legacyNodeRows(t, ctx, tx, legacyListNodes, sh, c, limit+1) + if !slices.EqualFunc(nodeLastSeen(got), nodeLastSeen(raw), time.Time.Equal) { + t.Fatalf("%q %s cursor=%d limit=%d vs raw:\n got %v\n raw %v", setting, sh.name, c, limit, nodeKeys(got), nodeKeys(raw)) + } + if len(got) > 0 { + nonEmpty++ + } + } + } + } + } + mustExec(t, ctx, tx, "RESET plan_cache_mode; RESET enable_seqscan") + if nonEmpty < 500 { + t.Fatalf("only %d non-empty comparisons; the fixture does not exercise the statement", nonEmpty) + } +} + +// A page boundary inside a tied millisecond (and inside an exact-microsecond pair): a pageCursor page starts +// with the tied rows the legacy cursor skips; a limit-1 pageCursor walk returns every node once, in order. +func TestNodesPageCursorTiesPostgres(t *testing.T) { + ctx, tx, s := keysetTx(t) + all := keysetOrder(t, ctx, tx, "SELECT id, last_seen, NULL::timestamptz FROM nodes ORDER BY last_seen DESC, id DESC") + if len(all) != 300 { + t.Fatalf("fixture: %d nodes", len(all)) + } + // The smallest page that ends inside a tied millisecond, and the smallest that ends between two rows with the + // same microsecond, where only the id separates them. + var limits []int + for i := 1; i < len(all) && len(limits) < 2; i++ { + sameMS := all[i].LastSeen.UnixMilli() == all[i-1].LastSeen.UnixMilli() + if (len(limits) == 0 && sameMS) || (len(limits) == 1 && all[i].LastSeen.Equal(all[i-1].LastSeen)) { + limits = append(limits, i) + } + } + if len(limits) != 2 || all[limits[0]].LastSeen.Equal(all[limits[0]-1].LastSeen) { + t.Fatalf("fixture: boundaries %v", limits) + } + none := nodeShape{name: "unfiltered"} + for _, limit := range limits { + page1, err := s.ListNodes(ctx, 0, nil, nil, nil, nil, "", "", "", 0, nil, int32(limit), false) + if err != nil { + t.Fatal(err) + } + last := all[limit-1] + lastMS := last.LastSeen.UnixMilli() + if !page1.HasMore || page1.NextCursor == nil || *page1.NextCursor != lastMS || page1.NextPageCursor == nil || + *page1.NextPageCursor != (api.NodeCursor{LastSeen: last.LastSeen, ID: last.ID}).String() { + t.Fatalf("limit %d page 1 cursors: %+v", limit, page1) + } + for i, n := range page1.Items { + if n.ID != all[i].ID { + t.Fatalf("limit %d page 1 row %d: %s, want %s", limit, i, n.ID, all[i].ID) + } + } + + // Legacy cursor: today's page, which skips the rest of the tied millisecond. + legacyNext := legacyNodeRows(t, ctx, tx, legacyListNodesDet, none, lastMS, int32(limit)+1) + gotLegacy := newNodeRows(t, ctx, s, none, lastMS, nil, int32(limit)) + if !sameRows(gotLegacy, legacyNext) { + t.Fatalf("limit %d legacy cursor:\n got %v\n want %v", limit, nodeKeys(gotLegacy), nodeKeys(legacyNext)) + } + if len(gotLegacy) == 0 || gotLegacy[0].LastSeen.Time.UnixMilli() >= lastMS { + t.Fatalf("limit %d legacy cursor did not skip the tied millisecond", limit) + } + + // pageCursor (sent with the legacy cursor, which it overrides): the skipped tied rows, then the legacy page. + pc, err := api.ParseNodeCursor(*page1.NextPageCursor) + if err != nil { + t.Fatal(err) + } + var want []uuid.UUID + for _, r := range all[limit:] { + if r.LastSeen.UnixMilli() != lastMS { + break + } + want = append(want, r.ID) + } + if len(want) == 0 { + t.Fatalf("limit %d: boundary is not inside a tied millisecond", limit) + } + for _, r := range legacyNext { + want = append(want, r.ID) + } + want = want[:min(len(want), limit+1)] + gotPC := newNodeRows(t, ctx, s, none, lastMS, pc, int32(limit)) + gotIDs := make([]uuid.UUID, len(gotPC)) + for i, r := range gotPC { + gotIDs[i] = r.ID + } + if !slices.Equal(gotIDs, want) { + t.Fatalf("limit %d pageCursor page:\n got %v\n want %v", limit, gotIDs, want) + } + } + + // A limit-1 walk following nextPageCursor, with nextCursor sent alongside: every node once, in order. + var walked []uuid.UUID + var cursor int64 + var pageCursor *api.NodeCursor + for range len(all) + 2 { + page, err := s.ListNodes(ctx, 0, nil, nil, nil, nil, "", "", "", cursor, pageCursor, 1, false) + if err != nil { + t.Fatal(err) + } + for _, n := range page.Items { + walked = append(walked, n.ID) + } + if !page.HasMore { + if page.NextPageCursor != nil || page.NextCursor != nil { + t.Fatal("last page carries a cursor") + } + break + } + cursor = *page.NextCursor + if pageCursor, err = api.ParseNodeCursor(*page.NextPageCursor); err != nil { + t.Fatal(err) + } + } + if !slices.Equal(walked, keysetIDs(all)) { + t.Fatalf("limit-1 walk: %d rows, want %d in (last_seen DESC, id DESC) order", len(walked), len(all)) + } +} + +// legacyObserverRow is today's ListObservers row. +type legacyObserverRow struct { + ID uuid.UUID + DisplayName *string + ObserverType *string + LastStatusAt pgtype.Timestamptz + RadioFreqMhz *float32 + RadioSf *int16 + RadioBwKhz *float32 + Scopes []string + Status string + Iata string +} + +type observerShape struct { + name string + iatas []string + observerType, broker, status, nameF, scope string +} + +func observerShapes() []observerShape { + return []observerShape{ + {name: "unfiltered"}, + {name: "iatas YVR", iatas: []string{"YVR"}}, + {name: "type repeater", observerType: "repeater"}, + {name: "status online", status: "online"}, + {name: "status offline", status: "offline"}, + {name: "broker b2", broker: "b2"}, + {name: "name obs-1", nameF: "obs-1"}, + {name: "scope #test", scope: "#test"}, + } +} + +func legacyObserverRows(t *testing.T, ctx context.Context, tx pgx.Tx, sql string, sh observerShape, cts pgtype.Timestamptz, pageLimit int32) []legacyObserverRow { + t.Helper() + return legacyRows[legacyObserverRow](t, ctx, tx, sql, sh.iatas, sh.observerType, sh.broker, sh.status, sh.nameF, cts, pageLimit, sh.scope) +} + +// newObserverRows runs today's columns of the new statement for one request. +func newObserverRows(t *testing.T, ctx context.Context, s *Store, sh observerShape, cursor int64, pc *api.ObserverCursor, limit int32) []legacyObserverRow { + t.Helper() + rows, err := s.q.ListObservers(ctx, listObserversParams(sh.iatas, sh.observerType, sh.broker, sh.status, sh.nameF, sh.scope, cursor, pc, limit)) + if err != nil { + t.Fatalf("%s: %v", sh.name, err) + } + out := make([]legacyObserverRow, len(rows)) + for i, r := range rows { + out[i] = legacyObserverRow{r.ID, r.DisplayName, r.ObserverType, r.LastStatusAt, r.RadioFreqMhz, r.RadioSf, r.RadioBwKhz, r.Scopes, r.Status, r.Iata} + } + return out +} + +func observerIDs(rows []legacyObserverRow) []uuid.UUID { + out := make([]uuid.UUID, len(rows)) + for i, r := range rows { + out[i] = r.ID + } + return out +} + +func TestObserversLegacyCursorMatchesTodayPostgres(t *testing.T) { + ctx, tx, s := keysetTx(t) + all := keysetOrder(t, ctx, tx, "SELECT id, last_seen, last_status_at FROM observers ORDER BY last_seen DESC, id DESC") + lastSeen := make(map[uuid.UUID]time.Time, len(all)) + for _, r := range all { + lastSeen[r.ID] = r.LastSeen + } + tied := tiedMillis(all) + if len(tied) != 12 { + t.Fatalf("fixture: %d tied milliseconds, want 12", len(tied)) + } + cursors := append([]int64{0, -5, all[3].LastSeen.Add(-2500 * time.Millisecond).UnixMilli()}, tied...) + nonEmpty := 0 + for _, setting := range planSettings { + mustExec(t, ctx, tx, setting) + for _, sh := range observerShapes() { + for _, c := range cursors { + for _, limit := range []int32{1, 2, 51} { + want := legacyObserverRows(t, ctx, tx, legacyListObserversDet, sh, legacyCursorTS(c), limit+1) + got := newObserverRows(t, ctx, s, sh, c, nil, limit) + if !sameRows(got, want) { + t.Fatalf("%q %s cursor=%d limit=%d:\n got %v\n want %v", setting, sh.name, c, limit, observerIDs(got), observerIDs(want)) + } + raw := legacyObserverRows(t, ctx, tx, legacyListObservers, sh, legacyCursorTS(c), limit+1) + seen := func(id uuid.UUID) time.Time { return lastSeen[id] } + if !slices.EqualFunc(observerIDs(got), observerIDs(raw), func(a, b uuid.UUID) bool { return seen(a).Equal(seen(b)) }) { + t.Fatalf("%q %s cursor=%d limit=%d vs raw:\n got %v\n raw %v", setting, sh.name, c, limit, observerIDs(got), observerIDs(raw)) + } + if len(got) > 0 { + nonEmpty++ + } + } + } + } + } + mustExec(t, ctx, tx, "RESET plan_cache_mode; RESET enable_seqscan") + if nonEmpty < 500 { + t.Fatalf("only %d non-empty comparisons; the fixture does not exercise the statement", nonEmpty) + } +} + +// The legacy nextCursor now follows the ORDER BY column and is present whenever hasMore. +func TestObserversNextCursorFromLastSeenPostgres(t *testing.T) { + ctx, tx, s := keysetTx(t) + all := keysetOrder(t, ctx, tx, "SELECT id, last_seen, last_status_at FROM observers ORDER BY last_seen DESC, id DESC") + if len(all) != 120 { + t.Fatalf("fixture: %d observers", len(all)) + } + const limit = 7 + + // Store walk following nextCursor: exactly the rows a "last_seen < ms" walk reaches; the only rows missed + // share the millisecond of a page's last row. + var walked, want []uuid.UUID + var boundaries []int64 + var cursor int64 + for range len(all) + 2 { + page, err := s.ListObservers(ctx, nil, "", "", "", "", "", cursor, nil, limit) + if err != nil { + t.Fatal(err) + } + for _, o := range page.Items { + walked = append(walked, o.ID) + } + if !page.HasMore { + break + } + if page.NextCursor == nil { + t.Fatal("hasMore without nextCursor") + } + cursor = *page.NextCursor + boundaries = append(boundaries, cursor) + } + for next := int64(0); ; { + var page []keysetRow + for _, r := range all { + if next == 0 || r.LastSeen.Before(time.UnixMilli(next)) { + page = append(page, r) + } + } + if len(page) <= limit { + want = append(want, keysetIDs(page)...) + break + } + want = append(want, keysetIDs(page[:limit])...) + next = page[limit-1].LastSeen.UnixMilli() + } + if !slices.Equal(walked, want) { + t.Fatalf("nextCursor walk:\n got %v\n want %v", walked, want) + } + missed := 0 + for _, r := range all { + if !slices.Contains(walked, r.ID) { + missed++ + if !slices.Contains(boundaries, r.LastSeen.UnixMilli()) { + t.Errorf("observer %s missed outside a boundary millisecond", r.ID) + } + } + } + if missed == 0 { + t.Fatal("fixture: no page boundary falls inside a tied millisecond") + } + + // The pre-change walk (nextCursor = last_status_at, absent when NULL) misses more than half of them. + var before []uuid.UUID + var cts pgtype.Timestamptz + for range len(all) + 2 { + rows := legacyObserverRows(t, ctx, tx, legacyListObservers, observerShape{}, cts, limit+1) + hasMore := len(rows) > limit + if hasMore { + rows = rows[:limit] + } + before = append(before, observerIDs(rows)...) + if !hasMore || !rows[len(rows)-1].LastStatusAt.Valid { + break + } + cts = pgtype.Timestamptz{Time: time.UnixMilli(rows[len(rows)-1].LastStatusAt.Time.UnixMilli()), Valid: true} + } + if len(all)-len(before) <= len(all)/2 { + t.Fatalf("pre-change walk reached %d of %d observers; the fixture does not show the loss", len(before), len(all)) + } + + // A page whose last row has NULL last_status_at: hasMore with nextCursor from last_seen (was nil). + k := slices.IndexFunc(all, func(r keysetRow) bool { return !r.LastStatusAt.Valid }) + if k < 0 || k+1 >= len(all) { + t.Fatal("fixture: no observer with NULL last_status_at before the last row") + } + page, err := s.ListObservers(ctx, nil, "", "", "", "", "", 0, nil, int32(k+1)) + if err != nil { + t.Fatal(err) + } + wantPC := (api.ObserverCursor{LastSeen: all[k].LastSeen, ID: all[k].ID}).String() + if !page.HasMore || page.NextCursor == nil || *page.NextCursor != all[k].LastSeen.UnixMilli() || + page.NextPageCursor == nil || *page.NextPageCursor != wantPC { + t.Fatalf("page ending on NULL last_status_at: hasMore=%t nextCursor=%v nextPageCursor=%v", page.HasMore, page.NextCursor, page.NextPageCursor) + } + + // A limit-1 pageCursor walk: all 120 once, in (last_seen DESC, id DESC) order. + var pcWalk []uuid.UUID + var pc *api.ObserverCursor + for range len(all) + 2 { + page, err := s.ListObservers(ctx, nil, "", "", "", "", "", 0, pc, 1) + if err != nil { + t.Fatal(err) + } + for _, o := range page.Items { + pcWalk = append(pcWalk, o.ID) + } + if !page.HasMore { + break + } + if pc, err = api.ParseObserverCursor(*page.NextPageCursor); err != nil { + t.Fatal(err) + } + } + if !slices.Equal(pcWalk, keysetIDs(all)) { + t.Fatalf("limit-1 pageCursor walk: %d rows, want %d in order", len(pcWalk), len(all)) + } +} diff --git a/db/nodes_test.go b/db/nodes_test.go index db9a0dd..6e2722d 100644 --- a/db/nodes_test.go +++ b/db/nodes_test.go @@ -6,11 +6,13 @@ package db import ( "context" "errors" + "fmt" "testing" "time" sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc" mockdb "github.com/MeshCore-Beacon/beacon-server/db/sqlc/mock" + "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/pgtype" @@ -197,7 +199,7 @@ func TestListNodes_Pagination(t *testing.T) { Return(rows, nil) store := &Store{q: mock} - page, err := store.ListNodes(context.Background(), 0, []string{"YVR"}, nil, nil, nil, "", "", "", 0, 2, false) + page, err := store.ListNodes(context.Background(), 0, []string{"YVR"}, nil, nil, nil, "", "", "", 0, nil, 2, false) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -230,7 +232,7 @@ func TestListNodes_IATAsUnmarshal(t *testing.T) { }, nil) store := &Store{q: mock} - page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, 10, false) + page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, nil, 10, false) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -259,7 +261,7 @@ func TestListNodes_Stale(t *testing.T) { }, nil) store := &Store{q: mock, staleThreshold: 24 * time.Hour} - page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, 10, false) + page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, nil, 10, false) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -300,7 +302,7 @@ func TestListNodes_RadioStringFormatting(t *testing.T) { }, nil) store := &Store{q: mock} - page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, 10, false) + page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, nil, 10, false) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -644,8 +646,8 @@ func TestListNodes_IncludeNeighbors_PassesFlagAndMapsIDs(t *testing.T) { mock.EXPECT(). ListNodes(gomock.Any(), gomock.Eq(sqlc.ListNodesParams{ Column1: int16(0), Column2: nil, Column3: "any", Column4: "any", - Column5: nil, Column6: "", Column7: pgtype.Timestamptz{}, - Limit: 11, Column9: "", Column10: true, + Column5: nil, Column6: "", Column7: pgtype.Timestamptz{InfinityModifier: pgtype.Infinity, Valid: true}, + Limit: 11, Column9: "", Column12: uuid.Nil, Column10: true, })). Return([]sqlc.ListNodesRow{ { @@ -656,7 +658,7 @@ func TestListNodes_IncludeNeighbors_PassesFlagAndMapsIDs(t *testing.T) { }, nil) store := &Store{q: mock} - page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, 10, true) + page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, nil, 10, true) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -674,15 +676,15 @@ func TestListNodes_ExcludeNeighbors_LeavesIDsNil(t *testing.T) { mock.EXPECT(). ListNodes(gomock.Any(), gomock.Eq(sqlc.ListNodesParams{ Column1: int16(0), Column2: nil, Column3: "any", Column4: "any", - Column5: nil, Column6: "", Column7: pgtype.Timestamptz{}, - Limit: 11, Column9: "", Column10: false, + Column5: nil, Column6: "", Column7: pgtype.Timestamptz{InfinityModifier: pgtype.Infinity, Valid: true}, + Limit: 11, Column9: "", Column12: uuid.Nil, Column10: false, })). Return([]sqlc.ListNodesRow{ {ID: nodeID, PublicKey: []byte{0x01}, NeighborIds: nil}, }, nil) store := &Store{q: mock} - page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, 10, false) + page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, nil, 10, false) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -706,3 +708,77 @@ func TestDeleteOldNodes(t *testing.T) { t.Fatalf("unexpected error: %v", err) } } + +// Page 1 starts at infinity, a numeric cursor pairs its millisecond with the nil uuid, and a pageCursor sends its +// exact pair, winning over a numeric cursor sent alongside. +func TestListNodes_CursorParams(t *testing.T) { + const legacy = int64(1792442625019) + pc := &api.NodeCursor{LastSeen: time.UnixMicro(1792442625019123).UTC(), ID: uuid.MustParse("0b0b0b0b-0000-0000-0000-000000000001")} + for _, tc := range []struct { + name string + cursor int64 + pc *api.NodeCursor + ts pgtype.Timestamptz + id uuid.UUID + }{ + {"page 1", 0, nil, pgtype.Timestamptz{InfinityModifier: pgtype.Infinity, Valid: true}, uuid.Nil}, + {"negative cursor", -5, nil, pgtype.Timestamptz{InfinityModifier: pgtype.Infinity, Valid: true}, uuid.Nil}, + {"legacy cursor", legacy, nil, pgtype.Timestamptz{Time: time.UnixMilli(legacy), Valid: true}, uuid.Nil}, + {"pageCursor", 0, pc, pgtype.Timestamptz{Time: pc.LastSeen, Valid: true}, pc.ID}, + {"pageCursor wins", legacy, pc, pgtype.Timestamptz{Time: pc.LastSeen, Valid: true}, pc.ID}, + } { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + mock := mockdb.NewMockQuerier(ctrl) + mock.EXPECT(). + ListNodes(gomock.Any(), gomock.Eq(sqlc.ListNodesParams{ + Column1: int16(2), Column2: []string{"YVR"}, Column3: "true", Column4: "any", + Column5: []byte{0x01}, Column6: "n", Column7: tc.ts, + Limit: 6, Column9: "#test", Column10: true, Column11: "ab", Column12: tc.id, + })). + Return(nil, nil) + yes := true + store := &Store{q: mock} + page, err := store.ListNodes(context.Background(), 2, []string{"YVR"}, &yes, nil, []byte{0x01}, "ab", "n", "#test", tc.cursor, tc.pc, 5, true) + if err != nil { + t.Fatal(err) + } + if page.Items == nil || page.HasMore || page.NextCursor != nil || page.NextPageCursor != nil { + t.Fatalf("empty page: %+v", page) + } + }) + } +} + +func TestListNodes_NextPageCursor(t *testing.T) { + rows := make([]sqlc.ListNodesRow, 3) + for i := range rows { + rows[i] = sqlc.ListNodesRow{ + ID: uuid.MustParse(fmt.Sprintf("00000000-0000-0000-0000-00000000000%d", i+1)), + PublicKey: []byte{byte(i)}, + LastSeen: pgtype.Timestamptz{Time: time.UnixMicro(1792442625019123 - int64(i)).UTC(), Valid: true}, + } + } + for _, limit := range []int32{2, 3} { + ctrl := gomock.NewController(t) + mock := mockdb.NewMockQuerier(ctrl) + mock.EXPECT().ListNodes(gomock.Any(), gomock.Any()).Return(rows, nil) + store := &Store{q: mock} + page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, nil, limit, false) + if err != nil { + t.Fatal(err) + } + if limit == 3 { + if page.HasMore || page.NextCursor != nil || page.NextPageCursor != nil { + t.Fatalf("last page carries a cursor: %+v", page) + } + continue + } + last := rows[1] + want := api.NodeCursor{LastSeen: last.LastSeen.Time, ID: last.ID}.String() + if !page.HasMore || page.NextCursor == nil || *page.NextCursor != last.LastSeen.Time.UnixMilli() || + page.NextPageCursor == nil || *page.NextPageCursor != want { + t.Fatalf("limit 2: hasMore=%t nextCursor=%v nextPageCursor=%v, want %s", page.HasMore, page.NextCursor, page.NextPageCursor, want) + } + } +} diff --git a/db/observer_last_iata_integration_test.go b/db/observer_last_iata_integration_test.go index ff1c4af..b88c9f0 100644 --- a/db/observer_last_iata_integration_test.go +++ b/db/observer_last_iata_integration_test.go @@ -114,7 +114,7 @@ FROM observers o`) t.Errorf("GetObserverLastIATA(no packets) = %q, %v; want empty", got, err) } - page, err := store.ListObservers(ctx, []string{"YYJ", "YYZ"}, "", "", "", "", "", 0, 10) + page, err := store.ListObservers(ctx, []string{"YYJ", "YYZ"}, "", "", "", "", "", 0, nil, 10) if err != nil { t.Fatal(err) } diff --git a/db/observers.go b/db/observers.go index a458560..2186101 100644 --- a/db/observers.go +++ b/db/observers.go @@ -32,12 +32,17 @@ func (s *Store) UpsertObserver(ctx context.Context, pubkey []byte, iata string) return row.ID, displayName, err } -func (s *Store) ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, limit int32) (api.Page[api.ObserverSummary], error) { - var cursorTS pgtype.Timestamptz - if cursor > 0 { - cursorTS = pgtype.Timestamptz{Time: time.UnixMilli(cursor), Valid: true} +// listObserversParams builds the statement's arguments for one ListObservers request. +func listObserversParams(iatas []string, observerType, broker, status, name, scope string, cursor int64, pageCursor *api.ObserverCursor, limit int32) sqlc.ListObserversParams { + var ( + at *time.Time + id uuid.UUID + ) + if pageCursor != nil { + at, id = &pageCursor.LastSeen, pageCursor.ID } - params := sqlc.ListObserversParams{ + cursorTS, cursorID := uuidKeysetCursor(cursor, at, id) + return sqlc.ListObserversParams{ Column1: iatas, Column2: observerType, Column3: broker, @@ -46,10 +51,15 @@ func (s *Store) ListObservers(ctx context.Context, iatas []string, observerType, Column6: cursorTS, Limit: limit + 1, Column8: scope, + Column9: cursorID, } +} + +func (s *Store) ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, pageCursor *api.ObserverCursor, limit int32) (api.ObserverPage, error) { + params := listObserversParams(iatas, observerType, broker, status, name, scope, cursor, pageCursor, limit) rows, err := s.q.ListObservers(ctx, params) if err != nil { - return api.Page[api.ObserverSummary]{}, err + return api.ObserverPage{}, err } hasMore := len(rows) > int(limit) if hasMore { @@ -75,19 +85,16 @@ func (s *Store) ListObservers(ctx context.Context, iatas []string, observerType, } items = append(items, observer) } - var nextCursor *int64 - if hasMore { - // observers use UUID so encode last_seen as cursor - if rows[len(rows)-1].LastStatusAt.Valid { - ms := rows[len(rows)-1].LastStatusAt.Time.UnixMilli() - nextCursor = &ms - } + page := api.ObserverPage{Page: api.Page[api.ObserverSummary]{Items: items, HasMore: hasMore}} + if hasMore && len(rows) > 0 { + // Both cursors follow the ORDER BY column, last_seen. + last := rows[len(rows)-1] + ms := last.LastSeen.Time.UnixMilli() + next := api.ObserverCursor{LastSeen: last.LastSeen.Time, ID: last.ID}.String() + page.NextCursor = &ms + page.NextPageCursor = &next } - return api.Page[api.ObserverSummary]{ - Items: items, - NextCursor: nextCursor, - HasMore: hasMore, - }, nil + return page, nil } func (s *Store) GetObserver(ctx context.Context, observerID uuid.UUID) (*api.Observer, error) { diff --git a/db/observers_test.go b/db/observers_test.go index 0943a9a..465bf37 100644 --- a/db/observers_test.go +++ b/db/observers_test.go @@ -82,6 +82,7 @@ func TestListObservers_Pagination(t *testing.T) { rows[i] = sqlc.ListObserversRow{ ID: observerID, LastStatusAt: lastStatusAt, + LastSeen: lastStatusAt, } } @@ -90,7 +91,7 @@ func TestListObservers_Pagination(t *testing.T) { Return(rows, nil) store := &Store{q: mock} - page, err := store.ListObservers(context.Background(), []string{"YVR"}, "", "", "", "", "", 0, 2) + page, err := store.ListObservers(context.Background(), []string{"YVR"}, "", "", "", "", "", 0, nil, 2) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -126,7 +127,7 @@ func TestListObservers_RadioStringFormatting(t *testing.T) { }, nil) store := &Store{q: mock} - page, err := store.ListObservers(context.Background(), nil, "", "", "", "", "", 0, 10) + page, err := store.ListObservers(context.Background(), nil, "", "", "", "", "", 0, nil, 10) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -154,7 +155,7 @@ func TestListObservers_NilRadioFields(t *testing.T) { }, nil) store := &Store{q: mock} - page, err := store.ListObservers(context.Background(), nil, "", "", "", "", "", 0, 10) + page, err := store.ListObservers(context.Background(), nil, "", "", "", "", "", 0, nil, 10) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -172,7 +173,7 @@ func TestListObservers_DBError(t *testing.T) { Return(nil, errors.New("db error")) store := &Store{q: mock} - _, err := store.ListObservers(context.Background(), nil, "", "", "", "", "", 0, 10) + _, err := store.ListObservers(context.Background(), nil, "", "", "", "", "", 0, nil, 10) if err == nil { t.Fatal("expected error, got nil") } @@ -954,3 +955,68 @@ func TestGetObserverActivity_SinceAlignedToInterval(t *testing.T) { t.Errorf("expected interval %d us, got %+v", interval.Microseconds(), gotInterval) } } + +// Page 1 starts at infinity, a numeric cursor pairs its millisecond with the nil uuid, and a pageCursor sends its +// exact pair, winning over a numeric cursor sent alongside. +func TestListObservers_CursorParams(t *testing.T) { + const legacy = int64(1792442625019) + pc := &api.ObserverCursor{LastSeen: time.UnixMicro(1792442625019123).UTC(), ID: uuid.MustParse("0b0b0b0b-0000-0000-0000-000000000001")} + for _, tc := range []struct { + name string + cursor int64 + pc *api.ObserverCursor + ts pgtype.Timestamptz + id uuid.UUID + }{ + {"page 1", 0, nil, pgtype.Timestamptz{InfinityModifier: pgtype.Infinity, Valid: true}, uuid.Nil}, + {"negative cursor", -5, nil, pgtype.Timestamptz{InfinityModifier: pgtype.Infinity, Valid: true}, uuid.Nil}, + {"legacy cursor", legacy, nil, pgtype.Timestamptz{Time: time.UnixMilli(legacy), Valid: true}, uuid.Nil}, + {"pageCursor", 0, pc, pgtype.Timestamptz{Time: pc.LastSeen, Valid: true}, pc.ID}, + {"pageCursor wins", legacy, pc, pgtype.Timestamptz{Time: pc.LastSeen, Valid: true}, pc.ID}, + } { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + mock := mockdb.NewMockQuerier(ctrl) + mock.EXPECT(). + ListObservers(gomock.Any(), gomock.Eq(sqlc.ListObserversParams{ + Column1: []string{"YVR"}, Column2: "repeater", Column3: "b1", Column4: "online", Column5: "n", + Column6: tc.ts, Limit: 6, Column8: "#test", Column9: tc.id, + })). + Return(nil, nil) + store := &Store{q: mock} + page, err := store.ListObservers(context.Background(), []string{"YVR"}, "repeater", "b1", "online", "n", "#test", tc.cursor, tc.pc, 5) + if err != nil { + t.Fatal(err) + } + if page.Items == nil || page.HasMore || page.NextCursor != nil || page.NextPageCursor != nil { + t.Fatalf("empty page: %+v", page) + } + }) + } +} + +// Both cursors come from the last row's last_seen, also when its last_status_at is NULL. +func TestListObservers_NextCursorFromLastSeen(t *testing.T) { + rows := []sqlc.ListObserversRow{ + {ID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), LastSeen: pgtype.Timestamptz{Time: time.UnixMicro(1792442625019123).UTC(), Valid: true}, + LastStatusAt: pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}}, + {ID: uuid.MustParse("00000000-0000-0000-0000-000000000002"), LastSeen: pgtype.Timestamptz{Time: time.UnixMicro(1792442625018456).UTC(), Valid: true}}, + {ID: uuid.MustParse("00000000-0000-0000-0000-000000000003"), LastSeen: pgtype.Timestamptz{Time: time.UnixMicro(1792442625017000).UTC(), Valid: true}}, + } + for _, limit := range []int32{1, 2} { + ctrl := gomock.NewController(t) + mock := mockdb.NewMockQuerier(ctrl) + mock.EXPECT().ListObservers(gomock.Any(), gomock.Any()).Return(rows[:limit+1], nil) + store := &Store{q: mock} + page, err := store.ListObservers(context.Background(), nil, "", "", "", "", "", 0, nil, limit) + if err != nil { + t.Fatal(err) + } + last := rows[limit-1] + want := api.ObserverCursor{LastSeen: last.LastSeen.Time, ID: last.ID}.String() + if !page.HasMore || page.NextCursor == nil || *page.NextCursor != last.LastSeen.Time.UnixMilli() || + page.NextPageCursor == nil || *page.NextPageCursor != want { + t.Fatalf("limit %d: hasMore=%t nextCursor=%v nextPageCursor=%v, want %s", limit, page.HasMore, page.NextCursor, page.NextPageCursor, want) + } + } +} diff --git a/db/packet_endpoints_integration_test.go b/db/packet_endpoints_integration_test.go index 50d2c0b..045bcb0 100644 --- a/db/packet_endpoints_integration_test.go +++ b/db/packet_endpoints_integration_test.go @@ -137,7 +137,7 @@ VALUES ($1,decode($2,'hex'),$3,$4,$5,0,1,0)`, n, fmt.Sprintf("%064x", n), observ // One page query plus one hash batch and one advert batch. for _, iatas := range [][]string{nil, {"YYZ", "YVR"}} { q.calls = 0 - page, err := store.ListPackets(ctx, nil, nil, iatas, nil, time.Time{}, time.Time{}, 0, 50) + page, err := store.ListPackets(ctx, nil, nil, iatas, nil, time.Time{}, time.Time{}, 0, nil, 50) if err != nil || len(page.Items) != 4 || q.calls != 3 { t.Fatalf("list %v: rows=%d queries=%d err=%v", iatas, len(page.Items), q.calls, err) } diff --git a/db/packet_observation_count_integration_test.go b/db/packet_observation_count_integration_test.go index 9f5ebad..93b4a51 100644 --- a/db/packet_observation_count_integration_test.go +++ b/db/packet_observation_count_integration_test.go @@ -78,7 +78,7 @@ FROM packets p WHERE p.packet_hash = $1`, hash).Scan(&stored, &raw); err != nil t.Errorf("packets.observation_count = %d, raw count = %d; want 2", stored, raw) } - page, err := store.ListPackets(ctx, nil, nil, nil, nil, time.Time{}, time.Time{}, 0, 10) + page, err := store.ListPackets(ctx, nil, nil, nil, nil, time.Time{}, time.Time{}, 0, nil, 10) if err != nil { t.Fatal(err) } diff --git a/db/packet_summary_integration_test.go b/db/packet_summary_integration_test.go index f27cf63..48c89f1 100644 --- a/db/packet_summary_integration_test.go +++ b/db/packet_summary_integration_test.go @@ -110,7 +110,7 @@ FROM (VALUES } counter.calls = 0 } - page, err := store.ListPackets(ctx, nil, nil, nil, nil, time.Time{}, time.Time{}, 0, 50) + page, err := store.ListPackets(ctx, nil, nil, nil, nil, time.Time{}, time.Time{}, 0, nil, 50) if err != nil { t.Fatal(err) } @@ -125,7 +125,7 @@ SELECT id,decode(lpad(to_hex(id),2,'0'),'hex'),'00000000-0000-0000-0000-00000000 t.Fatal(err) } for _, iatas := range [][]string{nil, {"YVR"}} { - page, err := store.ListPackets(ctx, nil, nil, iatas, nil, time.Time{}, time.Time{}, 0, 50) + page, err := store.ListPackets(ctx, nil, nil, iatas, nil, time.Time{}, time.Time{}, 0, nil, 50) if err != nil { t.Fatal(err) } diff --git a/db/packetfilter.go b/db/packetfilter.go new file mode 100644 index 0000000..5594740 --- /dev/null +++ b/db/packetfilter.go @@ -0,0 +1,117 @@ +// Copyright 2026 Beacon Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +package db + +import ( + "time" + + "github.com/MeshCore-Beacon/beacon-server/internal/api" + "github.com/jackc/pgx/v5/pgtype" +) + +// Domains enforced by the packets_payload_type_domain and +// packets_route_type_domain check constraints. A value outside them matches no +// row, so dropping it from a filter list cannot change the result. +const ( + payloadTypeCount = 16 // payload_type BETWEEN 0 AND 15 + routeTypeCount = 4 // route_type BETWEEN 0 AND 3 +) + +// packetFilter is a global /packets request after normalisation. +type packetFilter struct { + payloadTypes []int16 // deduplicated, in 0..15, order kept; all 16 when the request gave none + routeTypes []int16 // deduplicated, in 0..3, order kept; all 4 when the request gave none + typed bool // the request gave a payload or route type list + scopes []string // deduplicated, order kept, values as given (no trim) + windowed bool // since or until was given + since, until pgtype.Timestamptz // -infinity / infinity when absent + cursorTS pgtype.Timestamptz // infinity on page 1 + cursorHash []byte // never nil; empty on page 1 and for a legacy cursor + pageLimit int32 // limit + 1 +} + +// newPacketFilter normalises a request. ok is false when no row can match: a type list that held values and lost +// all of them to the domain drop. The caller then returns an empty page without a query. +func newPacketFilter(payloadTypes, routeTypes []int16, scopes []string, since, until time.Time, cursor int64, pageCursor *api.PacketCursor, limit int32) (f packetFilter, ok bool) { + var ptOK, rtOK bool + f.payloadTypes, ptOK = normaliseTypes(payloadTypes, payloadTypeCount) + f.routeTypes, rtOK = normaliseTypes(routeTypes, routeTypeCount) + if !ptOK || !rtOK { + return packetFilter{}, false + } + f.typed = len(payloadTypes) > 0 || len(routeTypes) > 0 + + f.scopes = dedupeStrings(scopes) + + // since after until is not short-circuited: the statement compares the encoded values, which wrap for + // out-of-range times exactly as they always have, so it alone decides which rows match. + f.since = pgtype.Timestamptz{InfinityModifier: pgtype.NegativeInfinity, Valid: true} + f.until = pgtype.Timestamptz{InfinityModifier: pgtype.Infinity, Valid: true} + if !since.IsZero() { + f.since = pgtype.Timestamptz{Time: since, Valid: true} + f.windowed = true + } + if !until.IsZero() { + f.until = pgtype.Timestamptz{Time: until, Valid: true} + f.windowed = true + } + + switch { + case pageCursor != nil: + f.cursorTS = pgtype.Timestamptz{Time: pageCursor.At, Valid: true} + f.cursorHash = pageCursor.Hash + case cursor > 0: + // Same encoding as the numeric cursor has always had, including the + // wrap of out-of-range values to an early instant. + f.cursorTS = pgtype.Timestamptz{Time: time.UnixMilli(cursor), Valid: true} + default: + f.cursorTS = pgtype.Timestamptz{InfinityModifier: pgtype.Infinity, Valid: true} + } + if f.cursorHash == nil { + f.cursorHash = []byte{} + } + + f.pageLimit = limit + 1 + return f, true +} + +// normaliseTypes returns the distinct values of in that fall in 0..n-1, in +// their first-seen order, or all of 0..n-1 when in is empty. ok is false when +// in held values and none of them were in range. +func normaliseTypes(in []int16, n int16) (out []int16, ok bool) { + if len(in) == 0 { + out = make([]int16, n) + for i := range out { + out[i] = int16(i) + } + return out, true + } + var seen [16]bool + for _, v := range in { + if v < 0 || v >= n || seen[v] { + continue + } + seen[v] = true + out = append(out, v) + } + return out, len(out) > 0 +} + +// dedupeStrings returns the distinct values of in, in first-seen order, or nil when +// in is empty. +func dedupeStrings(in []string) []string { + if len(in) == 0 { + return nil + } + seen := make(map[string]struct{}, len(in)) + out := make([]string, 0, len(in)) + for _, v := range in { + if _, dup := seen[v]; dup { + continue + } + seen[v] = struct{}{} + out = append(out, v) + } + return out +} diff --git a/db/packetfilter_test.go b/db/packetfilter_test.go new file mode 100644 index 0000000..bbb3b63 --- /dev/null +++ b/db/packetfilter_test.go @@ -0,0 +1,166 @@ +// Copyright 2026 Beacon Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +package db + +import ( + "bytes" + "slices" + "testing" + "time" + + "github.com/MeshCore-Beacon/beacon-server/internal/api" + "github.com/jackc/pgx/v5/pgtype" +) + +func domain(n int16) []int16 { + out := make([]int16, n) + for i := range out { + out[i] = int16(i) + } + return out +} + +func TestPacketFilterNormalisation(t *testing.T) { + full16, full4 := domain(16), domain(4) + var zero time.Time + t0 := time.UnixMilli(1792442625019).UTC() + t1 := t0.Add(time.Minute) + epoch := time.UnixMilli(0) + for _, tc := range []struct { + name string + pt, rt []int16 + scopes []string + since, until time.Time + wantOK, wantTyped bool + wantPT, wantRT []int16 + wantScopes []string + wantWindowed bool + }{ + {"absent", nil, nil, nil, zero, zero, true, false, full16, full4, nil, false}, + {"empty after parse", []int16{}, []int16{}, []string{}, zero, zero, true, false, full16, full4, nil, false}, + {"dup types", []int16{4, 4}, []int16{0, 0, 1}, nil, zero, zero, true, true, []int16{4}, []int16{0, 1}, nil, false}, + {"order kept", []int16{9, 2, 4, 2}, nil, nil, zero, zero, true, true, []int16{9, 2, 4}, full4, nil, false}, + {"drop 99", []int16{99, 4}, nil, nil, zero, zero, true, true, []int16{4}, full4, nil, false}, + {"drop negative and 16", []int16{-3, 16, 15}, nil, nil, zero, zero, true, true, []int16{15}, full4, nil, false}, + {"all dropped", []int16{99}, nil, nil, zero, zero, false, false, nil, nil, nil, false}, + {"unknown name", []int16{-1}, nil, nil, zero, zero, false, false, nil, nil, nil, false}, + {"route 9", nil, []int16{9}, nil, zero, zero, false, false, nil, nil, nil, false}, + {"route 3,7", nil, []int16{3, 7}, nil, zero, zero, true, true, full16, []int16{3}, nil, false}, + {"route given payload dropped", []int16{99}, []int16{1}, nil, zero, zero, false, false, nil, nil, nil, false}, + {"scope dups kept untrimmed", nil, nil, []string{"#a", " #b", "#a"}, zero, zero, true, false, full16, full4, []string{"#a", " #b"}, false}, + {"scope blank singular kept", nil, nil, []string{" "}, zero, zero, true, false, full16, full4, []string{" "}, false}, + {"since > until is left to the statement", nil, nil, nil, t1, t0, true, false, full16, full4, nil, true}, + {"since = until", nil, nil, nil, t0, t0, true, false, full16, full4, nil, true}, + {"since < until", nil, nil, nil, t0, t1, true, false, full16, full4, nil, true}, + {"until only", nil, nil, nil, zero, t0, true, false, full16, full4, nil, true}, + {"since only", nil, nil, nil, t0, zero, true, false, full16, full4, nil, true}, + {"since epoch is a bound", nil, nil, nil, epoch, zero, true, false, full16, full4, nil, true}, + {"since epoch > until", nil, nil, nil, epoch, time.UnixMilli(-1), true, false, full16, full4, nil, true}, + } { + t.Run(tc.name, func(t *testing.T) { + f, ok := newPacketFilter(tc.pt, tc.rt, tc.scopes, tc.since, tc.until, 0, nil, 50) + if ok != tc.wantOK { + t.Fatalf("ok = %v, want %v", ok, tc.wantOK) + } + if !ok { + return + } + if f.typed != tc.wantTyped { + t.Errorf("typed = %v, want %v", f.typed, tc.wantTyped) + } + if !slices.Equal(f.payloadTypes, tc.wantPT) { + t.Errorf("payloadTypes = %v, want %v", f.payloadTypes, tc.wantPT) + } + if !slices.Equal(f.routeTypes, tc.wantRT) { + t.Errorf("routeTypes = %v, want %v", f.routeTypes, tc.wantRT) + } + if !slices.Equal(f.scopes, tc.wantScopes) { + t.Errorf("scopes = %q, want %q", f.scopes, tc.wantScopes) + } + if f.windowed != tc.wantWindowed { + t.Errorf("windowed = %v, want %v", f.windowed, tc.wantWindowed) + } + checkBound(t, "since", f.since, tc.since, pgtype.NegativeInfinity) + checkBound(t, "until", f.until, tc.until, pgtype.Infinity) + if f.pageLimit != 51 { + t.Errorf("pageLimit = %d, want 51", f.pageLimit) + } + if f.cursorHash == nil { + t.Error("cursorHash is nil") + } + }) + } + + // The caller's slices are not modified. + in := []int16{4, 4, 99} + newPacketFilter(in, nil, nil, zero, zero, 0, nil, 1) + if !slices.Equal(in, []int16{4, 4, 99}) { + t.Errorf("input mutated: %v", in) + } +} + +func checkBound(t *testing.T, name string, got pgtype.Timestamptz, in time.Time, absent pgtype.InfinityModifier) { + t.Helper() + if !got.Valid { + t.Errorf("%s not valid", name) + return + } + if in.IsZero() { + if got.InfinityModifier != absent { + t.Errorf("%s = %+v, want infinity modifier %v", name, got, absent) + } + return + } + if got.InfinityModifier != pgtype.Finite || !got.Time.Equal(in) { + t.Errorf("%s = %+v, want %v", name, got, in) + } +} + +func TestPacketFilterCursor(t *testing.T) { + var zero time.Time + at := time.UnixMicro(1792442625019123).UTC() + hash := []byte{0xde, 0xad, 0xbe, 0xef} + for _, tc := range []struct { + name string + cursor int64 + pc *api.PacketCursor + wantInf bool + wantTime time.Time + wantHash []byte + }{ + {"page 1", 0, nil, true, zero, []byte{}}, + {"negative cursor", -5, nil, true, zero, []byte{}}, + {"legacy cursor", 1792442625019, nil, false, time.UnixMilli(1792442625019), []byte{}}, + {"legacy max int64 wraps as today", 9223372036854775807, nil, false, time.UnixMilli(9223372036854775807), []byte{}}, + {"pageCursor", 0, &api.PacketCursor{At: at, Hash: hash}, false, at, hash}, + {"pageCursor wins over cursor", 5, &api.PacketCursor{At: at, Hash: hash}, false, at, hash}, + {"pageCursor with nil hash", 0, &api.PacketCursor{At: at}, false, at, []byte{}}, + } { + t.Run(tc.name, func(t *testing.T) { + f, ok := newPacketFilter(nil, nil, nil, zero, zero, tc.cursor, tc.pc, 100) + if !ok { + t.Fatal("ok = false") + } + if f.cursorHash == nil { + t.Fatal("cursorHash is nil") + } + if !bytes.Equal(f.cursorHash, tc.wantHash) { + t.Errorf("cursorHash = %x, want %x", f.cursorHash, tc.wantHash) + } + if !f.cursorTS.Valid { + t.Fatal("cursorTS not valid") + } + if tc.wantInf { + if f.cursorTS.InfinityModifier != pgtype.Infinity { + t.Errorf("cursorTS = %+v, want infinity", f.cursorTS) + } + } else if f.cursorTS.InfinityModifier != pgtype.Finite || !f.cursorTS.Time.Equal(tc.wantTime) { + t.Errorf("cursorTS = %+v, want %v", f.cursorTS, tc.wantTime) + } + if f.pageLimit != 101 { + t.Errorf("pageLimit = %d, want 101", f.pageLimit) + } + }) + } +} diff --git a/db/packets.go b/db/packets.go index 172084e..354aba1 100644 --- a/db/packets.go +++ b/db/packets.go @@ -83,38 +83,97 @@ func buildLatestObserverPath(pathLengthByte, hashSize, hopCount *int16, pathByte return pathLength, pathBytesHex } -func (s *Store) ListPackets(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) { +// packetRow is the shared row of the six global packets statements; the other five Row types convert to it. +type packetRow = sqlc.ListPacketsPageRow + +// ListPackets serves the packet list. Region (iatas) requests keep their site-local statement and cursor and ignore +// pageCursor. Every other request is normalised and sent to the one statement for its filter shape; pageCursor wins +// over cursor there. +func (s *Store) ListPackets(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, pageCursor *api.PacketCursor, limit int32) (api.PacketPage, error) { if len(iatas) > 0 { - return s.listPacketsByIATAs(ctx, payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit) + p, err := s.listPacketsByIATAs(ctx, payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit) + return api.PacketPage{Page: p}, err } - var cursorTS pgtype.Timestamptz - if cursor > 0 { - cursorTS = pgtype.Timestamptz{Time: time.UnixMilli(cursor), Valid: true} + f, ok := newPacketFilter(payloadTypes, routeTypes, scopes, since, until, cursor, pageCursor, limit) + if !ok { + return api.PacketPage{Page: api.Page[api.PacketSummary]{Items: []api.PacketSummary{}}}, nil } - var sinceTS pgtype.Timestamptz - if !since.IsZero() { - sinceTS = pgtype.Timestamptz{Time: since, Valid: true} - } - var untilTS pgtype.Timestamptz - if !until.IsZero() { - untilTS = pgtype.Timestamptz{Time: until, Valid: true} - } - rows, err := s.q.ListPackets(ctx, sqlc.ListPacketsParams{ - Column1: payloadTypes, - Column2: routeTypes, - Column3: sinceTS, - Column4: untilTS, - Column5: cursorTS, - Limit: limit + 1, - Column7: scopes, - }) + rows, err := s.listPacketRows(ctx, f) if err != nil { - return api.Page[api.PacketSummary]{}, err + return api.PacketPage{}, err } hasMore := len(rows) > int(limit) if hasMore { rows = rows[:limit] } + page := api.PacketPage{Page: api.Page[api.PacketSummary]{ + Items: s.packetSummaries(ctx, rows), + HasMore: hasMore, + }} + if hasMore && len(rows) > 0 { + last := rows[len(rows)-1] + ms := last.LastHeardAt.Time.UnixMilli() + next := api.PacketCursor{At: last.LastHeardAt.Time, Hash: last.PacketHash}.String() + page.NextCursor = &ms + page.NextPageCursor = &next + } + return page, nil +} + +// listPacketRows runs the statement for f's filter shape and returns up to f.pageLimit rows in +// (last_heard_at DESC, packet_hash DESC) order. +func (s *Store) listPacketRows(ctx context.Context, f packetFilter) ([]packetRow, error) { + switch { + case f.windowed && len(f.scopes) > 0: + rows, err := s.q.ListPacketsWindowScoped(ctx, sqlc.ListPacketsWindowScopedParams{ + SinceTs: f.since, CursorTs: f.cursorTS, CursorHash: f.cursorHash, UntilTs: f.until, + PayloadTypes: f.payloadTypes, RouteTypes: f.routeTypes, PageLimit: f.pageLimit, ScopeNames: f.scopes, + }) + return convertPacketRows(rows, err, func(r sqlc.ListPacketsWindowScopedRow) packetRow { return packetRow(r) }) + case f.windowed: + rows, err := s.q.ListPacketsWindow(ctx, sqlc.ListPacketsWindowParams{ + SinceTs: f.since, CursorTs: f.cursorTS, CursorHash: f.cursorHash, UntilTs: f.until, + PayloadTypes: f.payloadTypes, RouteTypes: f.routeTypes, PageLimit: f.pageLimit, + }) + return convertPacketRows(rows, err, func(r sqlc.ListPacketsWindowRow) packetRow { return packetRow(r) }) + case len(f.scopes) > 0 && f.typed: + rows, err := s.q.ListPacketsByScopesTypes(ctx, sqlc.ListPacketsByScopesTypesParams{ + CursorTs: f.cursorTS, CursorHash: f.cursorHash, PayloadTypes: f.payloadTypes, RouteTypes: f.routeTypes, + PageLimit: f.pageLimit, ScopeNames: f.scopes, + }) + return convertPacketRows(rows, err, func(r sqlc.ListPacketsByScopesTypesRow) packetRow { return packetRow(r) }) + case len(f.scopes) > 0: + rows, err := s.q.ListPacketsByScopes(ctx, sqlc.ListPacketsByScopesParams{ + CursorTs: f.cursorTS, CursorHash: f.cursorHash, PageLimit: f.pageLimit, ScopeNames: f.scopes, + }) + return convertPacketRows(rows, err, func(r sqlc.ListPacketsByScopesRow) packetRow { return packetRow(r) }) + case f.typed: + rows, err := s.q.ListPacketsByTypes(ctx, sqlc.ListPacketsByTypesParams{ + PayloadTypes: f.payloadTypes, RouteTypes: f.routeTypes, CursorTs: f.cursorTS, CursorHash: f.cursorHash, + PageLimit: f.pageLimit, + }) + return convertPacketRows(rows, err, func(r sqlc.ListPacketsByTypesRow) packetRow { return packetRow(r) }) + } + return s.q.ListPacketsPage(ctx, sqlc.ListPacketsPageParams{ + CursorTs: f.cursorTS, CursorHash: f.cursorHash, PageLimit: f.pageLimit, + }) +} + +// convertPacketRows converts one statement's rows to packetRow with conv, a plain Go conversion (the Row types +// share every field name and type). +func convertPacketRows[R any](rows []R, err error, conv func(R) packetRow) ([]packetRow, error) { + if err != nil { + return nil, err + } + out := make([]packetRow, len(rows)) + for i, r := range rows { + out[i] = conv(r) + } + return out, nil +} + +// packetSummaries builds the API items for global packet rows and resolves their endpoints. +func (s *Store) packetSummaries(ctx context.Context, rows []packetRow) []api.PacketSummary { items := make([]api.PacketSummary, 0, len(rows)) lookups := make([]endpointLookup, 0, len(rows)) for _, v := range rows { @@ -148,16 +207,7 @@ func (s *Store) ListPackets(ctx context.Context, payloadTypes, routeTypes []int1 lookups = append(lookups, lookup) } s.fillLatestEndpoints(ctx, items, lookups) - var nextCursor *int64 - if hasMore && len(items) > 0 { - last := items[len(items)-1].LastHeardAt - nextCursor = &last - } - return api.Page[api.PacketSummary]{ - Items: items, - NextCursor: nextCursor, - HasMore: hasMore, - }, nil + return items } func (s *Store) listPacketsByIATAs(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) { diff --git a/db/packets_global_integration_test.go b/db/packets_global_integration_test.go new file mode 100644 index 0000000..b21723d --- /dev/null +++ b/db/packets_global_integration_test.go @@ -0,0 +1,403 @@ +// Copyright 2026 Beacon Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +package db + +import ( + "bytes" + "context" + "crypto/md5" + "fmt" + "math" + "slices" + "testing" + "time" + + "github.com/MeshCore-Beacon/beacon-server/internal/api" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" +) + +// packetFixture: scopes #a, #b, #c and #empty (no packets); 600 packets over 40 tied milliseconds with +// microsecond-tie groups inside each, mixed types, a quarter unscoped, first_heard_at up to 6 s before +// last_heard_at; 21 packets (i % 29 = 1) with first_heard_at 300 µs after last_heard_at; two observations per +// packet on distinct observers, and for the 10 packets with i % 60 = 0 both observations share the newest heard_at. +const packetFixture = ` +INSERT INTO transport_scopes (id, name, transport_key, key_fingerprint) VALUES + (1, '#a', '\x01', '\x01'), (2, '#b', '\x02', '\x02'), (3, '#c', '\x03', '\x03'), (4, '#empty', '\x04', '\x04'); +INSERT INTO observers (id, public_key, display_name) +SELECT ('00000000-0000-0000-0000-00000000030' || n)::uuid, int2send(n::int2), 'obs' || n FROM generate_series(1, 4) n; +INSERT INTO packets (packet_hash, payload_type, payload_version, route_type, scope_id, raw_payload, raw_header, + parsed_payload, first_heard_at, last_heard_at, observation_count) +SELECT decode(md5(i::text), 'hex'), (i * 7) % 16, 0, (i * 3) % 4, CASE WHEN i % 4 = 0 THEN NULL ELSE 1 + i % 3 END, + '\x00', '\x00', + CASE WHEN (i * 7) % 16 = 4 THEN jsonb_build_object('appData', jsonb_build_object('name', 'n' || i)) END, + CASE WHEN i % 29 = 1 THEN x.l + interval '300 microseconds' ELSE x.l - (i % 7) * interval '1 second' END, + x.l, 2 +FROM generate_series(1, 600) i, + LATERAL (SELECT '2026-01-10 00:00:00+00'::timestamptz + (i % 40) * interval '1 millisecond' + + (i % 3) * interval '1 microsecond') x(l); +INSERT INTO packet_observations (packet_hash, observer_id, iata, heard_at, path_length_byte, hash_size, hop_count, path_bytes) +SELECT packet_hash, ('00000000-0000-0000-0000-00000000030' || (1 + i % 4))::uuid, 'YVR', + last_heard_at - CASE WHEN i % 60 = 0 THEN interval '0' ELSE interval '1 millisecond' END, 65, 1, 1, '\x01' +FROM (SELECT i, decode(md5(i::text), 'hex') AS packet_hash FROM generate_series(1, 600) i) g JOIN packets USING (packet_hash); +INSERT INTO packet_observations (packet_hash, observer_id, iata, heard_at, path_length_byte, hash_size, hop_count, path_bytes) +SELECT packet_hash, ('00000000-0000-0000-0000-00000000030' || (1 + (i + 1) % 4))::uuid, 'YYZ', last_heard_at, 66, 1, 2, '\x0203' +FROM (SELECT i, decode(md5(i::text), 'hex') AS packet_hash FROM generate_series(1, 600) i) g JOIN packets USING (packet_hash); +ANALYZE packets;` + +var packetBase = time.Date(2026, 1, 10, 0, 0, 0, 0, time.UTC) + +// fixtureHash is packet i's hash in packetFixture. +func fixtureHash(i int) []byte { + h := md5.Sum(fmt.Appendf(nil, "%d", i)) + return h[:] +} + +// packetsGlobalTx is a rolled-back transaction holding the end-state schema and packetFixture. +func packetsGlobalTx(t *testing.T) (context.Context, pgx.Tx, *Store) { + t.Helper() + ctx, tx := retentionTx(t) + applyBaseline(t, ctx, tx) + applyPacketsEndState(t, ctx, tx) + mustExec(t, ctx, tx, packetFixture) + return ctx, tx, routeStore(tx) +} + +type packetShape struct { + name string + pt, rt []int16 + scopes []string + since, until time.Time +} + +func optTS(t time.Time) pgtype.Timestamptz { + if t.IsZero() { + return pgtype.Timestamptz{} + } + return pgtype.Timestamptz{Time: t, Valid: true} +} + +// legacyPacketRows runs one of today's statements with the arguments today's Store passed it. +func legacyPacketRows(t *testing.T, ctx context.Context, tx pgx.Tx, sql string, sh packetShape, cursor int64, pageLimit int32) []packetRow { + t.Helper() + var cts pgtype.Timestamptz + if cursor > 0 { + cts = pgtype.Timestamptz{Time: time.UnixMilli(cursor), Valid: true} + } + return legacyRows[packetRow](t, ctx, tx, sql, sh.pt, sh.rt, optTS(sh.since), optTS(sh.until), cts, pageLimit, sh.scopes) +} + +// newPacketRows runs the dispatcher's core for one request; no rows when the filter cannot match. +func newPacketRows(t *testing.T, ctx context.Context, s *Store, sh packetShape, cursor int64, pc *api.PacketCursor, limit int32) []packetRow { + t.Helper() + f, ok := newPacketFilter(sh.pt, sh.rt, sh.scopes, sh.since, sh.until, cursor, pc, limit) + if !ok { + return nil + } + rows, err := s.listPacketRows(ctx, f) + if err != nil { + t.Fatalf("%s: %v", sh.name, err) + } + return rows +} + +func packetKeys(rows []packetRow) []string { + out := make([]string, len(rows)) + for i, r := range rows { + out[i] = fmt.Sprintf("%s/%x", r.LastHeardAt.Time.Format("15:04:05.000000"), r.PacketHash[:3]) + } + return out +} + +// sameKeyRuns reports whether got and raw have the same last_heard_at sequence and, for every run of equal +// last_heard_at, the same hash set. A run the LIMIT cut (the last one of a full page) is compared by length only. +func sameKeyRuns(got, raw []packetRow, full bool) bool { + if len(got) != len(raw) { + return false + } + for i := range got { + if !got[i].LastHeardAt.Time.Equal(raw[i].LastHeardAt.Time) { + return false + } + } + for i := 0; i < len(got); { + j := i + for j < len(got) && got[j].LastHeardAt.Time.Equal(got[i].LastHeardAt.Time) { + j++ + } + if j < len(got) || !full { + a, b := hashSet(got[i:j]), hashSet(raw[i:j]) + if !slices.Equal(a, b) { + return false + } + } + i = j + } + return true +} + +func hashSet(rows []packetRow) []string { + out := make([]string, len(rows)) + for i, r := range rows { + out[i] = string(r.PacketHash) + } + slices.Sort(out) + return out +} + +func globalShapes() []packetShape { + bogus := make([]string, 5000) + for i := range bogus { + bogus[i] = fmt.Sprintf("#bogus%d", i) + } + all16x4 := packetShape{name: "all 16x4 explicit"} + for i := int16(0); i < 16; i++ { + all16x4.pt = append(all16x4.pt, i) + } + all16x4.rt = []int16{0, 1, 2, 3} + oneFirst := packetBase.Add(7*time.Millisecond + time.Microsecond) // packet 7: first_heard_at = last_heard_at + return []packetShape{ + {name: "unfiltered"}, + {name: "pt 4", pt: []int16{4}}, + {name: "pt 4,4 rt 0,0,1", pt: []int16{4, 4}, rt: []int16{0, 0, 1}}, + {name: "pt 99,4", pt: []int16{99, 4}}, + {name: "pt 99", pt: []int16{99}}, + {name: "pt empty", pt: []int16{}}, + {name: "rt 3", rt: []int16{3}}, + {name: "rt 3,7", rt: []int16{3, 7}}, + {name: "web 13 types", pt: []int16{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 15}}, + all16x4, + {name: "scopes #a", scopes: []string{"#a"}}, + {name: "scopes #a,#b,#a,#nope", scopes: []string{"#a", "#b", "#a", "#nope"}}, + {name: "scopes #empty", scopes: []string{"#empty"}}, + {name: "scopes #nope", scopes: []string{"#nope"}}, + {name: "scopes untrimmed", scopes: []string{" #a"}}, + {name: "scopes #a,#c pt fib", scopes: []string{"#a", "#c"}, pt: []int16{1, 2, 3, 5, 8, 13}}, + {name: "window since until", since: packetBase.Add(-3 * time.Second), until: packetBase.Add(20 * time.Millisecond)}, + {name: "window since only", since: packetBase.Add(14*time.Millisecond + 200*time.Microsecond)}, + {name: "window since early", since: packetBase.Add(-4 * time.Second)}, + {name: "window until only", until: packetBase.Add(-2 * time.Second)}, + {name: "window typed", since: packetBase.Add(-5 * time.Second), pt: []int16{4, 9}, rt: []int16{1, 2}}, + {name: "window scoped", since: packetBase.Add(-5 * time.Second), scopes: []string{"#a", "#b"}}, + {name: "window since = until", since: oneFirst, until: oneFirst}, + {name: "window since > until", since: oneFirst, until: oneFirst.Add(-time.Microsecond)}, + {name: "window since > until by ms", since: packetBase.Add(20 * time.Millisecond), until: packetBase.Add(10 * time.Millisecond)}, + {name: "window since max int64", since: time.UnixMilli(math.MaxInt64), until: packetBase.Add(20 * time.Millisecond)}, + {name: "window until max int64", since: packetBase.Add(-5 * time.Second), until: time.UnixMilli(math.MaxInt64)}, + {name: "5000 bogus scopes", scopes: bogus}, + } +} + +// Shapes × cursors × limits × both plan modes: rows equal today's statement with the tiebreak, in order, on all +// columns; against today's raw statement the key sequence and the set per key run are equal (tie order). +func TestListPacketsGlobalMatchesLegacyPostgres(t *testing.T) { + ctx, tx, s := packetsGlobalTx(t) + cursors := []int64{ + 0, -5, + packetBase.Add(17 * time.Millisecond).UnixMilli(), // a tied millisecond + packetBase.Add(30 * time.Millisecond).UnixMilli(), // just above a millisecond of microsecond-tie groups + packetBase.Add(-time.Millisecond).UnixMilli(), + packetBase.Add(40 * time.Millisecond).UnixMilli(), + math.MaxInt64, + } + nonEmpty := 0 + for _, mode := range []string{"force_custom_plan", "force_generic_plan"} { + mustExec(t, ctx, tx, "SET LOCAL plan_cache_mode = "+mode) + for _, sh := range globalShapes() { + for _, c := range cursors { + for _, limit := range []int32{1, 2, 3, 7, 51} { + want := legacyPacketRows(t, ctx, tx, legacyListPacketsDet, sh, c, limit+1) + got := newPacketRows(t, ctx, s, sh, c, nil, limit) + if !sameRows(got, want) { + t.Fatalf("%s %s cursor=%d limit=%d:\n got %v\n want %v", mode, sh.name, c, limit, packetKeys(got), packetKeys(want)) + } + raw := legacyPacketRows(t, ctx, tx, legacyListPackets, sh, c, limit+1) + if !sameKeyRuns(got, raw, len(raw) == int(limit)+1) { + t.Fatalf("%s %s cursor=%d limit=%d vs raw:\n got %v\n raw %v", mode, sh.name, c, limit, packetKeys(got), packetKeys(raw)) + } + if len(got) > 0 { + nonEmpty++ + } + } + } + } + } + // since = MaxInt64 ms wraps when encoded and today returns rows; a time-based since > until check would not. + wrapped := packetShape{since: time.UnixMilli(math.MaxInt64), until: packetBase.Add(20 * time.Millisecond)} + if len(legacyPacketRows(t, ctx, tx, legacyListPackets, wrapped, 0, 100000)) == 0 { + t.Fatal("fixture: the wrapped since returns no rows today, so the shape pins nothing") + } + if nonEmpty < 500 { + t.Fatalf("only %d non-empty comparisons; the fixture does not exercise the statements", nonEmpty) + } +} + +// Three or more packets in one millisecond at a page boundary. +func TestPacketsGlobalCursorTiesPostgres(t *testing.T) { + ctx, tx, s := packetsGlobalTx(t) + for _, sh := range []packetShape{ + {name: "unfiltered"}, + {name: "pt 4", pt: []int16{4}}, + {name: "scopes #a", scopes: []string{"#a"}}, + {name: "window", since: packetBase.Add(-4 * time.Second)}, + } { + all := legacyPacketRows(t, ctx, tx, legacyListPacketsDet, sh, 0, 100000) + // Smallest page that ends inside a millisecond with at least one more row after the boundary. + limit := 0 + for i := 1; i < len(all); i++ { + if all[i].LastHeardAt.Time.UnixMilli() == all[i-1].LastHeardAt.Time.UnixMilli() && i >= 2 { + limit = i + break + } + } + if limit == 0 { + t.Fatalf("%s: no tied millisecond in %d rows", sh.name, len(all)) + } + page1 := newPacketRows(t, ctx, s, sh, 0, nil, int32(limit)) + if !sameRows(page1, all[:limit+1]) { + t.Fatalf("%s page 1: %v", sh.name, packetKeys(page1)) + } + last := page1[limit-1] + lastMS := last.LastHeardAt.Time.UnixMilli() + + // Legacy cursor: today's page, which skips the rest of the tied millisecond. + legacyNext := legacyPacketRows(t, ctx, tx, legacyListPacketsDet, sh, lastMS, int32(limit)+1) + gotLegacy := newPacketRows(t, ctx, s, sh, lastMS, nil, int32(limit)) + if !sameRows(gotLegacy, legacyNext) { + t.Fatalf("%s legacy cursor:\n got %v\n want %v", sh.name, packetKeys(gotLegacy), packetKeys(legacyNext)) + } + if len(gotLegacy) > 0 && gotLegacy[0].LastHeardAt.Time.UnixMilli() >= lastMS { + t.Fatalf("%s legacy cursor did not skip the tied millisecond", sh.name) + } + + // pageCursor: exactly the skipped tied rows, then the legacy page's rows. + var skipped []packetRow + for _, r := range all[limit:] { + if r.LastHeardAt.Time.UnixMilli() != lastMS { + break + } + skipped = append(skipped, r) + } + if len(skipped) == 0 { + t.Fatalf("%s: boundary is not inside a tied millisecond", sh.name) + } + want := append(slices.Clone(skipped), legacyNext...) + if len(want) > limit+1 { + want = want[:limit+1] + } + gotPC := newPacketRows(t, ctx, s, sh, 0, &api.PacketCursor{At: last.LastHeardAt.Time, Hash: last.PacketHash}, int32(limit)) + if !sameRows(gotPC, want) { + t.Fatalf("%s pageCursor:\n got %v\n want %v", sh.name, packetKeys(gotPC), packetKeys(want)) + } + + // A limit-1 pageCursor walk returns every row once, in order. A legacy cursor passed alongside is ignored. + var walked []packetRow + var pc *api.PacketCursor + for range len(all) + 2 { + var page []packetRow + if pc == nil { + page = newPacketRows(t, ctx, s, sh, 0, nil, 1) + } else { + page = newPacketRows(t, ctx, s, sh, lastMS, pc, 1) + } + if len(page) == 0 { + break + } + walked = append(walked, page[0]) + pc = &api.PacketCursor{At: page[0].LastHeardAt.Time, Hash: page[0].PacketHash} + } + if !sameRows(walked, all) { + t.Fatalf("%s limit-1 walk: %d rows, want %d", sh.name, len(walked), len(all)) + } + } +} + +// An inverted row inside [since, until] appears once, at its last_heard_at position, also when only the inverted +// branch can reach it. +func TestListPacketsWindowInvertedRowsPostgres(t *testing.T) { + ctx, tx, s := packetsGlobalTx(t) + since := packetBase.Add(14*time.Millisecond + 200*time.Microsecond) + inverted := fixtureHash(494) // last_heard_at base+14ms+2µs < since <= first_heard_at = last_heard_at + 300µs + var lha, fha time.Time + if err := tx.QueryRow(ctx, `SELECT last_heard_at, first_heard_at FROM packets WHERE packet_hash = $1`, inverted).Scan(&lha, &fha); err != nil { + t.Fatal(err) + } + if !lha.Before(since) || fha.Before(since) { + t.Fatalf("fixture: packet 494 last %v first %v, since %v", lha, fha, since) + } + for _, sh := range []packetShape{ + {name: "since", since: since}, + {name: "since scoped", since: since, scopes: []string{"#a", "#b", "#c"}}, + } { + want := legacyPacketRows(t, ctx, tx, legacyListPacketsDet, sh, 0, 100000) + for _, limit := range []int32{1, 3, 100000 - 1} { + got := newPacketRows(t, ctx, s, sh, 0, nil, limit) + if !sameRows(got, want[:min(len(want), int(limit)+1)]) { + t.Fatalf("%s limit %d:\n got %v\n want %v", sh.name, limit, packetKeys(got), packetKeys(want)) + } + } + n := 0 + for _, r := range want { + if bytes.Equal(r.PacketHash, inverted) { + n++ + } + } + if n != 1 { + t.Fatalf("%s: inverted packet appears %d times", sh.name, n) + } + pc := &api.PacketCursor{At: lha.Add(time.Microsecond), Hash: inverted} + got := newPacketRows(t, ctx, s, sh, 0, pc, 3) + if len(got) == 0 || !bytes.Equal(got[0].PacketHash, inverted) { + t.Fatalf("%s pageCursor just above the inverted row: %v", sh.name, packetKeys(got)) + } + } + for _, sh := range []packetShape{ + {name: "until only", until: packetBase.Add(-2 * time.Second)}, + {name: "since only", since: since}, + {name: "since = until", since: fha, until: fha}, + } { + want := legacyPacketRows(t, ctx, tx, legacyListPacketsDet, sh, 0, 100000) + got := newPacketRows(t, ctx, s, sh, 0, nil, 100000-1) + if len(want) == 0 || !sameRows(got, want) { + t.Fatalf("%s:\n got %v\n want %v", sh.name, packetKeys(got), packetKeys(want)) + } + } +} + +// Ties on the newest heard_at resolve to the highest observation id. +func TestPacketsLatestObserverTiePostgres(t *testing.T) { + ctx, tx, s := packetsGlobalTx(t) + rows := newPacketRows(t, ctx, s, packetShape{name: "all"}, 0, nil, 1000) + byHash := map[string]packetRow{} + for _, r := range rows { + byHash[string(r.PacketHash)] = r + } + checked := 0 + for i := 60; i <= 600; i += 60 { + h := fixtureHash(i) + var want uuid.UUID + var tied int + if err := tx.QueryRow(ctx, ` +SELECT (SELECT observer_id FROM packet_observations WHERE packet_hash = $1 ORDER BY heard_at DESC, id DESC LIMIT 1), + (SELECT count(*) FROM packet_observations WHERE packet_hash = $1 + AND heard_at = (SELECT max(heard_at) FROM packet_observations WHERE packet_hash = $1))::int`, h).Scan(&want, &tied); err != nil { + t.Fatal(err) + } + if tied != 2 { + t.Fatalf("packet %d: %d observations on the newest heard_at, want 2", i, tied) + } + r, ok := byHash[string(h)] + if !ok { + t.Fatalf("packet %d missing", i) + } + if r.LatestObserverID != want { + t.Errorf("packet %d: latest observer %v, want %v (highest id)", i, r.LatestObserverID, want) + } + checked++ + } + if checked != 10 { + t.Fatalf("checked %d packets, want 10", checked) + } +} diff --git a/db/packets_integration_test.go b/db/packets_integration_test.go index 2d019ee..d173dac 100644 --- a/db/packets_integration_test.go +++ b/db/packets_integration_test.go @@ -43,7 +43,7 @@ VALUES ('\x01', 4, 0, 1, '\x00', '\x00', NOW(), NOW())`) t.Fatal(err) } store := &Store{q: sqlc.New(tx)} - page, err := store.ListPackets(ctx, nil, nil, nil, nil, time.Time{}, time.Time{}, 0, 50) + page, err := store.ListPackets(ctx, nil, nil, nil, nil, time.Time{}, time.Time{}, 0, nil, 50) if err != nil { t.Fatalf("packet without observation must remain readable: %v", err) } @@ -62,7 +62,7 @@ UPDATE packets SET observation_count = 1 WHERE packet_hash = '\x01';`) t.Fatal(err) } for _, iatas := range [][]string{nil, {"YVR"}} { - page, err := store.ListPackets(ctx, nil, nil, iatas, nil, time.Time{}, time.Time{}, 0, 50) + page, err := store.ListPackets(ctx, nil, nil, iatas, nil, time.Time{}, time.Time{}, 0, nil, 50) if err != nil { t.Fatal(err) } diff --git a/db/packets_legacy_test.go b/db/packets_legacy_test.go new file mode 100644 index 0000000..82f4d0a --- /dev/null +++ b/db/packets_legacy_test.go @@ -0,0 +1,176 @@ +// Copyright 2026 Beacon Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +package db + +import "strings" + +// legacyListPackets is the pre-change global packets statement, verbatim. Args: $1 payload_types, $2 route_types, +// $3 since, $4 until, $5 cursor (all timestamptz, NULL when absent), $6 limit, $7 scopes. +const legacyListPackets = `-- name: ListPackets :many +SELECT + p.packet_hash, + p.payload_type, + COALESCE(CASE + WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' + THEN p.parsed_payload #>> '{appData,name}' + WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' + AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' + AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' + THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') + WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') + AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' + AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' + THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') + END, '')::text AS summary, + p.route_type, + p.first_heard_at, + p.last_heard_at, + p.scope_id, + ts.name AS scope_name, + p.observation_count AS observation_count, + -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. + po.observer_id AS latest_observer_id, + o.display_name AS latest_observer_name, + COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, + COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, + COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, + COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, + po.path_bytes AS latest_observer_path_bytes, + p.raw_payload, + p.origin_pubkey +FROM packets p +LEFT JOIN LATERAL ( + SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes + FROM packet_observations + WHERE packet_hash = p.packet_hash + ORDER BY heard_at DESC + LIMIT 1 +) po ON true +LEFT JOIN observers o ON o.id = po.observer_id +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +WHERE + (COALESCE(cardinality($1::smallint[]), 0) = 0 OR p.payload_type = ANY($1::smallint[])) + AND (COALESCE(cardinality($2::smallint[]), 0) = 0 OR p.route_type = ANY($2::smallint[])) + AND ($3::timestamptz IS NULL OR p.first_heard_at >= $3) + AND ($4::timestamptz IS NULL OR p.first_heard_at <= $4) + AND ($5::timestamptz IS NULL OR p.last_heard_at < $5) + AND (COALESCE(cardinality($7::text[]), 0) = 0 OR ts.name = ANY($7::text[])) +ORDER BY p.last_heard_at DESC +LIMIT $6 +` + +// legacyListPacketsDet is today's statement plus the two tiebreaks only. +var legacyListPacketsDet = strings.NewReplacer( + "ORDER BY p.last_heard_at DESC\nLIMIT $6", "ORDER BY p.last_heard_at DESC, p.packet_hash DESC\nLIMIT $6", + "ORDER BY heard_at DESC\n LIMIT 1", "ORDER BY heard_at DESC, id DESC\n LIMIT 1", +).Replace(legacyListPackets) + +func init() { + if !strings.Contains(legacyListPacketsDet, "p.last_heard_at DESC, p.packet_hash DESC\nLIMIT $6") || + !strings.Contains(legacyListPacketsDet, "heard_at DESC, id DESC\n LIMIT 1") { + panic("legacyListPacketsDet: a tiebreak replacement did not apply") + } + if strings.Count(legacyListNodesDet, "n.last_seen DESC, n.id DESC\n") != 2 { + panic("legacyListNodesDet: a tiebreak replacement did not apply") + } + if !strings.Contains(legacyListObserversDet, "o.last_seen DESC, o.id DESC\nLIMIT $7") { + panic("legacyListObserversDet: the tiebreak replacement did not apply") + } +} + +// legacyListNodes is the pre-change ListNodes statement, verbatim. Args: $1 node_type, $2 iatas, $3 paths tristate, +// $4 traces tristate, $5 pubkey, $6 name, $7 cursor (NULL when absent), $8 limit, $9 scope, $10 neighbors, +// $11 pubkey prefix. +const legacyListNodes = `-- name: ListNodes :many +WITH page AS ( +SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_seen, + n.radio_freq_mhz, n.radio_sf, n.radio_bw_khz, ts.name AS default_scope_name +FROM nodes n +LEFT JOIN transport_scopes ts ON ts.id = n.default_scope_id +WHERE + ($1 = 0 OR n.node_type = $1) + AND (COALESCE(cardinality($2::bpchar[]), 0) = 0 OR n.id IN (SELECT node_id FROM node_iatas WHERE iata = ANY($2::bpchar[]))) + AND ( + $3::text = 'any' + OR ($3::text = 'true' AND n.supports_multibyte_paths = TRUE) + OR ($3::text = 'false' AND n.supports_multibyte_paths = FALSE) + ) + AND ( + $4::text = 'any' + OR ($4::text = 'true' AND n.supports_multibyte_traces = TRUE) + OR ($4::text = 'false' AND n.supports_multibyte_traces = FALSE) + ) + AND ($5::bytea IS NULL OR n.public_key = $5) + AND ($6 = '' OR n.name ILIKE '%' || $6 || '%') + AND ($7::timestamptz IS NULL OR n.last_seen < $7) + AND ($9::text = '' OR ts.name = $9::text) + AND ($11::text = '' OR encode(n.public_key, 'hex') ILIKE $11 || '%') +ORDER BY n.last_seen DESC +LIMIT $8 +) +SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_seen, + n.radio_freq_mhz, n.radio_sf, n.radio_bw_khz, n.default_scope_name, + (SELECT json_agg(json_build_object('iata', ni.iata, 'lastHeard', (extract(epoch from ni.last_heard) * 1000)::bigint) ORDER BY ni.last_heard DESC) + FROM node_iatas ni WHERE ni.node_id = n.id) AS iatas, + EXISTS (SELECT 1 FROM observers o WHERE o.public_key = n.public_key) AS is_observer, + (SELECT o.id FROM observers o WHERE o.public_key = n.public_key LIMIT 1) AS observer_id, + (SELECT COUNT(DISTINCT nn.neighbor_id) FROM node_neighbors nn WHERE nn.node_id = n.id)::bigint AS known_neighbor_count, + (CASE WHEN $10::bool THEN + (SELECT COALESCE(array_agg(DISTINCT nn.neighbor_id), '{}'::uuid[]) FROM node_neighbors nn WHERE nn.node_id = n.id) + ELSE NULL END)::uuid[] AS neighbor_ids +FROM page n +ORDER BY n.last_seen DESC +` + +// legacyListNodesDet is today's ListNodes plus the id tiebreak in both ORDER BYs. +var legacyListNodesDet = strings.NewReplacer( + "ORDER BY n.last_seen DESC\nLIMIT $8", "ORDER BY n.last_seen DESC, n.id DESC\nLIMIT $8", + "FROM page n\nORDER BY n.last_seen DESC\n", "FROM page n\nORDER BY n.last_seen DESC, n.id DESC\n", +).Replace(legacyListNodes) + +// legacyListObservers is the pre-change ListObservers statement, verbatim. Args: $1 iatas, $2 type, $3 broker, +// $4 status, $5 name, $6 cursor (NULL when absent), $7 limit, $8 scope. +const legacyListObservers = `-- name: ListObservers :many +SELECT + o.id, + o.display_name, + o.observer_type, + o.last_status_at, + o.radio_freq_mhz, + o.radio_sf, + o.radio_bw_khz, + array_remove(array_agg(DISTINCT ts.name ORDER BY ts.name), NULL)::text[] AS scopes, +COALESCE(CASE + WHEN GREATEST(COALESCE(o.last_status_at, o.last_seen), o.last_seen) > NOW() - INTERVAL '5 minutes' THEN 'online' + ELSE 'offline' +END, 'offline')::text AS status, +COALESCE(o.last_iata, '')::text AS iata +FROM observers o +LEFT JOIN observer_brokers ob ON ob.observer_id = o.id +LEFT JOIN observer_scopes os ON os.observer_id = o.id +LEFT JOIN transport_scopes ts ON ts.id = os.scope_id +WHERE + (COALESCE(cardinality($1::bpchar[]), 0) = 0 OR o.last_iata = ANY($1::bpchar[])) + AND ($2 = '' OR o.observer_type = $2) + AND ($3 = '' OR ob.broker_name = $3) + AND ($4 = '' OR CASE + WHEN GREATEST(COALESCE(o.last_status_at, o.last_seen), o.last_seen) > NOW() - INTERVAL '5 minutes' THEN 'online' + ELSE 'offline' + END = $4) + AND ($5 = '' OR o.display_name ILIKE '%' || $5 || '%') + AND ($6::timestamptz IS NULL OR o.last_seen < $6) + AND ($8::text = '' OR EXISTS ( + SELECT 1 FROM observer_scopes os2 + JOIN transport_scopes ts2 ON ts2.id = os2.scope_id + WHERE os2.observer_id = o.id AND ts2.name = $8::text + )) +GROUP BY o.id +ORDER BY o.last_seen DESC +LIMIT $7 +` + +// legacyListObserversDet is today's ListObservers plus the id tiebreak. +var legacyListObserversDet = strings.NewReplacer( + "ORDER BY o.last_seen DESC\nLIMIT $7", "ORDER BY o.last_seen DESC, o.id DESC\nLIMIT $7", +).Replace(legacyListObservers) diff --git a/db/packets_schema_integration_test.go b/db/packets_schema_integration_test.go new file mode 100644 index 0000000..011441b --- /dev/null +++ b/db/packets_schema_integration_test.go @@ -0,0 +1,123 @@ +// Copyright 2026 Beacon Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +package db + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +// packetsEndStateMigrations are the packet type domains, row-cursor indexes and the inverted-heard statistics. +var packetsEndStateMigrations = []string{ + "025_packets_payload_type_domain.sql", + "026_validate_packets_payload_type_domain.sql", + "027_packets_route_type_domain.sql", + "028_validate_packets_route_type_domain.sql", + "029_packets_last_heard_hash.sql", + "030_packets_type_last_heard.sql", + "031_packets_scope_last_heard.sql", + "032_packets_heard_inverted.sql", + "033_packets_heard_inverted_stat.sql", +} + +// packetsEndStateIndexes are the indexes packetsEndStateMigrations build. +var packetsEndStateIndexes = []string{ + "idx_packets_last_heard_hash", + "idx_packets_type_last_heard", + "idx_packets_scope_last_heard", + "idx_packets_heard_inverted", +} + +// applyPacketsEndState applies packetsEndStateMigrations inside tx. +func applyPacketsEndState(t *testing.T, ctx context.Context, tx pgx.Tx) { + t.Helper() + applyMigrationsTx(t, ctx, tx, packetsEndStateMigrations...) +} + +// The domain checks are validated and the four packet indexes and the statistics object exist and are usable. +func TestPacketsDomainChecksPostgres(t *testing.T) { + ctx, pool := schemaPool(t) + if err := RunMigrations(ctx, pool); err != nil { + t.Fatal(err) + } + + want := map[string]string{ + "packets_payload_type_domain": "CHECK (((payload_type >= 0) AND (payload_type <= 15)))", + "packets_route_type_domain": "CHECK (((route_type >= 0) AND (route_type <= 3)))", + } + rows, err := pool.Query(ctx, `SELECT conname::text, convalidated, pg_get_constraintdef(oid) +FROM pg_constraint WHERE conrelid = 'packets'::regclass AND contype = 'c'`) + if err != nil { + t.Fatal(err) + } + got := map[string]string{} + for rows.Next() { + var name, def string + var validated bool + if err := rows.Scan(&name, &validated, &def); err != nil { + t.Fatal(err) + } + if !validated { + t.Errorf("%s not validated", name) + } + got[name] = def + } + if err := rows.Err(); err != nil { + t.Fatal(err) + } + if len(got) != len(want) { + t.Fatalf("packets checks = %v, want %v", got, want) + } + for name, def := range want { + if got[name] != def { + t.Errorf("%s = %q, want %q", name, got[name], def) + } + } + + for _, idx := range packetsEndStateIndexes { + var usable bool + if err := pool.QueryRow(ctx, `SELECT i.indisvalid AND i.indisready FROM pg_index i +WHERE i.indexrelid = to_regclass($1)`, idx).Scan(&usable); err != nil { + t.Fatalf("%s: %v", idx, err) + } + if !usable { + t.Errorf("%s not valid and ready", idx) + } + } + var def string + if err := pool.QueryRow(ctx, "SELECT pg_get_indexdef('idx_packets_heard_inverted'::regclass)").Scan(&def); err != nil { + t.Fatal(err) + } + if !strings.HasSuffix(def, "WHERE ((first_heard_at > last_heard_at) IS TRUE)") { + t.Errorf("idx_packets_heard_inverted = %q", def) + } + + var stats int + if err := pool.QueryRow(ctx, "SELECT count(*) FROM pg_statistic_ext WHERE stxname = 'packets_heard_inverted_stat' AND stxrelid = 'packets'::regclass").Scan(&stats); err != nil { + t.Fatal(err) + } + if stats != 1 { + t.Errorf("packets_heard_inverted_stat rows = %d, want 1", stats) + } + + for _, tc := range []struct { + name string + payloadType, routeType int + }{ + {"payload_type 16", 16, 0}, + {"route_type 4", 0, 4}, + } { + _, err := pool.Exec(ctx, `INSERT INTO packets (packet_hash, payload_type, payload_version, route_type, raw_payload, raw_header, first_heard_at, last_heard_at) +VALUES ('\x01', $1, 0, $2, '\x', '\x', now(), now())`, tc.payloadType, tc.routeType) + var pgErr *pgconn.PgError + if !errors.As(err, &pgErr) || pgErr.Code != "23514" { + t.Errorf("%s: err = %v, want SQLSTATE 23514", tc.name, err) + } + } +} diff --git a/db/packets_test.go b/db/packets_test.go index c5f0627..bef9e79 100644 --- a/db/packets_test.go +++ b/db/packets_test.go @@ -4,12 +4,15 @@ package db import ( + "bytes" "context" + "slices" "testing" "time" sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc" mockdb "github.com/MeshCore-Beacon/beacon-server/db/sqlc/mock" + "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/pgtype" @@ -65,9 +68,9 @@ func TestListPackets_Pagination(t *testing.T) { mock := mockdb.NewMockQuerier(ctrl) heardAt := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true} - rows := make([]sqlc.ListPacketsRow, 3) + rows := make([]sqlc.ListPacketsPageRow, 3) for i := range rows { - rows[i] = sqlc.ListPacketsRow{ + rows[i] = sqlc.ListPacketsPageRow{ PacketHash: []byte{0xde, 0xad}, FirstHeardAt: heardAt, LastHeardAt: heardAt, @@ -75,11 +78,11 @@ func TestListPackets_Pagination(t *testing.T) { } mock.EXPECT(). - ListPackets(gomock.Any(), gomock.Any()). + ListPacketsPage(gomock.Any(), gomock.Any()). Return(rows, nil) store := &Store{q: mock} - page, err := store.ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, 2) + page, err := store.ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, nil, 2) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -101,8 +104,8 @@ func TestListPackets_LatestObserverNil(t *testing.T) { heardAt := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true} mock.EXPECT(). - ListPackets(gomock.Any(), gomock.Any()). - Return([]sqlc.ListPacketsRow{ + ListPacketsPage(gomock.Any(), gomock.Any()). + Return([]sqlc.ListPacketsPageRow{ { PacketHash: []byte{0xde, 0xad}, FirstHeardAt: heardAt, @@ -112,7 +115,7 @@ func TestListPackets_LatestObserverNil(t *testing.T) { }, nil) store := &Store{q: mock} - page, err := store.ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, 10) + page, err := store.ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, nil, 10) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -131,8 +134,8 @@ func TestListPackets_LatestObserverSet(t *testing.T) { observerIATA := "YVR" mock.EXPECT(). - ListPackets(gomock.Any(), gomock.Any()). - Return([]sqlc.ListPacketsRow{ + ListPacketsPage(gomock.Any(), gomock.Any()). + Return([]sqlc.ListPacketsPageRow{ { PacketHash: []byte{0xde, 0xad}, FirstHeardAt: heardAt, @@ -144,7 +147,7 @@ func TestListPackets_LatestObserverSet(t *testing.T) { }, nil) store := &Store{q: mock} - page, err := store.ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, 10) + page, err := store.ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, nil, 10) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -168,8 +171,8 @@ func TestListPackets_LatestObserverPathFields(t *testing.T) { pathBytes := []byte{0xa1, 0xb2} mock.EXPECT(). - ListPackets(gomock.Any(), gomock.Any()). - Return([]sqlc.ListPacketsRow{ + ListPacketsPage(gomock.Any(), gomock.Any()). + Return([]sqlc.ListPacketsPageRow{ { PacketHash: []byte{0xde, 0xad}, FirstHeardAt: heardAt, @@ -183,7 +186,7 @@ func TestListPackets_LatestObserverPathFields(t *testing.T) { }, nil) store := &Store{q: mock} - page, err := store.ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, 10) + page, err := store.ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, nil, 10) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -513,7 +516,7 @@ func TestListPackets_IATAFilterRoutesToObservationIndex(t *testing.T) { }) store := &Store{q: mock} - page, err := store.ListPackets(context.Background(), nil, nil, []string{"ALF"}, nil, time.Time{}, time.Time{}, 0, 1) + page, err := store.ListPackets(context.Background(), nil, nil, []string{"ALF"}, nil, time.Time{}, time.Time{}, 0, nil, 1) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -555,7 +558,7 @@ func TestListPackets_SaturatedShortPageKeepsPaging(t *testing.T) { }, nil) store := &Store{q: mock} - page, err := store.ListPackets(context.Background(), nil, nil, []string{"YOW"}, nil, time.Time{}, time.Time{}, 0, 5) + page, err := store.ListPackets(context.Background(), nil, nil, []string{"YOW"}, nil, time.Time{}, time.Time{}, 0, nil, 5) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -590,7 +593,7 @@ func TestListPackets_CursorClampsToScanFloor(t *testing.T) { }}, nil) store := &Store{q: mock} - page, err := store.ListPackets(context.Background(), nil, nil, []string{"YOW", "YYZ"}, nil, time.Time{}, time.Time{}, 0, 5) + page, err := store.ListPackets(context.Background(), nil, nil, []string{"YOW", "YYZ"}, nil, time.Time{}, time.Time{}, 0, nil, 5) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -615,7 +618,7 @@ func TestListPackets_UnsaturatedShortPageEndsPaging(t *testing.T) { }}, nil) store := &Store{q: mock} - page, err := store.ListPackets(context.Background(), nil, nil, []string{"YOW"}, nil, time.Time{}, time.Time{}, 0, 5) + page, err := store.ListPackets(context.Background(), nil, nil, []string{"YOW"}, nil, time.Time{}, time.Time{}, 0, nil, 5) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -633,11 +636,11 @@ func TestListPackets_UnfilteredKeepsGlobalQuery(t *testing.T) { // gomock is strict: an unexpected ListPacketsByIATAs call fails the test. mock.EXPECT(). - ListPackets(gomock.Any(), gomock.Any()). - Return([]sqlc.ListPacketsRow{}, nil) + ListPacketsPage(gomock.Any(), gomock.Any()). + Return([]sqlc.ListPacketsPageRow{}, nil) store := &Store{q: mock} - if _, err := store.ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, 50); err != nil { + if _, err := store.ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, nil, 50); err != nil { t.Fatalf("unexpected error: %v", err) } } @@ -661,3 +664,233 @@ func TestDeleteOldPackets_LoopsUntilShortBatch(t *testing.T) { t.Fatal(err) } } + +// Each request shape runs exactly one statement (gomock fails on any other call), and a filter no row can match +// runs none and answers an empty page. +func TestListPacketsDispatch(t *testing.T) { + since := time.Date(2026, 1, 10, 0, 0, 0, 0, time.UTC) + allPayload := []int16{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} + cases := []struct { + name string + pt, rt []int16 + iatas []string + scopes []string + since, until time.Time + expect func(m *mockdb.MockQuerier) + }{ + {name: "unfiltered", expect: func(m *mockdb.MockQuerier) { + m.EXPECT().ListPacketsPage(gomock.Any(), gomock.Any()).Return(nil, nil) + }}, + {name: "payload types", pt: []int16{4}, expect: func(m *mockdb.MockQuerier) { + m.EXPECT().ListPacketsByTypes(gomock.Any(), gomock.Cond(func(p sqlc.ListPacketsByTypesParams) bool { + return slices.Equal(p.PayloadTypes, []int16{4}) && slices.Equal(p.RouteTypes, []int16{0, 1, 2, 3}) + })).Return(nil, nil) + }}, + {name: "route types only", rt: []int16{3}, expect: func(m *mockdb.MockQuerier) { + m.EXPECT().ListPacketsByTypes(gomock.Any(), gomock.Cond(func(p sqlc.ListPacketsByTypesParams) bool { + return slices.Equal(p.PayloadTypes, allPayload) && slices.Equal(p.RouteTypes, []int16{3}) + })).Return(nil, nil) + }}, + {name: "scopes", scopes: []string{"#a"}, expect: func(m *mockdb.MockQuerier) { + m.EXPECT().ListPacketsByScopes(gomock.Any(), gomock.Cond(func(p sqlc.ListPacketsByScopesParams) bool { + return slices.Equal(p.ScopeNames, []string{"#a"}) + })).Return(nil, nil) + }}, + {name: "scopes and types", pt: []int16{4}, scopes: []string{"#a"}, expect: func(m *mockdb.MockQuerier) { + m.EXPECT().ListPacketsByScopesTypes(gomock.Any(), gomock.Any()).Return(nil, nil) + }}, + {name: "since", since: since, expect: func(m *mockdb.MockQuerier) { + m.EXPECT().ListPacketsWindow(gomock.Any(), gomock.Cond(func(p sqlc.ListPacketsWindowParams) bool { + return p.SinceTs.Time.Equal(since) && p.UntilTs.InfinityModifier == pgtype.Infinity + })).Return(nil, nil) + }}, + {name: "since and scopes", since: since, scopes: []string{"#a"}, expect: func(m *mockdb.MockQuerier) { + m.EXPECT().ListPacketsWindowScoped(gomock.Any(), gomock.Any()).Return(nil, nil) + }}, + {name: "iatas", pt: []int16{4}, iatas: []string{"YVR"}, scopes: []string{"#a"}, since: since, expect: func(m *mockdb.MockQuerier) { + m.EXPECT().ListPacketsByIATAs(gomock.Any(), sqlc.ListPacketsByIATAsParams{ + Iatas: []string{"YVR"}, + ScanDepth: (7 + 1) * 8, + PayloadTypes: []int16{4}, + SinceTs: pgtype.Timestamptz{Time: since, Valid: true}, + ScopeNames: []string{"#a"}, + PageLimit: 7 + 1, + }).Return(nil, nil) + }}, + {name: "unknown payload type", pt: []int16{99}, expect: func(*mockdb.MockQuerier) {}}, + {name: "since after until", since: since, until: since.Add(-time.Millisecond), expect: func(m *mockdb.MockQuerier) { + m.EXPECT().ListPacketsWindow(gomock.Any(), gomock.Any()).Return(nil, nil) + }}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + mock := mockdb.NewMockQuerier(ctrl) + tc.expect(mock) + store := &Store{q: mock} + page, err := store.ListPackets(context.Background(), tc.pt, tc.rt, tc.iatas, tc.scopes, tc.since, tc.until, 0, nil, 7) + if err != nil { + t.Fatal(err) + } + if page.Items == nil { + t.Error("Items is nil, want an empty slice") + } + if len(page.Items) != 0 || page.HasMore || page.NextCursor != nil || page.NextPageCursor != nil { + t.Errorf("page = %+v, want empty", page) + } + }) + } +} + +// pgx sends a nil []byte as NULL, and (k, h) < (ts, NULL) matches no row: every statement must get a non-nil hash. +func TestListPackets_CursorHashNeverNil(t *testing.T) { + since := time.Date(2026, 1, 10, 0, 0, 0, 0, time.UTC) + shapes := []struct { + name string + pt []int16 + scopes []string + since time.Time + }{ + {name: "page"}, + {name: "types", pt: []int16{4}}, + {name: "scopes", scopes: []string{"#a"}}, + {name: "scopes types", pt: []int16{4}, scopes: []string{"#a"}}, + {name: "window", since: since}, + {name: "window scoped", since: since, scopes: []string{"#a"}}, + } + cursors := []struct { + name string + cursor int64 + pageCursor *api.PacketCursor + }{ + {name: "first page"}, + {name: "legacy cursor", cursor: 1792442625019}, + {name: "pageCursor", pageCursor: &api.PacketCursor{At: since, Hash: []byte{0xab}}}, + } + for _, sh := range shapes { + for _, cur := range cursors { + t.Run(sh.name+"/"+cur.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + mock := mockdb.NewMockQuerier(ctrl) + var hash []byte + mock.EXPECT().ListPacketsPage(gomock.Any(), gomock.Any()).AnyTimes(). + DoAndReturn(func(_ context.Context, p sqlc.ListPacketsPageParams) ([]sqlc.ListPacketsPageRow, error) { + hash = p.CursorHash + return nil, nil + }) + mock.EXPECT().ListPacketsByTypes(gomock.Any(), gomock.Any()).AnyTimes(). + DoAndReturn(func(_ context.Context, p sqlc.ListPacketsByTypesParams) ([]sqlc.ListPacketsByTypesRow, error) { + hash = p.CursorHash + return nil, nil + }) + mock.EXPECT().ListPacketsByScopes(gomock.Any(), gomock.Any()).AnyTimes(). + DoAndReturn(func(_ context.Context, p sqlc.ListPacketsByScopesParams) ([]sqlc.ListPacketsByScopesRow, error) { + hash = p.CursorHash + return nil, nil + }) + mock.EXPECT().ListPacketsByScopesTypes(gomock.Any(), gomock.Any()).AnyTimes(). + DoAndReturn(func(_ context.Context, p sqlc.ListPacketsByScopesTypesParams) ([]sqlc.ListPacketsByScopesTypesRow, error) { + hash = p.CursorHash + return nil, nil + }) + mock.EXPECT().ListPacketsWindow(gomock.Any(), gomock.Any()).AnyTimes(). + DoAndReturn(func(_ context.Context, p sqlc.ListPacketsWindowParams) ([]sqlc.ListPacketsWindowRow, error) { + hash = p.CursorHash + return nil, nil + }) + mock.EXPECT().ListPacketsWindowScoped(gomock.Any(), gomock.Any()).AnyTimes(). + DoAndReturn(func(_ context.Context, p sqlc.ListPacketsWindowScopedParams) ([]sqlc.ListPacketsWindowScopedRow, error) { + hash = p.CursorHash + return nil, nil + }) + store := &Store{q: mock} + if _, err := store.ListPackets(context.Background(), sh.pt, nil, nil, sh.scopes, sh.since, time.Time{}, cur.cursor, cur.pageCursor, 50); err != nil { + t.Fatal(err) + } + if hash == nil { + t.Fatal("CursorHash is nil") + } + if cur.pageCursor != nil && !bytes.Equal(hash, cur.pageCursor.Hash) { + t.Errorf("CursorHash = %x, want %x", hash, cur.pageCursor.Hash) + } + }) + } + } +} + +// A full page carries both cursors from its last row; the final page carries neither. +func TestListPackets_NextPageCursor(t *testing.T) { + base := time.Date(2026, 1, 10, 0, 0, 0, 0, time.UTC) + rows := []sqlc.ListPacketsPageRow{ + {PacketHash: []byte{0x03}, LastHeardAt: pgtype.Timestamptz{Time: base.Add(3*time.Millisecond + 7*time.Microsecond), Valid: true}}, + {PacketHash: []byte{0x02, 0xff}, LastHeardAt: pgtype.Timestamptz{Time: base.Add(2*time.Millisecond + 5*time.Microsecond), Valid: true}}, + {PacketHash: []byte{0x01}, LastHeardAt: pgtype.Timestamptz{Time: base.Add(time.Millisecond), Valid: true}}, + } + run := func(t *testing.T, rows []sqlc.ListPacketsPageRow) api.PacketPage { + t.Helper() + ctrl := gomock.NewController(t) + mock := mockdb.NewMockQuerier(ctrl) + mock.EXPECT().ListPacketsPage(gomock.Any(), gomock.Any()).Return(rows, nil) + page, err := (&Store{q: mock}).ListPackets(context.Background(), nil, nil, nil, nil, time.Time{}, time.Time{}, 0, nil, 2) + if err != nil { + t.Fatal(err) + } + return page + } + + t.Run("full page", func(t *testing.T) { + page := run(t, rows) + last := rows[1] + wantPC := api.PacketCursor{At: last.LastHeardAt.Time, Hash: last.PacketHash}.String() + if len(page.Items) != 2 || !page.HasMore { + t.Fatalf("items %d, hasMore %v; want 2, true", len(page.Items), page.HasMore) + } + if page.NextCursor == nil || *page.NextCursor != last.LastHeardAt.Time.UnixMilli() { + t.Errorf("NextCursor = %v, want %d", page.NextCursor, last.LastHeardAt.Time.UnixMilli()) + } + if page.NextPageCursor == nil || *page.NextPageCursor != wantPC { + t.Errorf("NextPageCursor = %v, want %s", page.NextPageCursor, wantPC) + } + }) + t.Run("last page", func(t *testing.T) { + page := run(t, rows[:2]) + if len(page.Items) != 2 || page.HasMore || page.NextCursor != nil || page.NextPageCursor != nil { + t.Errorf("page = %+v, want 2 items and no cursors", page) + } + }) +} + +// The region path keeps today's numeric cursor, saturation padding and scan-floor clamp, and emits no pageCursor. +func TestListPackets_RegionPathIgnoresPageCursor(t *testing.T) { + ctrl := gomock.NewController(t) + mock := mockdb.NewMockQuerier(ctrl) + const cursor = int64(1792442625019) + oldest := time.Date(2026, 8, 7, 12, 57, 25, 0, time.UTC) + floor := oldest.Add(30 * time.Second) + + mock.EXPECT(). + ListPacketsByIATAs(gomock.Any(), gomock.Cond(func(p sqlc.ListPacketsByIATAsParams) bool { + return p.CursorTs.Valid && p.CursorTs.Time.Equal(time.UnixMilli(cursor)) && slices.Equal(p.Iatas, []string{"YVR"}) + })). + Return([]sqlc.ListPacketsByIATAsRow{{ + PacketHash: []byte{0x01}, + SiteHeardAt: pgtype.Timestamptz{Time: oldest, Valid: true}, + ScanSaturated: true, + ScanFloor: pgtype.Timestamptz{Time: floor, Valid: true}, + }}, nil) + + pc := &api.PacketCursor{At: time.Date(2026, 1, 10, 0, 0, 0, 0, time.UTC), Hash: []byte{0xab}} + page, err := (&Store{q: mock}).ListPackets(context.Background(), nil, nil, []string{"YVR"}, nil, time.Time{}, time.Time{}, cursor, pc, 5) + if err != nil { + t.Fatal(err) + } + if len(page.Items) != 1 || !page.HasMore { + t.Fatalf("items %d, hasMore %v; want 1, true", len(page.Items), page.HasMore) + } + if page.NextCursor == nil || *page.NextCursor != floor.UnixMilli() { + t.Errorf("NextCursor = %v, want the scan floor %d", page.NextCursor, floor.UnixMilli()) + } + if page.NextPageCursor != nil { + t.Errorf("NextPageCursor = %q, want nil", *page.NextPageCursor) + } +} diff --git a/db/planguard/baseline/fixture/pk.e_c1.auto.json b/db/planguard/baseline/fixture/pk.e_c1.auto.json new file mode 100644 index 0000000..206711c --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_c1.auto.json @@ -0,0 +1,16 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_c1.custom.json b/db/planguard/baseline/fixture/pk.e_c1.custom.json new file mode 100644 index 0000000..206711c --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_c1.custom.json @@ -0,0 +1,16 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_c1.generic.json b/db/planguard/baseline/fixture/pk.e_c1.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_c1.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_c9999.auto.json b/db/planguard/baseline/fixture/pk.e_c9999.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_c9999.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_c9999.custom.json b/db/planguard/baseline/fixture/pk.e_c9999.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_c9999.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_c9999.generic.json b/db/planguard/baseline/fixture/pk.e_c9999.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_c9999.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_cneg.auto.json b/db/planguard/baseline/fixture/pk.e_cneg.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_cneg.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_cneg.custom.json b/db/planguard/baseline/fixture/pk.e_cneg.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_cneg.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_cneg.generic.json b/db/planguard/baseline/fixture/pk.e_cneg.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_cneg.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_l1.auto.json b/db/planguard/baseline/fixture/pk.e_l1.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_l1.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_l1.custom.json b/db/planguard/baseline/fixture/pk.e_l1.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_l1.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_l1.generic.json b/db/planguard/baseline/fixture/pk.e_l1.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_l1.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_l200.auto.json b/db/planguard/baseline/fixture/pk.e_l200.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_l200.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_l200.custom.json b/db/planguard/baseline/fixture/pk.e_l200.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_l200.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_l200.generic.json b/db/planguard/baseline/fixture/pk.e_l200.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_l200.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_l50.auto.json b/db/planguard/baseline/fixture/pk.e_l50.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_l50.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_l50.custom.json b/db/planguard/baseline/fixture/pk.e_l50.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_l50.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_l50.generic.json b/db/planguard/baseline/fixture/pk.e_l50.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_l50.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_pc_deep.auto.json b/db/planguard/baseline/fixture/pk.e_pc_deep.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_pc_deep.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_pc_deep.custom.json b/db/planguard/baseline/fixture/pk.e_pc_deep.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_pc_deep.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_pc_deep.generic.json b/db/planguard/baseline/fixture/pk.e_pc_deep.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_pc_deep.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_pc_ustie_l1.auto.json b/db/planguard/baseline/fixture/pk.e_pc_ustie_l1.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_pc_ustie_l1.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_pc_ustie_l1.custom.json b/db/planguard/baseline/fixture/pk.e_pc_ustie_l1.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_pc_ustie_l1.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_pc_ustie_l1.generic.json b/db/planguard/baseline/fixture/pk.e_pc_ustie_l1.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_pc_ustie_l1.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_pc_ustie_l2.auto.json b/db/planguard/baseline/fixture/pk.e_pc_ustie_l2.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_pc_ustie_l2.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_pc_ustie_l2.custom.json b/db/planguard/baseline/fixture/pk.e_pc_ustie_l2.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_pc_ustie_l2.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.e_pc_ustie_l2.generic.json b/db/planguard/baseline/fixture/pk.e_pc_ustie_l2.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.e_pc_ustie_l2.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.maint.delete_old_packets.victims.custom.json b/db/planguard/baseline/fixture/pk.maint.delete_old_packets.victims.custom.json index f38b4d4..b68ce8b 100644 --- a/db/planguard/baseline/fixture/pk.maint.delete_old_packets.victims.custom.json +++ b/db/planguard/baseline/fixture/pk.maint.delete_old_packets.victims.custom.json @@ -1,5 +1,4 @@ [ "Limit", - " Incremental Sort", - " Index Scan on packets vp using idx_packets_last_heard scan=Backward" + " Index Only Scan on packets vp using idx_packets_last_heard_hash scan=Backward" ] diff --git a/db/planguard/baseline/fixture/pk.s_all.custom.json b/db/planguard/baseline/fixture/pk.s_all.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_all.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_all_pt14.custom.json b/db/planguard/baseline/fixture/pk.s_all_pt14.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_all_pt14.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_all_pt4.custom.json b/db/planguard/baseline/fixture/pk.s_all_pt4.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_all_pt4.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_all_pt9.custom.json b/db/planguard/baseline/fixture/pk.s_all_pt9.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_all_pt9.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_all_rt3.custom.json b/db/planguard/baseline/fixture/pk.s_all_rt3.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_all_rt3.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_allbogus6491.custom.json b/db/planguard/baseline/fixture/pk.s_allbogus6491.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_allbogus6491.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_big0.custom.json b/db/planguard/baseline/fixture/pk.s_big0.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_big0.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_big0_pt12rt2.custom.json b/db/planguard/baseline/fixture/pk.s_big0_pt12rt2.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_big0_pt12rt2.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_big0_pt14.custom.json b/db/planguard/baseline/fixture/pk.s_big0_pt14.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_big0_pt14.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_big1.custom.json b/db/planguard/baseline/fixture/pk.s_big1.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_big1.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_bogus5000.custom.json b/db/planguard/baseline/fixture/pk.s_bogus5000.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_bogus5000.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_bogus5000_pt4.custom.json b/db/planguard/baseline/fixture/pk.s_bogus5000_pt4.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_bogus5000_pt4.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_case.custom.json b/db/planguard/baseline/fixture/pk.s_case.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_case.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_dash.custom.json b/db/planguard/baseline/fixture/pk.s_dash.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_dash.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_dash_pt14.custom.json b/db/planguard/baseline/fixture/pk.s_dash_pt14.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_dash_pt14.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_dash_pt4.custom.json b/db/planguard/baseline/fixture/pk.s_dash_pt4.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_dash_pt4.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_dash_rt1.custom.json b/db/planguard/baseline/fixture/pk.s_dash_rt1.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_dash_rt1.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_dups.custom.json b/db/planguard/baseline/fixture/pk.s_dups.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_dups.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_emptyslice.auto.json b/db/planguard/baseline/fixture/pk.s_emptyslice.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_emptyslice.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_emptyslice.custom.json b/db/planguard/baseline/fixture/pk.s_emptyslice.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_emptyslice.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_emptyslice.generic.json b/db/planguard/baseline/fixture/pk.s_emptyslice.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_emptyslice.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_grr.custom.json b/db/planguard/baseline/fixture/pk.s_grr.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_grr.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_nl.custom.json b/db/planguard/baseline/fixture/pk.s_nl.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_nl.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_nl_l1_tie.custom.json b/db/planguard/baseline/fixture/pk.s_nl_l1_tie.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_nl_l1_tie.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_nl_pt14.custom.json b/db/planguard/baseline/fixture/pk.s_nl_pt14.custom.json new file mode 100644 index 0000000..d134b76 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_nl_pt14.custom.json @@ -0,0 +1,20 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_nl_pt4rt1.custom.json b/db/planguard/baseline/fixture/pk.s_nl_pt4rt1.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_nl_pt4rt1.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_nl_rt3.custom.json b/db/planguard/baseline/fixture/pk.s_nl_rt3.custom.json new file mode 100644 index 0000000..d134b76 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_nl_rt3.custom.json @@ -0,0 +1,20 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_unknown.custom.json b/db/planguard/baseline/fixture/pk.s_unknown.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_unknown.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.s_ws.custom.json b/db/planguard/baseline/fixture/pk.s_ws.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.s_ws.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_all16_rt.auto.json b/db/planguard/baseline/fixture/pk.t_all16_rt.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_all16_rt.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_all16_rt.custom.json b/db/planguard/baseline/fixture/pk.t_all16_rt.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_all16_rt.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_all16_rt.generic.json b/db/planguard/baseline/fixture/pk.t_all16_rt.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_all16_rt.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt054.auto.json b/db/planguard/baseline/fixture/pk.t_pt054.auto.json new file mode 100644 index 0000000..3ad4285 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt054.auto.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt054.custom.json b/db/planguard/baseline/fixture/pk.t_pt054.custom.json new file mode 100644 index 0000000..3ad4285 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt054.custom.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt054.generic.json b/db/planguard/baseline/fixture/pk.t_pt054.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt054.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt12_rt2.auto.json b/db/planguard/baseline/fixture/pk.t_pt12_rt2.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt12_rt2.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt12_rt2.custom.json b/db/planguard/baseline/fixture/pk.t_pt12_rt2.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt12_rt2.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt12_rt2.generic.json b/db/planguard/baseline/fixture/pk.t_pt12_rt2.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt12_rt2.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt13_l200.auto.json b/db/planguard/baseline/fixture/pk.t_pt13_l200.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt13_l200.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt13_l200.custom.json b/db/planguard/baseline/fixture/pk.t_pt13_l200.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt13_l200.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt13_l200.generic.json b/db/planguard/baseline/fixture/pk.t_pt13_l200.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt13_l200.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt14.auto.json b/db/planguard/baseline/fixture/pk.t_pt14.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt14.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt14.custom.json b/db/planguard/baseline/fixture/pk.t_pt14.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt14.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt14.generic.json b/db/planguard/baseline/fixture/pk.t_pt14.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt14.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt2_rt23.auto.json b/db/planguard/baseline/fixture/pk.t_pt2_rt23.auto.json new file mode 100644 index 0000000..58d52ac --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt2_rt23.auto.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt2_rt23.custom.json b/db/planguard/baseline/fixture/pk.t_pt2_rt23.custom.json new file mode 100644 index 0000000..58d52ac --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt2_rt23.custom.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt2_rt23.generic.json b/db/planguard/baseline/fixture/pk.t_pt2_rt23.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt2_rt23.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt4.auto.json b/db/planguard/baseline/fixture/pk.t_pt4.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt4.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt4.custom.json b/db/planguard/baseline/fixture/pk.t_pt4.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt4.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt4.generic.json b/db/planguard/baseline/fixture/pk.t_pt4.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt4.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt44.auto.json b/db/planguard/baseline/fixture/pk.t_pt44.auto.json new file mode 100644 index 0000000..3ad4285 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt44.auto.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt44.custom.json b/db/planguard/baseline/fixture/pk.t_pt44.custom.json new file mode 100644 index 0000000..3ad4285 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt44.custom.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt44.generic.json b/db/planguard/baseline/fixture/pk.t_pt44.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt44.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt4_pc_deep.auto.json b/db/planguard/baseline/fixture/pk.t_pt4_pc_deep.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt4_pc_deep.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt4_pc_deep.custom.json b/db/planguard/baseline/fixture/pk.t_pt4_pc_deep.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt4_pc_deep.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt4_pc_deep.generic.json b/db/planguard/baseline/fixture/pk.t_pt4_pc_deep.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt4_pc_deep.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt99_4.auto.json b/db/planguard/baseline/fixture/pk.t_pt99_4.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt99_4.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt99_4.custom.json b/db/planguard/baseline/fixture/pk.t_pt99_4.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt99_4.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_pt99_4.generic.json b/db/planguard/baseline/fixture/pk.t_pt99_4.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_pt99_4.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_ptempty.auto.json b/db/planguard/baseline/fixture/pk.t_ptempty.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_ptempty.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_ptempty.custom.json b/db/planguard/baseline/fixture/pk.t_ptempty.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_ptempty.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_ptempty.generic.json b/db/planguard/baseline/fixture/pk.t_ptempty.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_ptempty.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_ptempty_rt3.auto.json b/db/planguard/baseline/fixture/pk.t_ptempty_rt3.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_ptempty_rt3.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_ptempty_rt3.custom.json b/db/planguard/baseline/fixture/pk.t_ptempty_rt3.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_ptempty_rt3.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_ptempty_rt3.generic.json b/db/planguard/baseline/fixture/pk.t_ptempty_rt3.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_ptempty_rt3.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_rt00.auto.json b/db/planguard/baseline/fixture/pk.t_rt00.auto.json new file mode 100644 index 0000000..3ad4285 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_rt00.auto.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_rt00.custom.json b/db/planguard/baseline/fixture/pk.t_rt00.custom.json new file mode 100644 index 0000000..3ad4285 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_rt00.custom.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_rt00.generic.json b/db/planguard/baseline/fixture/pk.t_rt00.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_rt00.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_rt37.auto.json b/db/planguard/baseline/fixture/pk.t_rt37.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_rt37.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_rt37.custom.json b/db/planguard/baseline/fixture/pk.t_rt37.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_rt37.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_rt37.generic.json b/db/planguard/baseline/fixture/pk.t_rt37.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_rt37.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_rt3_l200.auto.json b/db/planguard/baseline/fixture/pk.t_rt3_l200.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_rt3_l200.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_rt3_l200.custom.json b/db/planguard/baseline/fixture/pk.t_rt3_l200.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_rt3_l200.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/pk.t_rt3_l200.generic.json b/db/planguard/baseline/fixture/pk.t_rt3_l200.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/fixture/pk.t_rt3_l200.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.SEA.custom.json b/db/planguard/baseline/fixture/sm.nodes.SEA.custom.json new file mode 100644 index 0000000..ceba4d4 --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.SEA.custom.json @@ -0,0 +1,20 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Bitmap Heap Scan on node_iatas [SubPlan]", + " Bitmap Index Scan using idx_node_iatas_iata", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.SEA_type2.custom.json b/db/planguard/baseline/fixture/sm.nodes.SEA_type2.custom.json new file mode 100644 index 0000000..ceba4d4 --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.SEA_type2.custom.json @@ -0,0 +1,20 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Bitmap Heap Scan on node_iatas [SubPlan]", + " Bitmap Index Scan using idx_node_iatas_iata", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.bogus5000.custom.json b/db/planguard/baseline/fixture/sm.nodes.bogus5000.custom.json new file mode 100644 index 0000000..8e3ea16 --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.bogus5000.custom.json @@ -0,0 +1,19 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Seq Scan on node_iatas [SubPlan]", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.first_l201_neighbors_SEA.custom.json b/db/planguard/baseline/fixture/sm.nodes.first_l201_neighbors_SEA.custom.json new file mode 100644 index 0000000..ceba4d4 --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.first_l201_neighbors_SEA.custom.json @@ -0,0 +1,20 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Bitmap Heap Scan on node_iatas [SubPlan]", + " Bitmap Index Scan using idx_node_iatas_iata", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.first_l501.custom.json b/db/planguard/baseline/fixture/sm.nodes.first_l501.custom.json new file mode 100644 index 0000000..6572a8b --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.first_l501.custom.json @@ -0,0 +1,18 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.global.custom.json b/db/planguard/baseline/fixture/sm.nodes.global.custom.json new file mode 100644 index 0000000..6572a8b --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.global.custom.json @@ -0,0 +1,18 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.global_deep_cursor200.custom.json b/db/planguard/baseline/fixture/sm.nodes.global_deep_cursor200.custom.json new file mode 100644 index 0000000..6572a8b --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.global_deep_cursor200.custom.json @@ -0,0 +1,18 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.legacy_cursor_l501.custom.json b/db/planguard/baseline/fixture/sm.nodes.legacy_cursor_l501.custom.json new file mode 100644 index 0000000..6572a8b --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.legacy_cursor_l501.custom.json @@ -0,0 +1,18 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.multibyte_true.custom.json b/db/planguard/baseline/fixture/sm.nodes.multibyte_true.custom.json new file mode 100644 index 0000000..b639805 --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.multibyte_true.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Nested Loop (Left)", + " Seq Scan on nodes n_1", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.name_a.custom.json b/db/planguard/baseline/fixture/sm.nodes.name_a.custom.json new file mode 100644 index 0000000..b639805 --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.name_a.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Nested Loop (Left)", + " Seq Scan on nodes n_1", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.pagecursor_l501.custom.json b/db/planguard/baseline/fixture/sm.nodes.pagecursor_l501.custom.json new file mode 100644 index 0000000..6572a8b --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.pagecursor_l501.custom.json @@ -0,0 +1,18 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.pubkey_prefix.custom.json b/db/planguard/baseline/fixture/sm.nodes.pubkey_prefix.custom.json new file mode 100644 index 0000000..b639805 --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.pubkey_prefix.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Nested Loop (Left)", + " Seq Scan on nodes n_1", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.traces_true.custom.json b/db/planguard/baseline/fixture/sm.nodes.traces_true.custom.json new file mode 100644 index 0000000..b639805 --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.traces_true.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Nested Loop (Left)", + " Seq Scan on nodes n_1", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.type1.custom.json b/db/planguard/baseline/fixture/sm.nodes.type1.custom.json new file mode 100644 index 0000000..6572a8b --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.type1.custom.json @@ -0,0 +1,18 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.type2.custom.json b/db/planguard/baseline/fixture/sm.nodes.type2.custom.json new file mode 100644 index 0000000..6572a8b --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.type2.custom.json @@ -0,0 +1,18 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.type2_deep_cursor.custom.json b/db/planguard/baseline/fixture/sm.nodes.type2_deep_cursor.custom.json new file mode 100644 index 0000000..6572a8b --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.type2_deep_cursor.custom.json @@ -0,0 +1,18 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.type3.custom.json b/db/planguard/baseline/fixture/sm.nodes.type3.custom.json new file mode 100644 index 0000000..6572a8b --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.type3.custom.json @@ -0,0 +1,18 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.nodes.type9_empty.custom.json b/db/planguard/baseline/fixture/sm.nodes.type9_empty.custom.json new file mode 100644 index 0000000..b639805 --- /dev/null +++ b/db/planguard/baseline/fixture/sm.nodes.type9_empty.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Nested Loop (Left)", + " Seq Scan on nodes n_1", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Seq Scan on node_neighbors nn" +] diff --git a/db/planguard/baseline/fixture/sm.obsv.first_l201.custom.json b/db/planguard/baseline/fixture/sm.obsv.first_l201.custom.json new file mode 100644 index 0000000..7deb7f9 --- /dev/null +++ b/db/planguard/baseline/fixture/sm.obsv.first_l201.custom.json @@ -0,0 +1,15 @@ +[ + "Limit", + " Sort", + " Aggregate partial=Simple {Sorted}", + " Incremental Sort", + " Merge Join (Left)", + " Merge Join (Left)", + " Index Scan on observers o using observers_pkey scan=Backward", + " Sort", + " Nested Loop (Left)", + " Seq Scan on observer_scopes os", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward", + " Sort", + " Seq Scan on observer_brokers ob" +] diff --git a/db/planguard/baseline/fixture/sm.obsv.legacy_l201.custom.json b/db/planguard/baseline/fixture/sm.obsv.legacy_l201.custom.json new file mode 100644 index 0000000..7deb7f9 --- /dev/null +++ b/db/planguard/baseline/fixture/sm.obsv.legacy_l201.custom.json @@ -0,0 +1,15 @@ +[ + "Limit", + " Sort", + " Aggregate partial=Simple {Sorted}", + " Incremental Sort", + " Merge Join (Left)", + " Merge Join (Left)", + " Index Scan on observers o using observers_pkey scan=Backward", + " Sort", + " Nested Loop (Left)", + " Seq Scan on observer_scopes os", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward", + " Sort", + " Seq Scan on observer_brokers ob" +] diff --git a/db/planguard/baseline/local/pk.e_at_tied.auto.json b/db/planguard/baseline/local/pk.e_at_tied.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_at_tied.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_at_tied.custom.json b/db/planguard/baseline/local/pk.e_at_tied.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_at_tied.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_at_tied.generic.json b/db/planguard/baseline/local/pk.e_at_tied.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_at_tied.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_c1.auto.json b/db/planguard/baseline/local/pk.e_c1.auto.json new file mode 100644 index 0000000..206711c --- /dev/null +++ b/db/planguard/baseline/local/pk.e_c1.auto.json @@ -0,0 +1,16 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_c1.custom.json b/db/planguard/baseline/local/pk.e_c1.custom.json new file mode 100644 index 0000000..206711c --- /dev/null +++ b/db/planguard/baseline/local/pk.e_c1.custom.json @@ -0,0 +1,16 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_c1.generic.json b/db/planguard/baseline/local/pk.e_c1.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_c1.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_c9999.auto.json b/db/planguard/baseline/local/pk.e_c9999.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_c9999.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_c9999.custom.json b/db/planguard/baseline/local/pk.e_c9999.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_c9999.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_c9999.generic.json b/db/planguard/baseline/local/pk.e_c9999.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_c9999.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_cneg.auto.json b/db/planguard/baseline/local/pk.e_cneg.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_cneg.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_cneg.custom.json b/db/planguard/baseline/local/pk.e_cneg.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_cneg.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_cneg.generic.json b/db/planguard/baseline/local/pk.e_cneg.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_cneg.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_deep.auto.json b/db/planguard/baseline/local/pk.e_deep.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_deep.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_deep.custom.json b/db/planguard/baseline/local/pk.e_deep.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_deep.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_deep.generic.json b/db/planguard/baseline/local/pk.e_deep.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_deep.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_deep200.auto.json b/db/planguard/baseline/local/pk.e_deep200.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_deep200.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_deep200.custom.json b/db/planguard/baseline/local/pk.e_deep200.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_deep200.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_deep200.generic.json b/db/planguard/baseline/local/pk.e_deep200.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_deep200.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_l1.auto.json b/db/planguard/baseline/local/pk.e_l1.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_l1.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_l1.custom.json b/db/planguard/baseline/local/pk.e_l1.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_l1.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_l1.generic.json b/db/planguard/baseline/local/pk.e_l1.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_l1.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_l200.auto.json b/db/planguard/baseline/local/pk.e_l200.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_l200.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_l200.custom.json b/db/planguard/baseline/local/pk.e_l200.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_l200.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_l200.generic.json b/db/planguard/baseline/local/pk.e_l200.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_l200.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_l50.auto.json b/db/planguard/baseline/local/pk.e_l50.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_l50.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_l50.custom.json b/db/planguard/baseline/local/pk.e_l50.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_l50.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_l50.generic.json b/db/planguard/baseline/local/pk.e_l50.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_l50.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_pc_deep.auto.json b/db/planguard/baseline/local/pk.e_pc_deep.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_pc_deep.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_pc_deep.custom.json b/db/planguard/baseline/local/pk.e_pc_deep.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_pc_deep.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_pc_deep.generic.json b/db/planguard/baseline/local/pk.e_pc_deep.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_pc_deep.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_pc_ustie_l1.auto.json b/db/planguard/baseline/local/pk.e_pc_ustie_l1.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_pc_ustie_l1.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_pc_ustie_l1.custom.json b/db/planguard/baseline/local/pk.e_pc_ustie_l1.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_pc_ustie_l1.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_pc_ustie_l1.generic.json b/db/planguard/baseline/local/pk.e_pc_ustie_l1.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_pc_ustie_l1.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_pc_ustie_l2.auto.json b/db/planguard/baseline/local/pk.e_pc_ustie_l2.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_pc_ustie_l2.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_pc_ustie_l2.custom.json b/db/planguard/baseline/local/pk.e_pc_ustie_l2.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_pc_ustie_l2.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_pc_ustie_l2.generic.json b/db/planguard/baseline/local/pk.e_pc_ustie_l2.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_pc_ustie_l2.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_tail.auto.json b/db/planguard/baseline/local/pk.e_tail.auto.json new file mode 100644 index 0000000..206711c --- /dev/null +++ b/db/planguard/baseline/local/pk.e_tail.auto.json @@ -0,0 +1,16 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_tail.custom.json b/db/planguard/baseline/local/pk.e_tail.custom.json new file mode 100644 index 0000000..206711c --- /dev/null +++ b/db/planguard/baseline/local/pk.e_tail.custom.json @@ -0,0 +1,16 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_tail.generic.json b/db/planguard/baseline/local/pk.e_tail.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_tail.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_tiedms_l1.auto.json b/db/planguard/baseline/local/pk.e_tiedms_l1.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_tiedms_l1.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_tiedms_l1.custom.json b/db/planguard/baseline/local/pk.e_tiedms_l1.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_tiedms_l1.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_tiedms_l1.generic.json b/db/planguard/baseline/local/pk.e_tiedms_l1.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_tiedms_l1.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_tiedms_l2.auto.json b/db/planguard/baseline/local/pk.e_tiedms_l2.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_tiedms_l2.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_tiedms_l2.custom.json b/db/planguard/baseline/local/pk.e_tiedms_l2.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_tiedms_l2.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_tiedms_l2.generic.json b/db/planguard/baseline/local/pk.e_tiedms_l2.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_tiedms_l2.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_ustie_l1.auto.json b/db/planguard/baseline/local/pk.e_ustie_l1.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_ustie_l1.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_ustie_l1.custom.json b/db/planguard/baseline/local/pk.e_ustie_l1.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_ustie_l1.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_ustie_l1.generic.json b/db/planguard/baseline/local/pk.e_ustie_l1.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_ustie_l1.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_ustie_l2.auto.json b/db/planguard/baseline/local/pk.e_ustie_l2.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_ustie_l2.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_ustie_l2.custom.json b/db/planguard/baseline/local/pk.e_ustie_l2.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_ustie_l2.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.e_ustie_l2.generic.json b/db/planguard/baseline/local/pk.e_ustie_l2.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.e_ustie_l2.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.inv_all_5m.custom.json b/db/planguard/baseline/local/pk.inv_all_5m.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.inv_all_5m.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.inv_pt1_10m.custom.json b/db/planguard/baseline/local/pk.inv_pt1_10m.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.inv_pt1_10m.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.inv_pt4rt2_1h.custom.json b/db/planguard/baseline/local/pk.inv_pt4rt2_1h.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.inv_pt4rt2_1h.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.inv_rt0_32m.custom.json b/db/planguard/baseline/local/pk.inv_rt0_32m.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.inv_rt0_32m.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.inv_rt0_cur.custom.json b/db/planguard/baseline/local/pk.inv_rt0_cur.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.inv_rt0_cur.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.inv_scoped_none.custom.json b/db/planguard/baseline/local/pk.inv_scoped_none.custom.json new file mode 100644 index 0000000..655e9a1 --- /dev/null +++ b/db/planguard/baseline/local/pk.inv_scoped_none.custom.json @@ -0,0 +1,24 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Append", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Index Scan on transport_scopes using transport_scopes_name_key scan=Forward [InitPlan]", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.inv_since_only.custom.json b/db/planguard/baseline/local/pk.inv_since_only.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.inv_since_only.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.maint.delete_old_packets.dml.custom.json b/db/planguard/baseline/local/pk.maint.delete_old_packets.dml.custom.json new file mode 100644 index 0000000..4647c08 --- /dev/null +++ b/db/planguard/baseline/local/pk.maint.delete_old_packets.dml.custom.json @@ -0,0 +1,18 @@ +[ + "Aggregate partial=Simple", + " Limit [InitPlan]", + " LockRows", + " Incremental Sort", + " Index Scan on packets vp using idx_packets_last_heard scan=Backward", + " ModifyTable on packets dp [InitPlan]", + " Nested Loop", + " CTE Scan", + " Index Scan on packets dp using packets_pkey scan=Forward", + " ModifyTable on analytics_raw_state [InitPlan]", + " Aggregate partial=Simple [InitPlan]", + " CTE Scan", + " CTE Scan [InitPlan]", + " Result", + " Seq Scan on analytics_raw_state", + " CTE Scan" +] diff --git a/db/planguard/baseline/local/pk.maint.delete_old_packets.victims.custom.json b/db/planguard/baseline/local/pk.maint.delete_old_packets.victims.custom.json new file mode 100644 index 0000000..f38b4d4 --- /dev/null +++ b/db/planguard/baseline/local/pk.maint.delete_old_packets.victims.custom.json @@ -0,0 +1,5 @@ +[ + "Limit", + " Incremental Sort", + " Index Scan on packets vp using idx_packets_last_heard scan=Backward" +] diff --git a/db/planguard/baseline/local/pk.s_all.custom.json b/db/planguard/baseline/local/pk.s_all.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_all.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_all_pt14.custom.json b/db/planguard/baseline/local/pk.s_all_pt14.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_all_pt14.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_all_pt4.custom.json b/db/planguard/baseline/local/pk.s_all_pt4.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_all_pt4.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_all_pt9.custom.json b/db/planguard/baseline/local/pk.s_all_pt9.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_all_pt9.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_all_rt3.custom.json b/db/planguard/baseline/local/pk.s_all_rt3.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_all_rt3.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_allbogus6491.custom.json b/db/planguard/baseline/local/pk.s_allbogus6491.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_allbogus6491.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_big0.custom.json b/db/planguard/baseline/local/pk.s_big0.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_big0.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_big0_pt12rt2.custom.json b/db/planguard/baseline/local/pk.s_big0_pt12rt2.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_big0_pt12rt2.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_big0_pt14.custom.json b/db/planguard/baseline/local/pk.s_big0_pt14.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_big0_pt14.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_big1.custom.json b/db/planguard/baseline/local/pk.s_big1.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_big1.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_bogus5000.custom.json b/db/planguard/baseline/local/pk.s_bogus5000.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_bogus5000.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_bogus5000_pt4.custom.json b/db/planguard/baseline/local/pk.s_bogus5000_pt4.custom.json new file mode 100644 index 0000000..639e479 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_bogus5000_pt4.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_case.custom.json b/db/planguard/baseline/local/pk.s_case.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_case.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_dash.custom.json b/db/planguard/baseline/local/pk.s_dash.custom.json new file mode 100644 index 0000000..645da76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_dash.custom.json @@ -0,0 +1,22 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Bitmap Heap Scan on transport_scopes s", + " Bitmap Index Scan using transport_scopes_name_key", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_dash_pt14.custom.json b/db/planguard/baseline/local/pk.s_dash_pt14.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_dash_pt14.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_dash_pt4.custom.json b/db/planguard/baseline/local/pk.s_dash_pt4.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_dash_pt4.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_dash_pt4_cur.custom.json b/db/planguard/baseline/local/pk.s_dash_pt4_cur.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_dash_pt4_cur.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_dash_rt1.custom.json b/db/planguard/baseline/local/pk.s_dash_rt1.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_dash_rt1.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_dups.custom.json b/db/planguard/baseline/local/pk.s_dups.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_dups.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_emptyslice.auto.json b/db/planguard/baseline/local/pk.s_emptyslice.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_emptyslice.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_emptyslice.custom.json b/db/planguard/baseline/local/pk.s_emptyslice.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_emptyslice.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_emptyslice.generic.json b/db/planguard/baseline/local/pk.s_emptyslice.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_emptyslice.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_grr.custom.json b/db/planguard/baseline/local/pk.s_grr.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_grr.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_nl.custom.json b/db/planguard/baseline/local/pk.s_nl.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_nl.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_nl_deep200.custom.json b/db/planguard/baseline/local/pk.s_nl_deep200.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_nl_deep200.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_nl_l1_tie.custom.json b/db/planguard/baseline/local/pk.s_nl_l1_tie.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_nl_l1_tie.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_nl_pt14.custom.json b/db/planguard/baseline/local/pk.s_nl_pt14.custom.json new file mode 100644 index 0000000..d134b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_nl_pt14.custom.json @@ -0,0 +1,20 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_nl_pt4rt1.custom.json b/db/planguard/baseline/local/pk.s_nl_pt4rt1.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_nl_pt4rt1.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_nl_rt3.custom.json b/db/planguard/baseline/local/pk.s_nl_rt3.custom.json new file mode 100644 index 0000000..d134b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_nl_rt3.custom.json @@ -0,0 +1,20 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_unknown.custom.json b/db/planguard/baseline/local/pk.s_unknown.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_unknown.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.s_ws.custom.json b/db/planguard/baseline/local/pk.s_ws.custom.json new file mode 100644 index 0000000..43d5b76 --- /dev/null +++ b/db/planguard/baseline/local/pk.s_ws.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_all16_rt.auto.json b/db/planguard/baseline/local/pk.t_all16_rt.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_all16_rt.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_all16_rt.custom.json b/db/planguard/baseline/local/pk.t_all16_rt.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_all16_rt.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_all16_rt.generic.json b/db/planguard/baseline/local/pk.t_all16_rt.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_all16_rt.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_dupmix.auto.json b/db/planguard/baseline/local/pk.t_dupmix.auto.json new file mode 100644 index 0000000..3ad4285 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_dupmix.auto.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_dupmix.custom.json b/db/planguard/baseline/local/pk.t_dupmix.custom.json new file mode 100644 index 0000000..3ad4285 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_dupmix.custom.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_dupmix.generic.json b/db/planguard/baseline/local/pk.t_dupmix.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_dupmix.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt054.auto.json b/db/planguard/baseline/local/pk.t_pt054.auto.json new file mode 100644 index 0000000..3ad4285 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt054.auto.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt054.custom.json b/db/planguard/baseline/local/pk.t_pt054.custom.json new file mode 100644 index 0000000..3ad4285 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt054.custom.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt054.generic.json b/db/planguard/baseline/local/pk.t_pt054.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt054.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt12_rt2.auto.json b/db/planguard/baseline/local/pk.t_pt12_rt2.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt12_rt2.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt12_rt2.custom.json b/db/planguard/baseline/local/pk.t_pt12_rt2.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt12_rt2.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt12_rt2.generic.json b/db/planguard/baseline/local/pk.t_pt12_rt2.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt12_rt2.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt13_l200.auto.json b/db/planguard/baseline/local/pk.t_pt13_l200.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt13_l200.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt13_l200.custom.json b/db/planguard/baseline/local/pk.t_pt13_l200.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt13_l200.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt13_l200.generic.json b/db/planguard/baseline/local/pk.t_pt13_l200.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt13_l200.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt14.auto.json b/db/planguard/baseline/local/pk.t_pt14.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt14.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt14.custom.json b/db/planguard/baseline/local/pk.t_pt14.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt14.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt14.generic.json b/db/planguard/baseline/local/pk.t_pt14.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt14.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt2_rt23.auto.json b/db/planguard/baseline/local/pk.t_pt2_rt23.auto.json new file mode 100644 index 0000000..58d52ac --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt2_rt23.auto.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt2_rt23.custom.json b/db/planguard/baseline/local/pk.t_pt2_rt23.custom.json new file mode 100644 index 0000000..58d52ac --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt2_rt23.custom.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt2_rt23.generic.json b/db/planguard/baseline/local/pk.t_pt2_rt23.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt2_rt23.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt4.auto.json b/db/planguard/baseline/local/pk.t_pt4.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt4.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt4.custom.json b/db/planguard/baseline/local/pk.t_pt4.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt4.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt4.generic.json b/db/planguard/baseline/local/pk.t_pt4.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt4.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt44.auto.json b/db/planguard/baseline/local/pk.t_pt44.auto.json new file mode 100644 index 0000000..3ad4285 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt44.auto.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt44.custom.json b/db/planguard/baseline/local/pk.t_pt44.custom.json new file mode 100644 index 0000000..3ad4285 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt44.custom.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt44.generic.json b/db/planguard/baseline/local/pk.t_pt44.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt44.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt4_pc_deep.auto.json b/db/planguard/baseline/local/pk.t_pt4_pc_deep.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt4_pc_deep.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt4_pc_deep.custom.json b/db/planguard/baseline/local/pk.t_pt4_pc_deep.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt4_pc_deep.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt4_pc_deep.generic.json b/db/planguard/baseline/local/pk.t_pt4_pc_deep.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt4_pc_deep.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt5_ustie_l1.auto.json b/db/planguard/baseline/local/pk.t_pt5_ustie_l1.auto.json new file mode 100644 index 0000000..58d52ac --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt5_ustie_l1.auto.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt5_ustie_l1.custom.json b/db/planguard/baseline/local/pk.t_pt5_ustie_l1.custom.json new file mode 100644 index 0000000..58d52ac --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt5_ustie_l1.custom.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt5_ustie_l1.generic.json b/db/planguard/baseline/local/pk.t_pt5_ustie_l1.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt5_ustie_l1.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt5_ustie_l2.auto.json b/db/planguard/baseline/local/pk.t_pt5_ustie_l2.auto.json new file mode 100644 index 0000000..58d52ac --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt5_ustie_l2.auto.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt5_ustie_l2.custom.json b/db/planguard/baseline/local/pk.t_pt5_ustie_l2.custom.json new file mode 100644 index 0000000..58d52ac --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt5_ustie_l2.custom.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt5_ustie_l2.generic.json b/db/planguard/baseline/local/pk.t_pt5_ustie_l2.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt5_ustie_l2.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt99_4.auto.json b/db/planguard/baseline/local/pk.t_pt99_4.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt99_4.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt99_4.custom.json b/db/planguard/baseline/local/pk.t_pt99_4.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt99_4.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_pt99_4.generic.json b/db/planguard/baseline/local/pk.t_pt99_4.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_pt99_4.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_ptempty.auto.json b/db/planguard/baseline/local/pk.t_ptempty.auto.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_ptempty.auto.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_ptempty.custom.json b/db/planguard/baseline/local/pk.t_ptempty.custom.json new file mode 100644 index 0000000..6497038 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_ptempty.custom.json @@ -0,0 +1,17 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_ptempty.generic.json b/db/planguard/baseline/local/pk.t_ptempty.generic.json new file mode 100644 index 0000000..895e392 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_ptempty.generic.json @@ -0,0 +1,18 @@ +[ + "Sort", + " Limit [InitPlan]", + " Index Only Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_ptempty_rt3.auto.json b/db/planguard/baseline/local/pk.t_ptempty_rt3.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_ptempty_rt3.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_ptempty_rt3.custom.json b/db/planguard/baseline/local/pk.t_ptempty_rt3.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_ptempty_rt3.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_ptempty_rt3.generic.json b/db/planguard/baseline/local/pk.t_ptempty_rt3.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_ptempty_rt3.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_rt00.auto.json b/db/planguard/baseline/local/pk.t_rt00.auto.json new file mode 100644 index 0000000..3ad4285 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_rt00.auto.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_rt00.custom.json b/db/planguard/baseline/local/pk.t_rt00.custom.json new file mode 100644 index 0000000..3ad4285 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_rt00.custom.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Unique", + " Sort", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_rt00.generic.json b/db/planguard/baseline/local/pk.t_rt00.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_rt00.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_rt37.auto.json b/db/planguard/baseline/local/pk.t_rt37.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_rt37.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_rt37.custom.json b/db/planguard/baseline/local/pk.t_rt37.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_rt37.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_rt37.generic.json b/db/planguard/baseline/local/pk.t_rt37.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_rt37.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_rt3_l200.auto.json b/db/planguard/baseline/local/pk.t_rt3_l200.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_rt3_l200.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_rt3_l200.custom.json b/db/planguard/baseline/local/pk.t_rt3_l200.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_rt3_l200.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_rt3_l200.generic.json b/db/planguard/baseline/local/pk.t_rt3_l200.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_rt3_l200.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_web13.auto.json b/db/planguard/baseline/local/pk.t_web13.auto.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_web13.auto.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_web13.custom.json b/db/planguard/baseline/local/pk.t_web13.custom.json new file mode 100644 index 0000000..da70253 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_web13.custom.json @@ -0,0 +1,27 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.t_web13.generic.json b/db/planguard/baseline/local/pk.t_web13.generic.json new file mode 100644 index 0000000..e6986f4 --- /dev/null +++ b/db/planguard/baseline/local/pk.t_web13.generic.json @@ -0,0 +1,28 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Nested Loop", + " Aggregate partial=Simple {Hashed}", + " ProjectSet", + " Result", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Memoize", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_epoch.custom.json b/db/planguard/baseline/local/pk.w_epoch.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.w_epoch.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_eq.custom.json b/db/planguard/baseline/local/pk.w_eq.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.w_eq.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_long.custom.json b/db/planguard/baseline/local/pk.w_long.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.w_long.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_negsince.custom.json b/db/planguard/baseline/local/pk.w_negsince.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.w_negsince.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_pf_32m.custom.json b/db/planguard/baseline/local/pk.w_pf_32m.custom.json new file mode 100644 index 0000000..c28a95f --- /dev/null +++ b/db/planguard/baseline/local/pk.w_pf_32m.custom.json @@ -0,0 +1,22 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Sort", + " Index Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_pf_32m_real.custom.json b/db/planguard/baseline/local/pk.w_pf_32m_real.custom.json new file mode 100644 index 0000000..c28a95f --- /dev/null +++ b/db/planguard/baseline/local/pk.w_pf_32m_real.custom.json @@ -0,0 +1,22 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Sort", + " Index Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_pf_5m.custom.json b/db/planguard/baseline/local/pk.w_pf_5m.custom.json new file mode 100644 index 0000000..d725c92 --- /dev/null +++ b/db/planguard/baseline/local/pk.w_pf_5m.custom.json @@ -0,0 +1,22 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Append", + " Limit", + " Sort", + " Index Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_pf_5m_b.custom.json b/db/planguard/baseline/local/pk.w_pf_5m_b.custom.json new file mode 100644 index 0000000..26fbcbe --- /dev/null +++ b/db/planguard/baseline/local/pk.w_pf_5m_b.custom.json @@ -0,0 +1,23 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Sort", + " Bitmap Heap Scan on packets p_1", + " Bitmap Index Scan using idx_packets_type_last_heard", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_pf_6h.custom.json b/db/planguard/baseline/local/pk.w_pf_6h.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.w_pf_6h.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_pf_6h_2ndlast.custom.json b/db/planguard/baseline/local/pk.w_pf_6h_2ndlast.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.w_pf_6h_2ndlast.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_pf_6h_cur.custom.json b/db/planguard/baseline/local/pk.w_pf_6h_cur.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.w_pf_6h_cur.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_pf_6h_last.custom.json b/db/planguard/baseline/local/pk.w_pf_6h_last.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.w_pf_6h_last.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_pt14_3d.custom.json b/db/planguard/baseline/local/pk.w_pt14_3d.custom.json new file mode 100644 index 0000000..c28a95f --- /dev/null +++ b/db/planguard/baseline/local/pk.w_pt14_3d.custom.json @@ -0,0 +1,22 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Sort", + " Index Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_pt4rt1_tied.custom.json b/db/planguard/baseline/local/pk.w_pt4rt1_tied.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.w_pt4rt1_tied.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_since75.custom.json b/db/planguard/baseline/local/pk.w_since75.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.w_since75.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_since75_pt4.custom.json b/db/planguard/baseline/local/pk.w_since75_pt4.custom.json new file mode 100644 index 0000000..c28a95f --- /dev/null +++ b/db/planguard/baseline/local/pk.w_since75_pt4.custom.json @@ -0,0 +1,22 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Sort", + " Index Scan on packets p_1 using idx_packets_type_last_heard scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_until_deep.custom.json b/db/planguard/baseline/local/pk.w_until_deep.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.w_until_deep.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.w_until_only_rt0.custom.json b/db/planguard/baseline/local/pk.w_until_only_rt0.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.w_until_only_rt0.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.ws_SEA_4d.custom.json b/db/planguard/baseline/local/pk.ws_SEA_4d.custom.json new file mode 100644 index 0000000..655e9a1 --- /dev/null +++ b/db/planguard/baseline/local/pk.ws_SEA_4d.custom.json @@ -0,0 +1,24 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Append", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Index Scan on transport_scopes using transport_scopes_name_key scan=Forward [InitPlan]", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.ws_all_1d.custom.json b/db/planguard/baseline/local/pk.ws_all_1d.custom.json new file mode 100644 index 0000000..e38ac95 --- /dev/null +++ b/db/planguard/baseline/local/pk.ws_all_1d.custom.json @@ -0,0 +1,24 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Append", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Seq Scan on transport_scopes [InitPlan]", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.ws_all_until_deep.custom.json b/db/planguard/baseline/local/pk.ws_all_until_deep.custom.json new file mode 100644 index 0000000..e38ac95 --- /dev/null +++ b/db/planguard/baseline/local/pk.ws_all_until_deep.custom.json @@ -0,0 +1,24 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Append", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Seq Scan on transport_scopes [InitPlan]", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.ws_bogus5000_1d.custom.json b/db/planguard/baseline/local/pk.ws_bogus5000_1d.custom.json new file mode 100644 index 0000000..e38ac95 --- /dev/null +++ b/db/planguard/baseline/local/pk.ws_bogus5000_1d.custom.json @@ -0,0 +1,24 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Append", + " Nested Loop", + " Seq Scan on transport_scopes s", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Seq Scan on transport_scopes [InitPlan]", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.ws_chain_dash.custom.json b/db/planguard/baseline/local/pk.ws_chain_dash.custom.json new file mode 100644 index 0000000..77721ac --- /dev/null +++ b/db/planguard/baseline/local/pk.ws_chain_dash.custom.json @@ -0,0 +1,26 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Append", + " Nested Loop", + " Bitmap Heap Scan on transport_scopes s", + " Bitmap Index Scan using transport_scopes_name_key", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Bitmap Heap Scan on transport_scopes [InitPlan]", + " Bitmap Index Scan using transport_scopes_name_key", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.ws_chain_nl_1h.custom.json b/db/planguard/baseline/local/pk.ws_chain_nl_1h.custom.json new file mode 100644 index 0000000..655e9a1 --- /dev/null +++ b/db/planguard/baseline/local/pk.ws_chain_nl_1h.custom.json @@ -0,0 +1,24 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Append", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Index Scan on transport_scopes using transport_scopes_name_key scan=Forward [InitPlan]", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.ws_chain_sea4d.custom.json b/db/planguard/baseline/local/pk.ws_chain_sea4d.custom.json new file mode 100644 index 0000000..655e9a1 --- /dev/null +++ b/db/planguard/baseline/local/pk.ws_chain_sea4d.custom.json @@ -0,0 +1,24 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Append", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Index Scan on transport_scopes using transport_scopes_name_key scan=Forward [InitPlan]", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.ws_dash_pt4_cur.custom.json b/db/planguard/baseline/local/pk.ws_dash_pt4_cur.custom.json new file mode 100644 index 0000000..77721ac --- /dev/null +++ b/db/planguard/baseline/local/pk.ws_dash_pt4_cur.custom.json @@ -0,0 +1,26 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Append", + " Nested Loop", + " Bitmap Heap Scan on transport_scopes s", + " Bitmap Index Scan using transport_scopes_name_key", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Bitmap Heap Scan on transport_scopes [InitPlan]", + " Bitmap Index Scan using transport_scopes_name_key", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.ws_emptyslice.custom.json b/db/planguard/baseline/local/pk.ws_emptyslice.custom.json new file mode 100644 index 0000000..db792ce --- /dev/null +++ b/db/planguard/baseline/local/pk.ws_emptyslice.custom.json @@ -0,0 +1,21 @@ +[ + "Sort", + " Limit [InitPlan]", + " Merge Append", + " Limit", + " Index Scan on packets p_1 using idx_packets_last_heard_hash scan=Forward", + " Sort", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.ws_nl_pt4.custom.json b/db/planguard/baseline/local/pk.ws_nl_pt4.custom.json new file mode 100644 index 0000000..655e9a1 --- /dev/null +++ b/db/planguard/baseline/local/pk.ws_nl_pt4.custom.json @@ -0,0 +1,24 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Append", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Index Scan on transport_scopes using transport_scopes_name_key scan=Forward [InitPlan]", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.ws_sea_1d.custom.json b/db/planguard/baseline/local/pk.ws_sea_1d.custom.json new file mode 100644 index 0000000..655e9a1 --- /dev/null +++ b/db/planguard/baseline/local/pk.ws_sea_1d.custom.json @@ -0,0 +1,24 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Append", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Index Scan on transport_scopes using transport_scopes_name_key scan=Forward [InitPlan]", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/pk.ws_unknown.custom.json b/db/planguard/baseline/local/pk.ws_unknown.custom.json new file mode 100644 index 0000000..655e9a1 --- /dev/null +++ b/db/planguard/baseline/local/pk.ws_unknown.custom.json @@ -0,0 +1,24 @@ +[ + "Sort", + " Limit [InitPlan]", + " Sort", + " Append", + " Nested Loop", + " Index Scan on transport_scopes s using transport_scopes_name_key scan=Forward", + " Limit", + " Index Only Scan on packets p_1 using idx_packets_scope_last_heard scan=Forward", + " Index Only Scan on packets p_2 using idx_packets_heard_inverted scan=Forward", + " Index Scan on transport_scopes using transport_scopes_name_key scan=Forward [InitPlan]", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop (Left)", + " Nested Loop", + " CTE Scan", + " Index Scan on packets p using packets_pkey scan=Forward", + " Limit", + " Sort", + " Index Scan on packet_observations using idx_observations_packet scan=Forward", + " Memoize", + " Index Scan on observers o using observers_pkey scan=Forward", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.SEA.custom.json b/db/planguard/baseline/local/sm.nodes.SEA.custom.json new file mode 100644 index 0000000..4dc9766 --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.SEA.custom.json @@ -0,0 +1,19 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Bitmap Heap Scan on node_iatas [SubPlan]", + " Bitmap Index Scan using idx_node_iatas_iata", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.SEA_type2.custom.json b/db/planguard/baseline/local/sm.nodes.SEA_type2.custom.json new file mode 100644 index 0000000..4dc9766 --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.SEA_type2.custom.json @@ -0,0 +1,19 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Bitmap Heap Scan on node_iatas [SubPlan]", + " Bitmap Index Scan using idx_node_iatas_iata", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.bogus5000.custom.json b/db/planguard/baseline/local/sm.nodes.bogus5000.custom.json new file mode 100644 index 0000000..e588984 --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.bogus5000.custom.json @@ -0,0 +1,18 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Seq Scan on node_iatas [SubPlan]", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.first_l201_neighbors_SEA.custom.json b/db/planguard/baseline/local/sm.nodes.first_l201_neighbors_SEA.custom.json new file mode 100644 index 0000000..4dc9766 --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.first_l201_neighbors_SEA.custom.json @@ -0,0 +1,19 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Bitmap Heap Scan on node_iatas [SubPlan]", + " Bitmap Index Scan using idx_node_iatas_iata", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.first_l501.custom.json b/db/planguard/baseline/local/sm.nodes.first_l501.custom.json new file mode 100644 index 0000000..1882e4c --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.first_l501.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.global.custom.json b/db/planguard/baseline/local/sm.nodes.global.custom.json new file mode 100644 index 0000000..1882e4c --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.global.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.global_deep_cursor200.custom.json b/db/planguard/baseline/local/sm.nodes.global_deep_cursor200.custom.json new file mode 100644 index 0000000..1882e4c --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.global_deep_cursor200.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.legacy_cursor_l501.custom.json b/db/planguard/baseline/local/sm.nodes.legacy_cursor_l501.custom.json new file mode 100644 index 0000000..1882e4c --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.legacy_cursor_l501.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.multibyte_true.custom.json b/db/planguard/baseline/local/sm.nodes.multibyte_true.custom.json new file mode 100644 index 0000000..1882e4c --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.multibyte_true.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.name_a.custom.json b/db/planguard/baseline/local/sm.nodes.name_a.custom.json new file mode 100644 index 0000000..1882e4c --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.name_a.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.pagecursor_l501.custom.json b/db/planguard/baseline/local/sm.nodes.pagecursor_l501.custom.json new file mode 100644 index 0000000..1882e4c --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.pagecursor_l501.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.pubkey_prefix.custom.json b/db/planguard/baseline/local/sm.nodes.pubkey_prefix.custom.json new file mode 100644 index 0000000..1882e4c --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.pubkey_prefix.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.traces_true.custom.json b/db/planguard/baseline/local/sm.nodes.traces_true.custom.json new file mode 100644 index 0000000..1882e4c --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.traces_true.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.type1.custom.json b/db/planguard/baseline/local/sm.nodes.type1.custom.json new file mode 100644 index 0000000..1882e4c --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.type1.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.type2.custom.json b/db/planguard/baseline/local/sm.nodes.type2.custom.json new file mode 100644 index 0000000..1882e4c --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.type2.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.type2_deep_cursor.custom.json b/db/planguard/baseline/local/sm.nodes.type2_deep_cursor.custom.json new file mode 100644 index 0000000..1882e4c --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.type2_deep_cursor.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.type3.custom.json b/db/planguard/baseline/local/sm.nodes.type3.custom.json new file mode 100644 index 0000000..1882e4c --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.type3.custom.json @@ -0,0 +1,17 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Hash Join (Left)", + " Seq Scan on nodes n_1", + " Hash", + " Seq Scan on transport_scopes ts", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.nodes.type9_empty.custom.json b/db/planguard/baseline/local/sm.nodes.type9_empty.custom.json new file mode 100644 index 0000000..95d6d86 --- /dev/null +++ b/db/planguard/baseline/local/sm.nodes.type9_empty.custom.json @@ -0,0 +1,16 @@ +[ + "Subquery Scan", + " Limit [Subquery]", + " Sort", + " Nested Loop (Left)", + " Seq Scan on nodes n_1", + " Index Scan on transport_scopes ts using transport_scopes_pkey scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Sort", + " Index Scan on node_iatas ni using node_iatas_pkey scan=Forward", + " Index Only Scan on observers o using observers_public_key_key scan=Forward [SubPlan]", + " Limit [SubPlan]", + " Index Scan on observers o_1 using observers_public_key_key scan=Forward", + " Aggregate partial=Simple [SubPlan]", + " Index Only Scan on node_neighbors nn using node_neighbors_pkey scan=Forward" +] diff --git a/db/planguard/baseline/local/sm.obsv.first_l201.custom.json b/db/planguard/baseline/local/sm.obsv.first_l201.custom.json new file mode 100644 index 0000000..26b2608 --- /dev/null +++ b/db/planguard/baseline/local/sm.obsv.first_l201.custom.json @@ -0,0 +1,16 @@ +[ + "Limit", + " Sort", + " Aggregate partial=Simple {Sorted}", + " Sort", + " Hash Join (Left)", + " Hash Join (Right)", + " Seq Scan on observer_scopes os", + " Hash", + " Hash Join (Right)", + " Seq Scan on observer_brokers ob", + " Hash", + " Seq Scan on observers o", + " Hash", + " Seq Scan on transport_scopes ts" +] diff --git a/db/planguard/baseline/local/sm.obsv.legacy_l201.custom.json b/db/planguard/baseline/local/sm.obsv.legacy_l201.custom.json new file mode 100644 index 0000000..26b2608 --- /dev/null +++ b/db/planguard/baseline/local/sm.obsv.legacy_l201.custom.json @@ -0,0 +1,16 @@ +[ + "Limit", + " Sort", + " Aggregate partial=Simple {Sorted}", + " Sort", + " Hash Join (Left)", + " Hash Join (Right)", + " Seq Scan on observer_scopes os", + " Hash", + " Hash Join (Right)", + " Seq Scan on observer_brokers ob", + " Hash", + " Seq Scan on observers o", + " Hash", + " Seq Scan on transport_scopes ts" +] diff --git a/db/planguard/corpus.yaml b/db/planguard/corpus.yaml index 7dc44d7..ab5f2ab 100644 --- a/db/planguard/corpus.yaml +++ b/db/planguard/corpus.yaml @@ -50,8 +50,10 @@ tiers: prod: {} # no search_path: the maint pool's own groups: - # Pending statements plan against an overlay schema on the local copy, which a later change builds. - packets_global: {search_path: {local: "t_s2b, public"}} + # These groups plan against an overlay schema on the local copy: packets_global against overlay/packets_global.sql + # (packets with its new indexes; nodes and observers as migrated), the pending groups against one a later change + # builds. + packets_global: {search_path: {local: "t_s5a, public"}} packet_sites: {search_path: {local: "t_s2b, public"}} channel_message_iatas: {search_path: {local: "t_s2b, public"}} stats_adverts: {search_path: {local: "t_s2b, public"}} @@ -77,6 +79,15 @@ statements: UpsertNodeIATA: {sqlc: UpsertNodeIATA, kind: dml, plan: ingest} TouchChannel: {sqlc: TouchChannel, kind: dml, plan: ingest} TouchChannelHashOnly: {sqlc: TouchChannelHashOnly, kind: dml, plan: ingest} + # Global packet pages, one statement per filter shape, and the node and observer keysets. + ListPacketsPage: {sqlc: ListPacketsPage, group: packets_global, plan: cached, admission: global_page} + ListPacketsByTypes: {sqlc: ListPacketsByTypes, group: packets_global, plan: cached, admission: global_page} + ListPacketsByScopes: {sqlc: ListPacketsByScopes, group: packets_global, plan: custom} + ListPacketsByScopesTypes: {sqlc: ListPacketsByScopesTypes, group: packets_global, plan: custom} + ListPacketsWindow: {sqlc: ListPacketsWindow, group: packets_global, plan: custom} + ListPacketsWindowScoped: {sqlc: ListPacketsWindowScoped, group: packets_global, plan: custom} + ListNodes: {sqlc: ListNodes, group: packets_global, plan: custom} + ListObservers: {sqlc: ListObservers, group: packets_global, plan: custom} # Read-only parts of maint DML, cut from today's sqlc text (lock clause removed); a drift test keeps them in step. DeleteExpiredRoutesSelect: {file: sql/derived/DeleteExpiredRoutesSelect.sql, derived_from: DeleteExpiredRoutes, plan: maint} DeleteGraceRoutesSliceCand: {file: sql/derived/DeleteGraceRoutesSliceCand.sql, derived_from: DeleteGraceRoutesSlice, plan: maint} @@ -97,14 +108,6 @@ statements: OracleBySites: {file: sql/oracle/OracleBySites.sql, plan: harness} # Not merged yet: frozen end-state texts. They SKIP until their feature lands, which switches them to sqlc:, # drops pending and commits their baselines in the same diff. - ListPacketsPage: {file: sql/pending/ListPacketsPage.sql, group: packets_global, plan: cached, admission: global_page, pending: packets_global} - ListPacketsByTypes: {file: sql/pending/ListPacketsByTypes.sql, group: packets_global, plan: cached, admission: global_page, pending: packets_global} - ListPacketsByScopes: {file: sql/pending/ListPacketsByScopes.sql, group: packets_global, plan: custom, pending: packets_global} - ListPacketsByScopesTypes: {file: sql/pending/ListPacketsByScopesTypes.sql, group: packets_global, plan: custom, pending: packets_global} - ListPacketsWindow: {file: sql/pending/ListPacketsWindow.sql, group: packets_global, plan: custom, pending: packets_global} - ListPacketsWindowScoped: {file: sql/pending/ListPacketsWindowScoped.sql, group: packets_global, plan: custom, pending: packets_global} - ListNodes_end: {file: sql/pending/ListNodes.sql, group: packets_global, plan: custom, pending: packets_global} - ListObservers_end: {file: sql/pending/ListObservers.sql, group: packets_global, plan: custom, pending: packets_global} ListPacketsBySite: {file: sql/pending/ListPacketsBySite.sql, group: packet_sites, plan: cached, admission: region_page, pending: packet_sites} ListPacketsBySites: {file: sql/pending/ListPacketsBySites.sql, group: packet_sites, plan: cached, admission: region_list, pending: packet_sites} ListPacketsBySitesFirst: {file: sql/pending/ListPacketsBySitesFirst.sql, group: packet_sites, plan: cached, admission: region_list, pending: packet_sites} @@ -2089,91 +2092,91 @@ entries: # ---- small tables: nodes, observers, channels, node types, clock drift ---- - id: sm.nodes.global - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes global" params: [{raw: "0"}, {raw: "NULL::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'infinity'::timestamptz"}, {raw: "50"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 3030, planning_ms: 10} note: "measured: 2020 / 40650 B (custom / generic); budget: 1.5x measured" - id: sm.nodes.type2 - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes type2" params: [{raw: "2"}, {raw: "NULL::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'infinity'::timestamptz"}, {raw: "50"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 3132, planning_ms: 10} note: "measured: 2088 / 29731 B (custom / generic); budget: 1.5x measured" - id: sm.nodes.type1 - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes type1" params: [{raw: "1"}, {raw: "NULL::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'infinity'::timestamptz"}, {raw: "50"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 2874, planning_ms: 10} note: "measured: 1916 / 11712 B (custom / generic); budget: 1.5x measured" - id: sm.nodes.type3 - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes type3" params: [{raw: "3"}, {raw: "NULL::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'infinity'::timestamptz"}, {raw: "50"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 3446, planning_ms: 10} note: "measured: 2297 / 3676 B (custom / generic); budget: 1.5x measured" - id: sm.nodes.type2_deep_cursor - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes type2_deep_cursor" params: [{raw: "2"}, {raw: "NULL::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'2026-10-10 00:00+00'::timestamptz"}, {raw: "50"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 3209, planning_ms: 10} note: "measured: 2139 / 3734 B (custom / generic); budget: 1.5x measured" - id: sm.nodes.global_deep_cursor200 - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes global_deep_cursor200" params: [{raw: "0"}, {raw: "NULL::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'2026-10-10 00:00+00'::timestamptz"}, {raw: "200"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 5829, planning_ms: 10} note: "measured: 3886 / 8415 B (custom / generic); budget: 1.5x measured" - id: sm.nodes.SEA - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes SEA" params: [{raw: "0"}, {raw: "'{SEA}'::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'infinity'::timestamptz"}, {raw: "50"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 8009, planning_ms: 10} note: "measured: 5339 / 9253 B (custom / generic); budget: 1.5x measured" - id: sm.nodes.SEA_type2 - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes SEA_type2" params: [{raw: "2"}, {raw: "'{SEA}'::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'infinity'::timestamptz"}, {raw: "50"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 7974, planning_ms: 10} note: "measured: 5316 / 9017 B (custom / generic); budget: 1.5x measured" - id: sm.nodes.name_a - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes name_a" params: [{raw: "0"}, {raw: "NULL::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "'a'"}, {raw: "'infinity'::timestamptz"}, {raw: "50"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 2876, planning_ms: 10} note: "measured: 1917 / 23166 B (custom / generic); budget: 1.5x measured" - id: sm.nodes.multibyte_true - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes multibyte_true" params: [{raw: "0"}, {raw: "NULL::bpchar[]"}, {raw: "'true'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'infinity'::timestamptz"}, {raw: "50"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 3413, planning_ms: 10} note: "measured: 2275 / 28864 B (custom / generic); budget: 1.5x measured" - id: sm.nodes.traces_true - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes traces_true" params: [{raw: "0"}, {raw: "NULL::bpchar[]"}, {raw: "'any'"}, {raw: "'true'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'infinity'::timestamptz"}, {raw: "50"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 3728, planning_ms: 10} note: "measured: 2485 / 19023 B (custom / generic); budget: 1.5x measured" - id: sm.nodes.pubkey_prefix - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes pubkey_prefix" params: [{raw: "0"}, {raw: "NULL::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'infinity'::timestamptz"}, {raw: "50"}, {raw: "''"}, {raw: "false"}, {raw: "'1a'"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 3458, planning_ms: 10} note: "measured: 2305 / 2490 B (custom / generic); budget: 1.5x measured" - id: sm.nodes.type9_empty - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes type9_empty" params: [{raw: "9"}, {raw: "NULL::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'infinity'::timestamptz"}, {raw: "50"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: @@ -2243,42 +2246,42 @@ entries: budget: {buffers: 50, planning_ms: 10} note: "measured: 7 / 7 B (custom / generic); budget: 1.5x measured" - id: sm.nodes.first_l501 - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes first_l501 (perf_small)" params: [{raw: "0"}, {raw: "NULL::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'infinity'::timestamptz"}, {raw: "501"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 13329, planning_ms: 10} note: "measured: 8886 / 48171 B (custom / generic); budget: 1.5x measured" - id: sm.nodes.first_l201_neighbors_SEA - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes first_l201_neighbors_SEA (perf_small)" params: [{raw: "0"}, {raw: "ARRAY['SEA']::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'infinity'::timestamptz"}, {raw: "201"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 15000, planning_ms: 10} note: "measured: - / - B (custom / generic); budget: class budget" - id: sm.nodes.legacy_cursor_l501 - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes legacy_cursor_l501 (perf_small)" params: [{raw: "0"}, {raw: "NULL::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'2026-10-18 12:00:00.123+00'::timestamptz"}, {raw: "501"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 15000, planning_ms: 10} note: "measured: - / - B (custom / generic); budget: class budget" - id: sm.nodes.pagecursor_l501 - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes, page cursor at the 501st node, limit 501" params: [{raw: "0"}, {raw: "NULL::bpchar[]"}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {r: nodes_p500_ts}, {raw: "501"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {r: nodes_p500_id}] expect: budget: {buffers: 14388, planning_ms: 10} note: "measured: 9592 / 48827 B (custom / generic); budget: 1.5x measured" - id: sm.obsv.first_l201 - stmt: ListObservers_end + stmt: ListObservers shape: "ListObservers first_l201 (perf_small)" params: [{raw: "NULL::bpchar[]"}, {raw: "''"}, {raw: "''"}, {raw: "''"}, {raw: "''"}, {raw: "'infinity'::timestamptz"}, {raw: "201"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: budget: {buffers: 1310, planning_ms: 10} note: "measured 873 B at fillfactor 50; budget 1.5x" - id: sm.obsv.legacy_l201 - stmt: ListObservers_end + stmt: ListObservers shape: "ListObservers legacy_l201 (perf_small)" params: [{raw: "NULL::bpchar[]"}, {raw: "''"}, {raw: "''"}, {raw: "''"}, {raw: "''"}, {raw: "'2026-10-18 17:28:26.053+00'::timestamptz"}, {raw: "201"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: @@ -2306,7 +2309,7 @@ entries: budget: {buffers: 191898, planning_ms: 10} note: "measured: 127932 B on the full local copy. The API pools run with parallel workers off by design, so the planner takes an Index Scan of the region rows (127,932 B, 46 ms) instead of a parallel bitmap scan (5,908 B, 23 ms); accepted" - id: sm.nodes.bogus5000 - stmt: ListNodes_end + stmt: ListNodes shape: "ListNodes, 5,000 bogus IATAs" params: [{raw: "0"}, {r: small_bogus5000}, {raw: "'any'"}, {raw: "'any'"}, {raw: "NULL::bytea"}, {raw: "''"}, {raw: "'infinity'::timestamptz"}, {raw: "51"}, {raw: "''"}, {raw: "false"}, {raw: "''"}, {raw: "'00000000-0000-0000-0000-000000000000'::uuid"}] expect: @@ -3429,9 +3432,11 @@ entries: expect: driver: [{relation: packets, index: packets_pkey, index_cond: ["packet_hash ="]}] - # ---- global packet pages (pending) ---- + # ---- global packet pages ---- - id: pk.e_at_tied stmt: ListPacketsPage + shapes: [tie] + tiers: [local] shape: "[pt - rt - scopes 0 since - until - cursor_ms 1792342797721 limit 50]" params: ["2026-10-18 16:59:57.721+00", "\\x", 51] expect: @@ -3440,6 +3445,7 @@ entries: note: "no measured case; page-CTE budget 50" - id: pk.e_c1 stmt: ListPacketsPage + shapes: [deep] shape: "cursor 1ms [pt - rt - scopes 0 since - until - cursor_ms 1 limit 50]" params: ["1970-01-01 00:00:00.001+00", "\\x", 51] expect: @@ -3448,6 +3454,7 @@ entries: note: "no measured case; page-CTE budget 50" - id: pk.e_c9999 stmt: ListPacketsPage + shapes: [page1] shape: "cursor year 9999 [pt - rt - scopes 0 since - until - cursor_ms 253402300799999 limit 50]" params: ["9999-12-31 23:59:59.999008+00", "\\x", 51] expect: @@ -3456,6 +3463,7 @@ entries: note: "no measured case; page-CTE budget 50" - id: pk.e_cneg stmt: ListPacketsPage + shapes: [page1] shape: "cursor<0 [pt - rt - scopes 0 since - until - cursor_ms -5 limit 50]" params: [infinity, "\\x", 51] expect: @@ -3464,6 +3472,8 @@ entries: note: "no measured case; page-CTE budget 50" - id: pk.e_deep stmt: ListPacketsPage + shapes: [deep] + tiers: [local] shape: "[pt - rt - scopes 0 since - until - cursor_ms 1792170000000 limit 50]" params: ["2026-10-16 17:00:00+00", "\\x", 51] expect: @@ -3472,6 +3482,8 @@ entries: note: "measured page CTE 4 / 4 B (custom / generic), access Index Only Scan:idx_packets_last_heard_hash; page-CTE budget 50" - id: pk.e_deep200 stmt: ListPacketsPage + shapes: [deep] + tiers: [local] shape: "[pt - rt - scopes 0 since - until - cursor_ms 1792170000000 limit 200]" params: ["2026-10-16 17:00:00+00", "\\x", 201] expect: @@ -3480,6 +3492,7 @@ entries: note: "no measured case; page-CTE budget 50" - id: pk.e_l1 stmt: ListPacketsPage + shapes: [page1] shape: "[pt - rt - scopes 0 since - until - cursor_ms 0 limit 1]" params: [infinity, "\\x", 2] expect: @@ -3488,6 +3501,7 @@ entries: note: "no measured case; page-CTE budget 50" - id: pk.e_l200 stmt: ListPacketsPage + shapes: [page1] shape: "[pt - rt - scopes 0 since - until - cursor_ms 0 limit 200]" params: [infinity, "\\x", 201] expect: @@ -3496,6 +3510,7 @@ entries: note: "measured page CTE 4 / 4 B (custom / generic), access Index Only Scan:idx_packets_last_heard_hash; page-CTE budget 50" - id: pk.e_l50 stmt: ListPacketsPage + shapes: [page1] shape: "[pt - rt - scopes 0 since - until - cursor_ms 0 limit 50]" params: [infinity, "\\x", 51] expect: @@ -3504,6 +3519,8 @@ entries: note: "measured page CTE 4 / 4 B (custom / generic), access Index Only Scan:idx_packets_last_heard_hash; page-CTE budget 50" - id: pk.e_tail stmt: ListPacketsPage + shapes: [deep] + tiers: [local] shape: "tail [pt - rt - scopes 0 since - until - cursor_ms 1792083600050 limit 50]" params: ["2026-10-15 17:00:00.05+00", "\\x", 51] expect: @@ -3512,6 +3529,8 @@ entries: note: "no measured case; page-CTE budget 50" - id: pk.e_tiedms_l1 stmt: ListPacketsPage + shapes: [tie] + tiers: [local] shape: "tied ms on top [pt - rt - scopes 0 since - until - cursor_ms 1792342797722 limit 1]" params: ["2026-10-18 16:59:57.722+00", "\\x", 2] expect: @@ -3520,6 +3539,8 @@ entries: note: "no measured case; page-CTE budget 50" - id: pk.e_tiedms_l2 stmt: ListPacketsPage + shapes: [tie] + tiers: [local] shape: "[pt - rt - scopes 0 since - until - cursor_ms 1792342797722 limit 2]" params: ["2026-10-18 16:59:57.722+00", "\\x", 3] expect: @@ -3528,6 +3549,8 @@ entries: note: "no measured case; page-CTE budget 50" - id: pk.e_ustie_l1 stmt: ListPacketsPage + shapes: [tie] + tiers: [local] shape: "us tie [pt - rt - scopes 0 since - until - cursor_ms 1792341411653 limit 1]" params: ["2026-10-18 16:36:51.653+00", "\\x", 2] expect: @@ -3536,6 +3559,8 @@ entries: note: "no measured case; page-CTE budget 50" - id: pk.e_ustie_l2 stmt: ListPacketsPage + shapes: [tie] + tiers: [local] shape: "[pt - rt - scopes 0 since - until - cursor_ms 1792341411653 limit 2]" params: ["2026-10-18 16:36:51.653+00", "\\x", 3] expect: @@ -3544,8 +3569,9 @@ entries: note: "no measured case; page-CTE budget 50" - id: pk.inv_all_5m stmt: ListPacketsWindow + tiers: [local] shape: "inverted pt11 rt2 [pt - rt - scopes 0 since 2026-10-18 10:19:12.796+00 until 2026-10-18 10:24:12.796+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", "2026-10-18 10:19:12.796+00", "2026-10-18 10:24:12.796+00"] + params: ["2026-10-18 10:19:12.796+00", infinity, "\\x", "2026-10-18 10:24:12.796+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -3554,8 +3580,9 @@ entries: note: "measured at server defaults page CTE 299732 / 299732 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 449598" - id: pk.inv_pt1_10m stmt: ListPacketsWindow + tiers: [local] shape: "inverted pt1 rt2 [pt {1} rt - scopes 0 since 2026-10-18 05:59:27.384+00 until 2026-10-18 06:09:27.384+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{1}", "{0,1,2,3}", "2026-10-18 05:59:27.384+00", "2026-10-18 06:09:27.384+00"] + params: ["2026-10-18 05:59:27.384+00", infinity, "\\x", "2026-10-18 06:09:27.384+00", "{1}", "{0,1,2,3}", 51] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -3564,8 +3591,9 @@ entries: note: "measured at server defaults page CTE 481667 / 481285 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 722501" - id: pk.inv_pt4rt2_1h stmt: ListPacketsWindow + tiers: [local] shape: "inverted pt4 rt2, deep [pt {4} rt {2} scopes 0 since 2026-10-16 20:36:52.971+00 until 2026-10-16 21:36:52.971+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{4}", "{2}", "2026-10-16 20:36:52.971+00", "2026-10-16 21:36:52.971+00"] + params: ["2026-10-16 20:36:52.971+00", infinity, "\\x", "2026-10-16 21:36:52.971+00", "{4}", "{2}", 51] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -3574,8 +3602,9 @@ entries: note: "measured at server defaults page CTE 1866811 / 1866811 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 2800217" - id: pk.inv_rt0_32m stmt: ListPacketsWindow + tiers: [local] shape: "inverted pt5 rt0 row k="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -3584,8 +3613,9 @@ entries: note: "measured at server defaults page CTE 1011876 / 950986 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 1517814" - id: pk.inv_rt0_cur stmt: ListPacketsWindow + tiers: [local] shape: "cursor = since: only the inverted row [pt - rt {0} scopes 0 since 2026-10-17 17:23:53.466+00 until 2026-10-17 17:55:53.466+00 cursor_ms 1792257833466 limit 200]" - params: ["2026-10-17 17:23:53.466+00", "\\x", 201, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", "2026-10-17 17:23:53.466+00", "2026-10-17 17:55:53.466+00"] + params: ["2026-10-17 17:23:53.466+00", "2026-10-17 17:23:53.466+00", "\\x", "2026-10-17 17:55:53.466+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", 201] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -3594,8 +3624,9 @@ entries: note: "measured at server defaults page CTE 5 / 5 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 50" - id: pk.inv_scoped_none stmt: ListPacketsWindowScoped + tiers: [local] shape: "scoped window at an inverted ms (row has no scope) [pt - rt - scopes 1 since 2026-10-17 06:10:14.61+00 until 2026-10-17 07:10:14.61+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nl}'::text[]) v"}, "2026-10-17 06:10:14.61+00", "2026-10-17 07:10:14.61+00"] + params: ["2026-10-17 06:10:14.61+00", infinity, "\\x", "2026-10-17 07:10:14.61+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nl}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id ="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -3604,8 +3635,9 @@ entries: note: "measured at server defaults page CTE 97 / 97 B (custom / generic), access IOS:idx_packets_heard_inverted, IOS:idx_packets_scope_last_heard; page-CTE budget 146" - id: pk.inv_since_only stmt: ListPacketsWindow + tiers: [local] shape: "inverted pt11 rt2, since only [pt {11} rt - scopes 0 since 2026-10-17 06:58:12.751+00 until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{11}", "{0,1,2,3}", "2026-10-17 06:58:12.751+00", infinity] + params: ["2026-10-17 06:58:12.751+00", infinity, "\\x", infinity, "{11}", "{0,1,2,3}", 51] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -3623,7 +3655,7 @@ entries: - id: pk.s_all_pt14 stmt: ListPacketsByScopesTypes shape: "rare type all scopes [pt {14} rt - scopes 1491 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, {sql: "SELECT array_agg(DISTINCT name) FROM transport_scopes"}, "{14}", "{0,1,2,3}"] + params: [infinity, "\\x", "{14}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT name) FROM transport_scopes"}] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_scope_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 27040, planning_ms: 10} @@ -3631,7 +3663,7 @@ entries: - id: pk.s_all_pt4 stmt: ListPacketsByScopesTypes shape: "[pt {4} rt - scopes 1491 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, {sql: "SELECT array_agg(DISTINCT name) FROM transport_scopes"}, "{4}", "{0,1,2,3}"] + params: [infinity, "\\x", "{4}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT name) FROM transport_scopes"}] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_scope_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 27040, planning_ms: 10} @@ -3639,7 +3671,7 @@ entries: - id: pk.s_all_pt9 stmt: ListPacketsByScopesTypes shape: "[pt {9} rt - scopes 1491 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, {sql: "SELECT array_agg(DISTINCT name) FROM transport_scopes"}, "{9}", "{0,1,2,3}"] + params: [infinity, "\\x", "{9}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT name) FROM transport_scopes"}] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_scope_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 27040, planning_ms: 10} @@ -3647,7 +3679,7 @@ entries: - id: pk.s_all_rt3 stmt: ListPacketsByScopesTypes shape: "[pt - rt {3} scopes 1491 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, {sql: "SELECT array_agg(DISTINCT name) FROM transport_scopes"}, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{3}"] + params: [infinity, "\\x", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{3}", 51, {sql: "SELECT array_agg(DISTINCT name) FROM transport_scopes"}] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_scope_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 27040, planning_ms: 10} @@ -3663,7 +3695,7 @@ entries: - id: pk.s_big0_pt12rt2 stmt: ListPacketsByScopesTypes shape: "[pt {12} rt {2} scopes 1258 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, {sql: "SELECT array_agg(DISTINCT name) FROM (SELECT name FROM transport_scopes ORDER BY name LIMIT 1258) s"}, "{12}", "{2}"] + params: [infinity, "\\x", "{12}", "{2}", 51, {sql: "SELECT array_agg(DISTINCT name) FROM (SELECT name FROM transport_scopes ORDER BY name LIMIT 1258) s"}] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_scope_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 27040, planning_ms: 10} @@ -3671,7 +3703,7 @@ entries: - id: pk.s_big0_pt14 stmt: ListPacketsByScopesTypes shape: "real list + rare type [pt {14} rt - scopes 1258 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, {sql: "SELECT array_agg(DISTINCT name) FROM (SELECT name FROM transport_scopes ORDER BY name LIMIT 1258) s"}, "{14}", "{0,1,2,3}"] + params: [infinity, "\\x", "{14}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT name) FROM (SELECT name FROM transport_scopes ORDER BY name LIMIT 1258) s"}] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_scope_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 27040, planning_ms: 10} @@ -3703,7 +3735,7 @@ entries: - id: pk.s_dash_pt14 stmt: ListPacketsByScopesTypes shape: "[pt {14} rt - scopes 8 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater}'::text[]) v"}, "{14}", "{0,1,2,3}"] + params: [infinity, "\\x", "{14}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_scope_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 7040, planning_ms: 10} @@ -3711,15 +3743,16 @@ entries: - id: pk.s_dash_pt4 stmt: ListPacketsByScopesTypes shape: "real dashboard [pt {4} rt - scopes 8 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater}'::text[]) v"}, "{4}", "{0,1,2,3}"] + params: [infinity, "\\x", "{4}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_scope_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 7040, planning_ms: 10} note: "measured page CTE 54 / 49 B (custom / generic), access Index Only Scan:idx_packets_scope_last_heard; page-CTE budget 5000" - id: pk.s_dash_pt4_cur stmt: ListPacketsByScopesTypes + tiers: [local] shape: "[pt {4} rt - scopes 8 since - until - cursor_ms 1792256400000 limit 50]" - params: ["2026-10-17 17:00:00+00", "\\x", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater}'::text[]) v"}, "{4}", "{0,1,2,3}"] + params: ["2026-10-17 17:00:00+00", "\\x", "{4}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_scope_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 7040, planning_ms: 10} @@ -3727,7 +3760,7 @@ entries: - id: pk.s_dash_rt1 stmt: ListPacketsByScopesTypes shape: "[pt - rt {1} scopes 8 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater}'::text[]) v"}, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{1}"] + params: [infinity, "\\x", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{1}", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_scope_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 7040, planning_ms: 10} @@ -3742,6 +3775,7 @@ entries: note: "no measured case; page-CTE budget 5000" - id: pk.s_emptyslice stmt: ListPacketsPage + shapes: [page1] shape: "scopes=, (empty slice) [pt - rt - scopes 0 since - until - cursor_ms 0 limit 50]" params: [infinity, "\\x", 51] expect: @@ -3766,6 +3800,7 @@ entries: note: "measured page CTE 7 / 7 B (custom / generic), access Index Only Scan:idx_packets_scope_last_heard; page-CTE budget 5000" - id: pk.s_nl_deep200 stmt: ListPacketsByScopes + tiers: [local] shape: "[pt - rt - scopes 1 since - until - cursor_ms 1792256400000 limit 200]" params: ["2026-10-17 17:00:00+00", "\\x", 201, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nl}'::text[]) v"}] expect: @@ -3783,7 +3818,7 @@ entries: - id: pk.s_nl_pt14 stmt: ListPacketsByScopesTypes shape: "[pt {14} rt - scopes 1 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nl}'::text[]) v"}, "{14}", "{0,1,2,3}"] + params: [infinity, "\\x", "{14}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nl}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_scope_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 7040, planning_ms: 10} @@ -3791,7 +3826,7 @@ entries: - id: pk.s_nl_pt4rt1 stmt: ListPacketsByScopesTypes shape: "[pt {4} rt {1} scopes 1 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nl}'::text[]) v"}, "{4}", "{1}"] + params: [infinity, "\\x", "{4}", "{1}", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nl}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_scope_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 7040, planning_ms: 10} @@ -3799,7 +3834,7 @@ entries: - id: pk.s_nl_rt3 stmt: ListPacketsByScopesTypes shape: "[pt - rt {3} scopes 1 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nl}'::text[]) v"}, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{3}"] + params: [infinity, "\\x", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{3}", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nl}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_scope_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 7040, planning_ms: 10} @@ -3822,102 +3857,118 @@ entries: note: "no measured case; page-CTE budget 5000" - id: pk.t_all16_rt stmt: ListPacketsByTypes + shapes: [page1] shape: "explicit full domain [pt {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15} rt {0,1,2,3} scopes 0 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}"] + params: ["{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", infinity, "\\x", 51] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 3040, planning_ms: 10} note: "no measured case; page-CTE budget 1000" - id: pk.t_dupmix stmt: ListPacketsByTypes + shapes: [deep] + tiers: [local] shape: "dup + cursor [pt {4,4,4,13,13} rt {1,1,2} scopes 0 since - until - cursor_ms 1792324800000 limit 200]" - params: ["2026-10-18 12:00:00+00", "\\x", 201, "{4,4,4,13,13}", "{1,1,2}"] + params: ["{4,4,4,13,13}", "{1,1,2}", "2026-10-18 12:00:00+00", "\\x", 201] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 9040, planning_ms: 10} note: "no measured case; page-CTE budget 1000" - id: pk.t_pt054 stmt: ListPacketsByTypes + shapes: [page1] shape: "[pt {0,5,4} rt - scopes 0 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,5,4}", "{0,1,2,3}"] + params: ["{0,5,4}", "{0,1,2,3}", infinity, "\\x", 51] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 3040, planning_ms: 10} note: "no measured case; page-CTE budget 1000" - id: pk.t_pt12_rt2 stmt: ListPacketsByTypes + shapes: [page1] shape: "rare combo [pt {12} rt {2} scopes 0 since - until - cursor_ms 0 limit 200]" - params: [infinity, "\\x", 201, "{12}", "{2}"] + params: ["{12}", "{2}", infinity, "\\x", 201] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 8340, planning_ms: 10} note: "measured page CTE 5 / 5 B (custom / generic), access Index Only Scan:idx_packets_type_last_heard; page-CTE budget 300" - id: pk.t_pt13_l200 stmt: ListPacketsByTypes + shapes: [page1] shape: "[pt {13} rt - scopes 0 since - until - cursor_ms 0 limit 200]" - params: [infinity, "\\x", 201, "{13}", "{0,1,2,3}"] + params: ["{13}", "{0,1,2,3}", infinity, "\\x", 201] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 8340, planning_ms: 10} note: "measured page CTE 17 / 17 B (custom / generic), access Index Only Scan:idx_packets_type_last_heard; page-CTE budget 300" - id: pk.t_pt14 stmt: ListPacketsByTypes + shapes: [page1] shape: "rare [pt {14} rt - scopes 0 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{14}", "{0,1,2,3}"] + params: ["{14}", "{0,1,2,3}", infinity, "\\x", 51] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 2340, planning_ms: 10} note: "measured page CTE 15 / 15 B (custom / generic), access Index Only Scan:idx_packets_type_last_heard; page-CTE budget 300" - id: pk.t_pt2_rt23 stmt: ListPacketsByTypes + shapes: [page1] shape: "[pt {2} rt {2,3} scopes 0 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{2}", "{2,3}"] + params: ["{2}", "{2,3}", infinity, "\\x", 51] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 2340, planning_ms: 10} note: "no measured case; page-CTE budget 300" - id: pk.t_pt4 stmt: ListPacketsByTypes + shapes: [page1] shape: "[pt {4} rt - scopes 0 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{4}", "{0,1,2,3}"] + params: ["{4}", "{0,1,2,3}", infinity, "\\x", 51] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 2340, planning_ms: 10} note: "measured page CTE 20 / 14 B (custom / generic), access Index Only Scan:idx_packets_type_last_heard; page-CTE budget 300" - id: pk.t_pt44 stmt: ListPacketsByTypes + shapes: [page1] shape: "dup [pt {4,4} rt - scopes 0 since - until - cursor_ms 0 limit 200]" - params: [infinity, "\\x", 201, "{4,4}", "{0,1,2,3}"] + params: ["{4,4}", "{0,1,2,3}", infinity, "\\x", 201] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 9040, planning_ms: 10} note: "no measured case; page-CTE budget 1000" - id: pk.t_pt5_ustie_l1 stmt: ListPacketsByTypes + shapes: [tie] + tiers: [local] shape: "[pt {5} rt {0,1} scopes 0 since - until - cursor_ms 1792341411653 limit 1]" - params: ["2026-10-18 16:36:51.653+00", "\\x", 2, "{5}", "{0,1}"] + params: ["{5}", "{0,1}", "2026-10-18 16:36:51.653+00", "\\x", 2] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 380, planning_ms: 10} note: "no measured case; page-CTE budget 300" - id: pk.t_pt5_ustie_l2 stmt: ListPacketsByTypes + shapes: [tie] + tiers: [local] shape: "[pt {5} rt {0,1} scopes 0 since - until - cursor_ms 1792341411653 limit 2]" - params: ["2026-10-18 16:36:51.653+00", "\\x", 3, "{5}", "{0,1}"] + params: ["{5}", "{0,1}", "2026-10-18 16:36:51.653+00", "\\x", 3] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 420, planning_ms: 10} note: "no measured case; page-CTE budget 300" - id: pk.t_pt99_4 stmt: ListPacketsByTypes + shapes: [page1] shape: "[pt {99,4} rt - scopes 0 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{4}", "{0,1,2,3}"] + params: ["{4}", "{0,1,2,3}", infinity, "\\x", 51] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 2340, planning_ms: 10} note: "no measured case; page-CTE budget 300" - id: pk.t_ptempty stmt: ListPacketsPage + shapes: [page1] shape: "payloadTypes=, (empty slice) [pt {} rt - scopes 0 since - until - cursor_ms 0 limit 50]" params: [infinity, "\\x", 51] expect: @@ -3926,48 +3977,55 @@ entries: note: "no measured case; page-CTE budget 50" - id: pk.t_ptempty_rt3 stmt: ListPacketsByTypes + shapes: [page1] shape: "[pt {} rt {3} scopes 0 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{3}"] + params: ["{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{3}", infinity, "\\x", 51] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 2340, planning_ms: 10} note: "no measured case; page-CTE budget 300" - id: pk.t_rt00 stmt: ListPacketsByTypes + shapes: [page1] shape: "dup [pt - rt {0,0} scopes 0 since - until - cursor_ms 0 limit 200]" - params: [infinity, "\\x", 201, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,0}"] + params: ["{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,0}", infinity, "\\x", 201] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 9040, planning_ms: 10} note: "no measured case; page-CTE budget 1000" - id: pk.t_rt3_l200 stmt: ListPacketsByTypes + shapes: [page1] shape: "[pt - rt {3} scopes 0 since - until - cursor_ms 0 limit 200]" - params: [infinity, "\\x", 201, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{3}"] + params: ["{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{3}", infinity, "\\x", 201] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 8340, planning_ms: 10} note: "measured page CTE 66 / 66 B (custom / generic), access Index Only Scan:idx_packets_type_last_heard; page-CTE budget 300" - id: pk.t_rt37 stmt: ListPacketsByTypes + shapes: [page1] shape: "[pt - rt {3,7} scopes 0 since - until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{3}"] + params: ["{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{3}", infinity, "\\x", 51] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 2340, planning_ms: 10} note: "no measured case; page-CTE budget 300" - id: pk.t_web13 stmt: ListPacketsByTypes + shapes: [deep] + tiers: [local] shape: "[pt {0,1,2,3,4,5,6,7,8,9,10,11,15} rt - scopes 0 since - until - cursor_ms 1792317600000 limit 50]" - params: ["2026-10-18 10:00:00+00", "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,15}", "{0,1,2,3}"] + params: ["{0,1,2,3,4,5,6,7,8,9,10,11,15}", "{0,1,2,3}", "2026-10-18 10:00:00+00", "\\x", 51] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 3040, planning_ms: 10} note: "measured page CTE 175 / 175 B (custom / generic), access Index Only Scan:idx_packets_type_last_heard; page-CTE budget 1000" - id: pk.w_epoch stmt: ListPacketsWindow + tiers: [local] shape: "extreme [pt - rt - scopes 0 since 1970-01-01 00:00:00+00 until 9999-12-31 23:59:59.999008+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", "1970-01-01 00:00:00+00", "9999-12-31 23:59:59.999008+00"] + params: ["1970-01-01 00:00:00+00", infinity, "\\x", "9999-12-31 23:59:59.999008+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -3976,8 +4034,9 @@ entries: note: "measured at server defaults page CTE 56 / 56 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 84" - id: pk.w_eq stmt: ListPacketsWindow + tiers: [local] shape: "tiny [pt - rt - scopes 0 since 2026-10-18 12:00:00.021+00 until 2026-10-18 12:00:00.022+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", "2026-10-18 12:00:00.021+00", "2026-10-18 12:00:00.022+00"] + params: ["2026-10-18 12:00:00.021+00", infinity, "\\x", "2026-10-18 12:00:00.022+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -3986,8 +4045,9 @@ entries: note: "measured at server defaults page CTE 24295 / 237399 B (custom / generic), access Bitmap Heap Scan:packets, Bitmap IS:idx_packets_type_last_heard, IOS:idx_packets_heard_inverted; page-CTE budget 36443" - id: pk.w_long stmt: ListPacketsWindow + tiers: [local] shape: "long-lived [pt - rt - scopes 0 since 2026-10-02 00:00:00+00 until 2026-10-12 00:00:00+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", "2026-10-02 00:00:00+00", "2026-10-12 00:00:00+00"] + params: ["2026-10-02 00:00:00+00", infinity, "\\x", "2026-10-12 00:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -3996,8 +4056,9 @@ entries: note: "measured at server defaults page CTE 84284 / 84284 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 126426" - id: pk.w_negsince stmt: ListPacketsWindow + tiers: [local] shape: "[pt - rt - scopes 0 since 1969-12-31 23:59:59+00 until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", "1969-12-31 23:59:59+00", infinity] + params: ["1969-12-31 23:59:59+00", infinity, "\\x", infinity, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4006,8 +4067,9 @@ entries: note: "measured at server defaults page CTE 56 / 56 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 84" - id: pk.w_pf_32m stmt: ListPacketsWindow + tiers: [local] shape: "[pt - rt {0} scopes 0 since 2026-10-18 16:28:00+00 until 2026-10-18 17:00:00+00 cursor_ms 0 limit 200]" - params: [infinity, "\\x", 201, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", "2026-10-18 16:28:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-18 16:28:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", 201] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4016,8 +4078,9 @@ entries: note: "measured at server defaults page CTE 2799 / 1831 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_type_last_heard; page-CTE budget 4199" - id: pk.w_pf_32m_real stmt: ListPacketsWindow + tiers: [local] shape: "pathfinder 32 min (log shape) [pt - rt {0} scopes 0 since 2026-10-18 16:28:00+00 until 2026-10-18 17:00:00+00 cursor_ms 0 limit 200]" - params: [infinity, "\\x", 201, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", "2026-10-18 16:28:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-18 16:28:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", 201] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4026,8 +4089,9 @@ entries: note: "measured at server defaults page CTE 2799 / 1831 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_type_last_heard; page-CTE budget 4199" - id: pk.w_pf_5m stmt: ListPacketsWindow + tiers: [local] shape: "5-min window, < 3x201 matches [pt - rt {0} scopes 0 since 2026-10-18 16:55:00+00 until 2026-10-18 17:00:00+00 cursor_ms 0 limit 200]" - params: [infinity, "\\x", 201, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", "2026-10-18 16:55:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-18 16:55:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", 201] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4036,18 +4100,20 @@ entries: note: "measured at server defaults page CTE 443 / 1959 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_type_last_heard; page-CTE budget 665" - id: pk.w_pf_5m_b stmt: ListPacketsWindow + tiers: [local] shape: "pathfinder 5 min mid-day [pt - rt {0} scopes 0 since 2026-10-18 12:00:00+00 until 2026-10-18 12:05:00+00 cursor_ms 0 limit 200]" - params: [infinity, "\\x", 201, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", "2026-10-18 12:00:00+00", "2026-10-18 12:05:00+00"] + params: ["2026-10-18 12:00:00+00", infinity, "\\x", "2026-10-18 12:05:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", 201] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] sort_exempt: [window_branches] budget: {buffers: 31496, planning_ms: 10} - note: "measured at server defaults page CTE 15637 / 234030 B (custom / generic), access Bitmap Heap Scan:packets, Bitmap IS:idx_packets_type_last_heard, IOS:idx_packets_heard_inverted; page-CTE budget 23456" + note: "measured at server defaults page CTE 15637 / 234030 B (custom / generic), access Bitmap Heap Scan:packets, Bitmap IS:idx_packets_type_last_heard, IOS:idx_packets_heard_inverted; page-CTE budget 23456; typed window far behind now: accepted slower shape" - id: pk.w_pf_6h stmt: ListPacketsWindow + tiers: [local] shape: "pathfinder real [pt - rt {0} scopes 0 since 2026-10-18 10:30:00+00 until 2026-10-18 17:00:00+00 cursor_ms 0 limit 200]" - params: [infinity, "\\x", 201, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", "2026-10-18 10:30:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-18 10:30:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", 201] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4056,8 +4122,9 @@ entries: note: "measured at server defaults page CTE 1831 / 1831 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 2747" - id: pk.w_pf_6h_2ndlast stmt: ListPacketsWindow + tiers: [local] shape: "2nd-last page [pt - rt {0} scopes 0 since 2026-10-18 10:30:00+00 until 2026-10-18 17:00:00+00 cursor_ms 1792319629827 limit 200]" - params: ["2026-10-18 10:33:49.827+00", "\\x", 201, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", "2026-10-18 10:30:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-18 10:30:00+00", "2026-10-18 10:33:49.827+00", "\\x", "2026-10-18 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", 201] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4066,8 +4133,9 @@ entries: note: "measured at server defaults page CTE 1812 / 1812 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 2718" - id: pk.w_pf_6h_cur stmt: ListPacketsWindow + tiers: [local] shape: "[pt - rt {0} scopes 0 since 2026-10-18 10:30:00+00 until 2026-10-18 17:00:00+00 cursor_ms 1792339200000 limit 200]" - params: ["2026-10-18 16:00:00+00", "\\x", 201, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", "2026-10-18 10:30:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-18 10:30:00+00", "2026-10-18 16:00:00+00", "\\x", "2026-10-18 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", 201] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4076,8 +4144,9 @@ entries: note: "measured at server defaults page CTE 1567 / 1567 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 2351" - id: pk.w_pf_6h_last stmt: ListPacketsWindow + tiers: [local] shape: "last page of 6.5h chain [pt - rt {0} scopes 0 since 2026-10-18 10:30:00+00 until 2026-10-18 17:00:00+00 cursor_ms 1792319480893 limit 200]" - params: ["2026-10-18 10:31:20.893+00", "\\x", 201, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", "2026-10-18 10:30:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-18 10:30:00+00", "2026-10-18 10:31:20.893+00", "\\x", "2026-10-18 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", 201] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4086,8 +4155,9 @@ entries: note: "measured at server defaults page CTE 912 / 912 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 1368" - id: pk.w_pt14_3d stmt: ListPacketsWindow + tiers: [local] shape: "rare type window [pt {14} rt - scopes 0 since 2026-10-15 17:00:00+00 until 2026-10-18 17:00:00+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{14}", "{0,1,2,3}", "2026-10-15 17:00:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-15 17:00:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", "{14}", "{0,1,2,3}", 51] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4096,8 +4166,9 @@ entries: note: "measured at server defaults page CTE 754 / 233316 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_type_last_heard; page-CTE budget 1131" - id: pk.w_pt4rt1_tied stmt: ListPacketsWindow + tiers: [local] shape: "[pt {4} rt {1} scopes 0 since 2026-10-18 00:00:00+00 until - cursor_ms 1792342797722 limit 50]" - params: ["2026-10-18 16:59:57.722+00", "\\x", 51, "{4}", "{1}", "2026-10-18 00:00:00+00", infinity] + params: ["2026-10-18 00:00:00+00", "2026-10-18 16:59:57.722+00", "\\x", infinity, "{4}", "{1}", 51] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4106,8 +4177,9 @@ entries: note: "measured at server defaults page CTE 768 / 768 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 1152" - id: pk.w_since75 stmt: ListPacketsWindow + tiers: [local] shape: "[pt - rt - scopes 0 since 2026-10-18 15:45:00+00 until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", "2026-10-18 15:45:00+00", infinity] + params: ["2026-10-18 15:45:00+00", infinity, "\\x", infinity, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4116,8 +4188,9 @@ entries: note: "measured at server defaults page CTE 57 / 57 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 86" - id: pk.w_since75_pt4 stmt: ListPacketsWindow + tiers: [local] shape: "[pt {4} rt - scopes 0 since 2026-10-18 15:45:00+00 until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{4}", "{0,1,2,3}", "2026-10-18 15:45:00+00", infinity] + params: ["2026-10-18 15:45:00+00", infinity, "\\x", infinity, "{4}", "{0,1,2,3}", 51] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4126,8 +4199,9 @@ entries: note: "measured at server defaults page CTE 7185 / 369 B (custom / generic), access Bitmap Heap Scan:packets, Bitmap IS:idx_packets_type_last_heard, IOS:idx_packets_heard_inverted; page-CTE budget 10778" - id: pk.w_until_deep stmt: ListPacketsWindow + tiers: [local] shape: "[pt - rt - scopes 0 since - until 2026-10-16 17:00:00+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", "-infinity", "2026-10-16 17:00:00+00"] + params: ["-infinity", infinity, "\\x", "2026-10-16 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4136,18 +4210,21 @@ entries: note: "measured at server defaults page CTE 46691 / 46691 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 70037" - id: pk.w_until_only_rt0 stmt: ListPacketsWindow + tiers: [local] shape: "until only (zero traffic) [pt - rt {0} scopes 0 since - until 2026-10-18 06:00:00+00 cursor_ms 0 limit 200]" - params: [infinity, "\\x", 201, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", "-infinity", "2026-10-18 06:00:00+00"] + params: ["-infinity", infinity, "\\x", "2026-10-18 06:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", 201] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] sort_exempt: [window_branches] budget: {buffers: 703970, planning_ms: 10} + known_issue: "until-only windows have no lower bound, as today" note: "measured at server defaults page CTE 463953 / 463953 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 695930" - id: pk.ws_all_1d stmt: ListPacketsWindowScoped + tiers: [local] shape: "[pt - rt - scopes 1491 since 2026-10-17 17:00:00+00 until 2026-10-18 17:00:00+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", {sql: "SELECT array_agg(DISTINCT name) FROM transport_scopes"}, "2026-10-17 17:00:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-17 17:00:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT name) FROM transport_scopes"}] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id ="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4156,8 +4233,9 @@ entries: note: "measured at server defaults page CTE 4549 / 7534 B (custom / generic), access IOS:idx_packets_heard_inverted, IOS:idx_packets_scope_last_heard; page-CTE budget 6824" - id: pk.ws_all_until_deep stmt: ListPacketsWindowScoped + tiers: [local] shape: "[pt - rt - scopes 1491 since - until 2026-10-16 17:00:00+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", {sql: "SELECT array_agg(DISTINCT name) FROM transport_scopes"}, "-infinity", "2026-10-16 17:00:00+00"] + params: ["-infinity", infinity, "\\x", "2026-10-16 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT name) FROM transport_scopes"}] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id ="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4166,8 +4244,9 @@ entries: note: "measured at server defaults page CTE 5467 / 8449 B (custom / generic), access IOS:idx_packets_heard_inverted, IOS:idx_packets_scope_last_heard; page-CTE budget 8201" - id: pk.ws_chain_dash stmt: ListPacketsWindowScoped + tiers: [local] shape: "dashboard scopes + pt4 1 day [pt {4} rt - scopes 8 since 2026-10-17 17:00:00+00 until 2026-10-18 17:00:00+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{4}", "{0,1,2,3}", {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater}'::text[]) v"}, "2026-10-17 17:00:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-17 17:00:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", "{4}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id ="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4176,8 +4255,9 @@ entries: note: "measured at server defaults page CTE 49 / 49 B (custom / generic), access Bitmap IS:transport_scopes_name_key, IOS:idx_packets_heard_inverted, IOS:idx_packets_scope_last_heard; page-CTE budget 74" - id: pk.ws_chain_nl_1h stmt: ListPacketsWindowScoped + tiers: [local] shape: "scoped window chain [pt - rt - scopes 1 since 2026-10-18 15:00:00+00 until 2026-10-18 16:00:00+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nl}'::text[]) v"}, "2026-10-18 15:00:00+00", "2026-10-18 16:00:00+00"] + params: ["2026-10-18 15:00:00+00", infinity, "\\x", "2026-10-18 16:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nl}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id ="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4186,8 +4266,9 @@ entries: note: "measured at server defaults page CTE 11 / 11 B (custom / generic), access IOS:idx_packets_heard_inverted, IOS:idx_packets_scope_last_heard; page-CTE budget 50" - id: pk.ws_chain_sea4d stmt: ListPacketsWindowScoped + tiers: [local] shape: "scoped window chain, limit 2 [pt - rt - scopes 1 since 2026-10-14 17:00:00+00 until 2026-10-18 17:00:00+00 cursor_ms 0 limit 2]" - params: [infinity, "\\x", 3, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#SEA}'::text[]) v"}, "2026-10-14 17:00:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-14 17:00:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 3, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#SEA}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id ="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4196,8 +4277,9 @@ entries: note: "measured at server defaults page CTE 9 / 9 B (custom / generic), access IOS:idx_packets_heard_inverted, IOS:idx_packets_scope_last_heard; page-CTE budget 50" - id: pk.ws_dash_pt4_cur stmt: ListPacketsWindowScoped + tiers: [local] shape: "[pt {4} rt - scopes 8 since 2026-10-17 17:00:00+00 until - cursor_ms 1792303200000 limit 50]" - params: ["2026-10-18 06:00:00+00", "\\x", 51, "{4}", "{0,1,2,3}", {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater}'::text[]) v"}, "2026-10-17 17:00:00+00", infinity] + params: ["2026-10-17 17:00:00+00", "2026-10-18 06:00:00+00", "\\x", infinity, "{4}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id ="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4206,8 +4288,9 @@ entries: note: "measured at server defaults page CTE 49 / 49 B (custom / generic), access Bitmap IS:transport_scopes_name_key, IOS:idx_packets_heard_inverted, IOS:idx_packets_scope_last_heard; page-CTE budget 74" - id: pk.ws_emptyslice stmt: ListPacketsWindow + tiers: [local] shape: "scopes=, with window [pt - rt - scopes 0 since 2026-10-18 16:00:00+00 until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", "2026-10-18 16:00:00+00", infinity] + params: ["2026-10-18 16:00:00+00", infinity, "\\x", infinity, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan", "Bitmap Heap Scan"], index_cond: ["last_heard_at >="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4216,8 +4299,9 @@ entries: note: "measured at server defaults page CTE 57 / 57 B (custom / generic), access IOS:idx_packets_heard_inverted, IS:idx_packets_last_heard_hash; page-CTE budget 86" - id: pk.ws_nl_pt4 stmt: ListPacketsWindowScoped + tiers: [local] shape: "[pt {4} rt - scopes 1 since 2026-10-17 17:00:00+00 until 2026-10-18 17:00:00+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{4}", "{0,1,2,3}", {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nl}'::text[]) v"}, "2026-10-17 17:00:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-17 17:00:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", "{4}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nl}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id ="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4226,8 +4310,9 @@ entries: note: "measured at server defaults page CTE 18 / 18 B (custom / generic), access IOS:idx_packets_heard_inverted, IOS:idx_packets_scope_last_heard; page-CTE budget 50" - id: pk.ws_sea_1d stmt: ListPacketsWindowScoped + tiers: [local] shape: "[pt - rt - scopes 1 since 2026-10-17 17:00:00+00 until 2026-10-18 17:00:00+00 cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#sea}'::text[]) v"}, "2026-10-17 17:00:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-17 17:00:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#sea}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id ="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4236,8 +4321,9 @@ entries: note: "measured at server defaults page CTE 9 / 9 B (custom / generic), access IOS:idx_packets_heard_inverted, IOS:idx_packets_scope_last_heard; page-CTE budget 50" - id: pk.ws_SEA_4d stmt: ListPacketsWindowScoped + tiers: [local] shape: "[pt - rt - scopes 1 since 2026-10-14 17:00:00+00 until 2026-10-18 17:00:00+00 cursor_ms 0 limit 200]" - params: [infinity, "\\x", 201, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#SEA}'::text[]) v"}, "2026-10-14 17:00:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-14 17:00:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 201, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#SEA}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id ="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4246,8 +4332,9 @@ entries: note: "measured at server defaults page CTE 10 / 10 B (custom / generic), access IOS:idx_packets_heard_inverted, IOS:idx_packets_scope_last_heard; page-CTE budget 50" - id: pk.ws_unknown stmt: ListPacketsWindowScoped + tiers: [local] shape: "[pt - rt - scopes 1 since 2026-10-17 17:00:00+00 until - cursor_ms 0 limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nope}'::text[]) v"}, "2026-10-17 17:00:00+00", infinity] + params: ["2026-10-17 17:00:00+00", infinity, "\\x", infinity, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51, {sql: "SELECT array_agg(DISTINCT v) FROM unnest('{#nope}'::text[]) v"}] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id ="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4265,7 +4352,7 @@ entries: - id: pk.s_bogus5000_pt4 stmt: ListPacketsByScopesTypes shape: "5,000 bogus names + payloadTypes=4 [pt - rt - scopes 5000 since - until - cursor_ms - limit 50]" - params: [infinity, "\\x", 51, {r: bogus5000}, "{4}", "{0,1,2,3}"] + params: [infinity, "\\x", "{4}", "{0,1,2,3}", 51, {r: bogus5000}] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_scope_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 27040, planning_ms: 60} @@ -4280,8 +4367,9 @@ entries: note: "no measured case; page-CTE budget 25000" - id: pk.ws_bogus5000_1d stmt: ListPacketsWindowScoped + tiers: [local] shape: "5,000 bogus names, 1-day window [pt - rt - scopes 5000 since 2026-10-17 17:00:00+00 until 2026-10-18 17:00:00+00 cursor_ms - limit 50]" - params: [infinity, "\\x", 51, "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", {r: bogus5000}, "2026-10-17 17:00:00+00", "2026-10-18 17:00:00+00"] + params: ["2026-10-17 17:00:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51, {r: bogus5000}] expect: driver: [{scope: "CTE page", relation: packets, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["scope_id ="]}] require: [{node: "Index Only Scan", relations: [idx_packets_heard_inverted]}] @@ -4290,6 +4378,8 @@ entries: note: "no measured case; page-CTE budget 25000" - id: pk.e_pc_ustie_l1 stmt: ListPacketsPage + shapes: [tie] + tiers: [fixture, local] shape: "pageCursor = the newest microsecond-tie pair's top row, limit 1 [pt - rt - scopes 0 since - until - cursor_ms - limit 1]" params: [{r: ustie_ts}, {r: ustie_hash}, 2] expect: @@ -4298,6 +4388,8 @@ entries: note: "no measured case; page-CTE budget 50" - id: pk.e_pc_ustie_l2 stmt: ListPacketsPage + shapes: [tie] + tiers: [fixture, local] shape: "same, limit 2 [pt - rt - scopes 0 since - until - cursor_ms - limit 2]" params: [{r: ustie_ts}, {r: ustie_hash}, 3] expect: @@ -4306,14 +4398,16 @@ entries: note: "no measured case; page-CTE budget 50" - id: pk.t_pt4_pc_deep stmt: ListPacketsByTypes + shapes: [deep] shape: "payloadTypes=4, pageCursor 100,000 packets deep (prototype) [pt - rt - scopes 0 since - until - cursor_ms - limit 200]" - params: [{r: pk_deep_ts}, {r: pk_deep_hash}, 201, "{4}", "{0,1,2,3}"] + params: ["{4}", "{0,1,2,3}", {r: pk_deep_ts}, {r: pk_deep_hash}, 201] expect: driver: [{scope: "CTE page", relation: packets, index: idx_packets_type_last_heard, nodes: ["Index Only Scan", "Index Scan"], index_cond: ["payload_type =", "ROW(last_heard_at, packet_hash) <"]}] budget: {buffers: 8340, planning_ms: 10} note: "no measured case; page-CTE budget 300" - id: pk.e_pc_deep stmt: ListPacketsPage + shapes: [deep] shape: "pageCursor 100,000 packets deep (prototype) [pt - rt - scopes 0 since - until - cursor_ms - limit 200]" params: [{r: pk_deep_ts}, {r: pk_deep_hash}, 201] expect: diff --git a/db/planguard/diff/packets_global.yaml b/db/planguard/diff/packets_global.yaml new file mode 100644 index 0000000..f5ac930 --- /dev/null +++ b/db/planguard/diff/packets_global.yaml @@ -0,0 +1,1082 @@ +# Copyright 2026 Beacon Contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +# Global /packets pages: the single ListPackets statement the server ran before (old:) against the per-shape +# statements that replace it (new:), one pair per corpus case of the packets_global group (same ids without "pk."). +# The ref gets the params the server used to send for the request (raw type and scope lists, NULL for an absent +# filter, the millisecond cursor, limit+1); the cand gets the params it sends now (normalised lists, -infinity and +# infinity for absent bounds, the cursor as a timestamp and an empty hash, or the exact page cursor). +# +# Needs the overlay overlay/packets_global.sql (schema t_s5a). Run it with walks: +# planguard diff --old-ref origin/dev --cases db/planguard/diff/packets_global.yaml --walk 1,2,3,web --dsn ... +# then DROP SCHEMA t_s5a CASCADE. +# +# Only the named classes may differ: rows tied on last_heard_at come out in packet_hash order (tie_order), the +# latest observer of a packet whose newest observations share heard_at is the highest id (latest_observer_tie), +# and a page cursor returns the rows of its own millisecond that the legacy cursor skips (pagecursor_tie_rows). +# +# The walk pairs page through the new statements with their exact cursor at 1, 2, 3 and web_limit rows a page +# and compare every page with an oracle: the old statement's filter in the exact (last_heard_at, packet_hash) +# order, read as one page of max_rows. A walk must return the oracle's rows, each once, in its order. +# +# The cases use fixed times from a 2026-10-18 snapshot (T0 17:00 UTC): they mean something only on a copy that +# holds data around those times. +batches: + - name: packets_global + search_path: "t_s5a, public" + statements: + old: "old:ListPackets" + page: "new:ListPacketsPage" + types: "new:ListPacketsByTypes" + scopes: "new:ListPacketsByScopes" + scopes_types: "new:ListPacketsByScopesTypes" + window: "new:ListPacketsWindow" + window_scoped: "new:ListPacketsWindowScoped" + oracle_page: "file:sql/oracle/OraclePacketsPage.sql" + oracle_types: "file:sql/oracle/OraclePacketsByTypes.sql" + oracle_scopes_types: "file:sql/oracle/OraclePacketsByScopesTypes.sql" + oracle_window: "file:sql/oracle/OraclePacketsWindow.sql" + pairs: + # [pt - rt - scopes 0 since - until - cursor_ms 1792342797721 limit 50] + - id: e_at_tied + ref: {stmt: old, params: [null, null, null, null, "2026-10-18 16:59:57.721+00", 51, null]} + cands: + - {stmt: page, params: ["2026-10-18 16:59:57.721+00", "\\x", 51]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: &latest [latest_observer_id, latest_observer_name, latest_observer_iata, latest_observer_path_length_byte, + latest_observer_hash_size, latest_observer_hop_count, latest_observer_path_bytes] + expect: {page: identical} + # cursor 1ms [pt - rt - scopes 0 since - until - cursor_ms 1 limit 50] + - id: e_c1 + ref: {stmt: old, params: [null, null, null, null, "1970-01-01 00:00:00.001+00", 51, null]} + cands: + - {stmt: page, params: ["1970-01-01 00:00:00.001+00", "\\x", 51]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {page: identical} + # cursor year 9999 [pt - rt - scopes 0 since - until - cursor_ms 253402300799999 limit 50] + - id: e_c9999 + ref: {stmt: old, params: [null, null, null, null, "9999-12-31 23:59:59.999+00", 51, null]} + cands: + - {stmt: page, params: ["9999-12-31 23:59:59.999008+00", "\\x", 51]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {page: identical} + # cursor<0 [pt - rt - scopes 0 since - until - cursor_ms -5 limit 50] + - id: e_cneg + ref: {stmt: old, params: [null, null, null, null, null, 51, null]} + cands: + - {stmt: page, params: [infinity, "\\x", 51]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {page: identical} + # [pt - rt - scopes 0 since - until - cursor_ms 1792170000000 limit 50] + - id: e_deep + ref: {stmt: old, params: [null, null, null, null, "2026-10-16 17:00:00+00", 51, null]} + cands: + - {stmt: page, params: ["2026-10-16 17:00:00+00", "\\x", 51]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {page: allowed} + # [pt - rt - scopes 0 since - until - cursor_ms 1792170000000 limit 200] + - id: e_deep200 + ref: {stmt: old, params: [null, null, null, null, "2026-10-16 17:00:00+00", 201, null]} + cands: + - {stmt: page, params: ["2026-10-16 17:00:00+00", "\\x", 201]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {page: allowed} + # [pt - rt - scopes 0 since - until - cursor_ms 0 limit 1] + - id: e_l1 + ref: {stmt: old, params: [null, null, null, null, null, 2, null]} + cands: + - {stmt: page, params: [infinity, "\\x", 2]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {page: identical} + # [pt - rt - scopes 0 since - until - cursor_ms 0 limit 200] + - id: e_l200 + ref: {stmt: old, params: [null, null, null, null, null, 201, null]} + cands: + - {stmt: page, params: [infinity, "\\x", 201]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {page: allowed} + # [pt - rt - scopes 0 since - until - cursor_ms 0 limit 50] + - id: e_l50 + ref: {stmt: old, params: [null, null, null, null, null, 51, null]} + cands: + - {stmt: page, params: [infinity, "\\x", 51]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {page: identical} + # tail [pt - rt - scopes 0 since - until - cursor_ms 1792083600050 limit 50] + - id: e_tail + ref: {stmt: old, params: [null, null, null, null, "2026-10-15 17:00:00.05+00", 51, null]} + cands: + - {stmt: page, params: ["2026-10-15 17:00:00.05+00", "\\x", 51]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {page: identical} + # tied ms on top [pt - rt - scopes 0 since - until - cursor_ms 1792342797722 limit 1] + - id: e_tiedms_l1 + ref: {stmt: old, params: [null, null, null, null, "2026-10-18 16:59:57.722+00", 2, null]} + cands: + - {stmt: page, params: ["2026-10-18 16:59:57.722+00", "\\x", 2]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {page: identical} + # [pt - rt - scopes 0 since - until - cursor_ms 1792342797722 limit 2] + - id: e_tiedms_l2 + ref: {stmt: old, params: [null, null, null, null, "2026-10-18 16:59:57.722+00", 3, null]} + cands: + - {stmt: page, params: ["2026-10-18 16:59:57.722+00", "\\x", 3]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {page: identical} + # us tie [pt - rt - scopes 0 since - until - cursor_ms 1792341411653 limit 1] + - id: e_ustie_l1 + ref: {stmt: old, params: [null, null, null, null, "2026-10-18 16:36:51.653+00", 2, null]} + cands: + - {stmt: page, params: ["2026-10-18 16:36:51.653+00", "\\x", 2]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {page: identical} + # [pt - rt - scopes 0 since - until - cursor_ms 1792341411653 limit 2] + - id: e_ustie_l2 + ref: {stmt: old, params: [null, null, null, null, "2026-10-18 16:36:51.653+00", 3, null]} + cands: + - {stmt: page, params: ["2026-10-18 16:36:51.653+00", "\\x", 3]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {page: identical} + # inverted pt11 rt2 [pt - rt - scopes 0 since 2026-10-18 10:19:12.796+00 until 2026-10-18 10:24:12.796+00 cursor_ms 0 limit 50] + - id: inv_all_5m + ref: {stmt: old, params: [null, null, "2026-10-18 10:19:12.796+00", "2026-10-18 10:24:12.796+00", null, 51, null]} + cands: + - {stmt: window, params: ["2026-10-18 10:19:12.796+00", infinity, "\\x", "2026-10-18 10:24:12.796+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {window: identical} + # inverted pt1 rt2 [pt {1} rt - scopes 0 since 2026-10-18 05:59:27.384+00 until 2026-10-18 06:09:27.384+00 cursor_ms 0 limit 50] + - id: inv_pt1_10m + ref: {stmt: old, params: ["{1}", null, "2026-10-18 05:59:27.384+00", "2026-10-18 06:09:27.384+00", null, 51, null]} + cands: + - {stmt: window, params: ["2026-10-18 05:59:27.384+00", infinity, "\\x", "2026-10-18 06:09:27.384+00", "{1}", "{0,1,2,3}", 51]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {window: identical} + # inverted pt4 rt2, deep [pt {4} rt {2} scopes 0 since 2026-10-16 20:36:52.971+00 until 2026-10-16 21:36:52.971+00 cursor_ms 0 limit 50] + - id: inv_pt4rt2_1h + ref: {stmt: old, params: ["{4}", "{2}", "2026-10-16 20:36:52.971+00", "2026-10-16 21:36:52.971+00", null, 51, null]} + cands: + - {stmt: window, params: ["2026-10-16 20:36:52.971+00", infinity, "\\x", "2026-10-16 21:36:52.971+00", "{4}", "{2}", 51]} + key: [last_heard_at] + allowed: [tie_order, latest_observer_tie] + omit: *latest + expect: {window: identical} + # inverted pt5 rt0 row k 1 ORDER BY last_heard_at DESC LIMIT 1))"}, 2, null]} + cands: + - {stmt: page, params: [{sql: "SELECT last_heard_at FROM packets GROUP BY last_heard_at HAVING count(*) > 1 ORDER BY last_heard_at DESC LIMIT 1"}, {sql: "SELECT packet_hash FROM packets WHERE last_heard_at = (SELECT last_heard_at FROM packets GROUP BY last_heard_at HAVING count(*) > 1 ORDER BY last_heard_at DESC LIMIT 1) ORDER BY packet_hash DESC LIMIT 1"}, 2]} + key: [last_heard_at] + allowed: [pagecursor_tie_rows, tie_order, latest_observer_tie] + require: [pagecursor_tie_rows] + tie_boundary: 5 + tie_cursor: {ts: 1, hash: 2, hash_column: packet_hash} + omit: *latest + expect: {page: allowed} + # same, limit 2 [pt - rt - scopes 0 since - until - cursor_ms - limit 2] + - id: e_pc_ustie_l2 + ref: {stmt: old, params: [null, null, null, null, {sql: "SELECT date_trunc('milliseconds', (SELECT last_heard_at FROM packets GROUP BY last_heard_at HAVING count(*) > 1 ORDER BY last_heard_at DESC LIMIT 1))"}, 3, null]} + cands: + - {stmt: page, params: [{sql: "SELECT last_heard_at FROM packets GROUP BY last_heard_at HAVING count(*) > 1 ORDER BY last_heard_at DESC LIMIT 1"}, {sql: "SELECT packet_hash FROM packets WHERE last_heard_at = (SELECT last_heard_at FROM packets GROUP BY last_heard_at HAVING count(*) > 1 ORDER BY last_heard_at DESC LIMIT 1) ORDER BY packet_hash DESC LIMIT 1"}, 3]} + key: [last_heard_at] + allowed: [pagecursor_tie_rows, tie_order, latest_observer_tie] + require: [pagecursor_tie_rows] + tie_boundary: 5 + tie_cursor: {ts: 1, hash: 2, hash_column: packet_hash} + omit: *latest + expect: {page: allowed} + # payloadTypes=4, pageCursor 100,000 packets deep (prototype) [pt - rt - scopes 0 since - until - cursor_ms - limit 200] + - id: t_pt4_pc_deep + ref: {stmt: old, params: ["{4}", null, null, null, {sql: "SELECT date_trunc('milliseconds', (SELECT last_heard_at FROM packets ORDER BY last_heard_at DESC, packet_hash DESC OFFSET 100000 LIMIT 1))"}, 201, null]} + cands: + - {stmt: types, params: ["{4}", "{0,1,2,3}", {sql: "SELECT last_heard_at FROM packets ORDER BY last_heard_at DESC, packet_hash DESC OFFSET 100000 LIMIT 1"}, {sql: "SELECT packet_hash FROM packets ORDER BY last_heard_at DESC, packet_hash DESC OFFSET 100000 LIMIT 1"}, 201]} + key: [last_heard_at] + allowed: [pagecursor_tie_rows, tie_order, latest_observer_tie] + tie_boundary: 5 + tie_cursor: {ts: 3, hash: 4, hash_column: packet_hash} + omit: *latest + expect: {types: allowed} + # pageCursor 100,000 packets deep (prototype) [pt - rt - scopes 0 since - until - cursor_ms - limit 200] + - id: e_pc_deep + ref: {stmt: old, params: [null, null, null, null, {sql: "SELECT date_trunc('milliseconds', (SELECT last_heard_at FROM packets ORDER BY last_heard_at DESC, packet_hash DESC OFFSET 100000 LIMIT 1))"}, 201, null]} + cands: + - {stmt: page, params: [{sql: "SELECT last_heard_at FROM packets ORDER BY last_heard_at DESC, packet_hash DESC OFFSET 100000 LIMIT 1"}, {sql: "SELECT packet_hash FROM packets ORDER BY last_heard_at DESC, packet_hash DESC OFFSET 100000 LIMIT 1"}, 201]} + key: [last_heard_at] + allowed: [pagecursor_tie_rows, tie_order, latest_observer_tie] + tie_boundary: 5 + tie_cursor: {ts: 1, hash: 2, hash_column: packet_hash} + omit: *latest + expect: {page: allowed} + + # Walks: the oracle as one page of max_rows rows, each cand walked with its exact cursor. + # every packet + - id: walk.page + ref: {stmt: oracle_page, params: [infinity, "\\x", 51, null, null, null, null, null]} + cands: + - {stmt: page, params: [infinity, "\\x", 51]} + walk: + limit_param: 3 + fetch_extra: 1 + cursor: [{param: 1, column: last_heard_at}, {param: 2, column: packet_hash}] + first: [infinity, '\x'] + web_limit: 50 + max_rows: 2000 + oracle: true + expect: {page: identical} + # payloadTypes=4 + - id: walk.types_pt4 + ref: {stmt: oracle_types, params: ["{4}", null, infinity, "\\x", 51, null, null, null]} + cands: + - {stmt: types, params: ["{4}", "{0,1,2,3}", infinity, "\\x", 51]} + walk: + limit_param: 5 + fetch_extra: 1 + cursor: [{param: 3, column: last_heard_at}, {param: 4, column: packet_hash}] + first: [infinity, '\x'] + web_limit: 50 + max_rows: 2000 + oracle: true + expect: {types: identical} + # scopes=#nl + - id: walk.scopes_nl + ref: {stmt: oracle_page, params: [infinity, "\\x", 51, "{#nl}", null, null, null, null]} + cands: + - {stmt: scopes, params: [infinity, "\\x", 51, "{#nl}"]} + walk: + limit_param: 3 + fetch_extra: 1 + cursor: [{param: 1, column: last_heard_at}, {param: 2, column: packet_hash}] + first: [infinity, '\x'] + web_limit: 50 + max_rows: 2000 + oracle: true + expect: {scopes: identical} + # the dashboard: 8 scopes and payloadTypes=4 + - id: walk.scopes_dash_pt4 + ref: {stmt: oracle_scopes_types, params: [infinity, "\\x", "{4}", null, 51, "{#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater}", null, null]} + cands: + - {stmt: scopes_types, params: [infinity, "\\x", "{4}", "{0,1,2,3}", 51, "{#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater}"]} + walk: + limit_param: 5 + fetch_extra: 1 + cursor: [{param: 1, column: last_heard_at}, {param: 2, column: packet_hash}] + first: [infinity, '\x'] + web_limit: 50 + max_rows: 2000 + oracle: true + expect: {scopes_types: identical} + # routeTypes=0, a 32-minute window + - id: walk.window_pf_32m + ref: {stmt: oracle_window, params: ["2026-10-18 16:28:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", null, "{0}", 201, null]} + cands: + - {stmt: window, params: ["2026-10-18 16:28:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0}", 201]} + walk: + limit_param: 7 + fetch_extra: 1 + cursor: [{param: 2, column: last_heard_at}, {param: 3, column: packet_hash}] + first: [infinity, '\x'] + web_limit: 50 + max_rows: 2000 + oracle: true + expect: {window: identical} + # payloadTypes=11, a 10-minute window that holds a row heard last before since + - id: walk.window_inv_pt11_10m + ref: {stmt: oracle_window, params: ["2026-10-17 06:58:12.751+00", infinity, "\\x", "2026-10-17 07:08:12.751+00", "{11}", null, 51, null]} + cands: + - {stmt: window, params: ["2026-10-17 06:58:12.751+00", infinity, "\\x", "2026-10-17 07:08:12.751+00", "{11}", "{0,1,2,3}", 51]} + walk: + limit_param: 7 + fetch_extra: 1 + cursor: [{param: 2, column: last_heard_at}, {param: 3, column: packet_hash}] + first: [infinity, '\x'] + web_limit: 50 + max_rows: 2000 + oracle: true + expect: {window: identical} + # scopes=#nl, a one-hour window + - id: walk.window_scoped_nl_1h + ref: {stmt: oracle_window, params: ["2026-10-18 15:00:00+00", infinity, "\\x", "2026-10-18 16:00:00+00", null, null, 51, "{#nl}"]} + cands: + - {stmt: window_scoped, params: ["2026-10-18 15:00:00+00", infinity, "\\x", "2026-10-18 16:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51, "{#nl}"]} + walk: + limit_param: 7 + fetch_extra: 1 + cursor: [{param: 2, column: last_heard_at}, {param: 3, column: packet_hash}] + first: [infinity, '\x'] + web_limit: 50 + max_rows: 2000 + oracle: true + expect: {window_scoped: identical} + # scopes=#SEA, a four-day window + - id: walk.window_scoped_sea_4d + ref: {stmt: oracle_window, params: ["2026-10-14 17:00:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", null, null, 51, "{#SEA}"]} + cands: + - {stmt: window_scoped, params: ["2026-10-14 17:00:00+00", infinity, "\\x", "2026-10-18 17:00:00+00", "{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}", "{0,1,2,3}", 51, "{#SEA}"]} + walk: + limit_param: 7 + fetch_extra: 1 + cursor: [{param: 2, column: last_heard_at}, {param: 3, column: packet_hash}] + first: [infinity, '\x'] + web_limit: 50 + max_rows: 2000 + oracle: true + expect: {window_scoped: identical} diff --git a/db/planguard/overlay/packets_global.sql b/db/planguard/overlay/packets_global.sql new file mode 100644 index 0000000..7c58102 --- /dev/null +++ b/db/planguard/overlay/packets_global.sql @@ -0,0 +1,81 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- Overlay for the local-copy tier of the packets_global group and for diff/packets_global.yaml, built in the schema +-- t_s5a from public: +-- packets with the index set it has once the old single-column indexes are dropped (migrations 025-033 plus +-- the indexes the table keeps); +-- nodes, node_iatas, observers +-- with the index set and fillfactor of migrations 011-024, for a copy taken before them. +-- Statements run with search_path "t_s5a, public", so these tables resolve here and every other table to public. +-- +-- It reads public and writes only inside t_s5a (about 860 MB on a full copy, most of it packets). Remove it with +-- DROP SCHEMA t_s5a CASCADE; +-- Run: psql "$DSN" -v ON_ERROR_STOP=1 -f db/planguard/overlay/packets_global.sql +-- +-- UNLOGGED, and autovacuum is off so the statistics stay as built. parsed_payload is not copied (NULL): it only +-- feeds the projection's summary text, so plans and buffer counts (one heap page per projected packet) are the +-- same, but result rows differ in summary; compare API results against public instead. +\set ON_ERROR_STOP 1 +\timing on +SET maintenance_work_mem = '512MB'; +SET jit = off; +CREATE SCHEMA IF NOT EXISTS t_s5a; +DROP TABLE IF EXISTS t_s5a.packets; +CREATE UNLOGGED TABLE t_s5a.packets (LIKE public.packets INCLUDING DEFAULTS INCLUDING CONSTRAINTS) + WITH (autovacuum_vacuum_scale_factor = 0.05, autovacuum_vacuum_cost_limit = 1000, autovacuum_enabled = false); +INSERT INTO t_s5a.packets +SELECT packet_hash, payload_type, payload_version, route_type, transport_codes_present, region_code, sub_region_code, scope_id, + origin_pubkey, raw_payload, raw_header, NULL::jsonb, decrypted, channel_hash, trace_tag, first_heard_at, last_heard_at, observation_count +FROM public.packets; +ALTER TABLE t_s5a.packets ADD CONSTRAINT packets_pkey PRIMARY KEY (packet_hash); +ALTER TABLE t_s5a.packets ADD CONSTRAINT packets_payload_type_domain CHECK (payload_type BETWEEN 0 AND 15); +ALTER TABLE t_s5a.packets ADD CONSTRAINT packets_route_type_domain CHECK (route_type BETWEEN 0 AND 3); +CREATE INDEX idx_packets_last_heard_hash ON t_s5a.packets (last_heard_at DESC, packet_hash DESC); +CREATE INDEX idx_packets_type_last_heard ON t_s5a.packets (payload_type, route_type, last_heard_at DESC, packet_hash DESC); +CREATE INDEX idx_packets_scope_last_heard ON t_s5a.packets (scope_id, last_heard_at DESC, packet_hash DESC) + INCLUDE (payload_type, route_type, first_heard_at) WHERE scope_id IS NOT NULL; +CREATE INDEX idx_packets_first_heard_brin ON t_s5a.packets USING brin (first_heard_at); +CREATE INDEX idx_packets_origin ON t_s5a.packets (origin_pubkey, first_heard_at DESC) WHERE origin_pubkey IS NOT NULL; +CREATE INDEX idx_packets_channel ON t_s5a.packets (channel_hash, first_heard_at DESC) WHERE channel_hash IS NOT NULL; +CREATE INDEX idx_packets_trace_tag ON t_s5a.packets (trace_tag) WHERE trace_tag IS NOT NULL; +CREATE INDEX idx_packets_heard_inverted ON t_s5a.packets (first_heard_at) + INCLUDE (last_heard_at, packet_hash, payload_type, route_type, scope_id) WHERE (first_heard_at > last_heard_at) IS TRUE; +-- Created before VACUUM ANALYZE so the extended statistics are collected. +CREATE STATISTICS t_s5a.packets_heard_inverted_stat ON ((first_heard_at > last_heard_at)) FROM t_s5a.packets; +VACUUM ANALYZE t_s5a.packets; +-- nodes, node_iatas and observers: every public index and key except the ones migrations 011-024 drop, node_iatas' +-- (iata) index in place of (iata, last_heard DESC), and observers at fillfactor 50. +DO $$ +DECLARE + t record; ix record; c record; + drops text[] := ARRAY['idx_nodes_type_last_seen', 'idx_nodes_pubkey', 'idx_nodes_location', 'idx_nodes_min_firmware', + 'idx_nodes_multibyte_paths', 'idx_nodes_multibyte_traces', 'idx_observers_last_seen', 'idx_observers_pubkey', + 'idx_observers_type', 'idx_node_iatas_iata']; +BEGIN + FOR t IN SELECT * FROM (VALUES ('nodes', 100), ('node_iatas', 100), ('observers', 50)) v(name, ff) LOOP + EXECUTE format('DROP TABLE IF EXISTS t_s5a.%I', t.name); + EXECUTE format('CREATE UNLOGGED TABLE t_s5a.%I (LIKE public.%I INCLUDING DEFAULTS INCLUDING CONSTRAINTS) ' + 'WITH (fillfactor = %s, autovacuum_enabled = false)', t.name, t.name, t.ff); + EXECUTE format('INSERT INTO t_s5a.%I SELECT * FROM public.%I', t.name, t.name); + FOR c IN SELECT conname, pg_get_constraintdef(oid) AS def FROM pg_constraint + WHERE conrelid = format('public.%I', t.name)::regclass AND contype IN ('p', 'u') LOOP + EXECUTE format('ALTER TABLE t_s5a.%I ADD CONSTRAINT %I %s', t.name, c.conname, c.def); + END LOOP; + FOR ix IN SELECT i.indexname, i.indexdef FROM pg_indexes i + WHERE i.schemaname = 'public' AND i.tablename = t.name + AND NOT EXISTS (SELECT 1 FROM pg_constraint k WHERE k.conname = i.indexname + AND k.conrelid = format('public.%I', t.name)::regclass) LOOP + IF ix.indexname = ANY (drops) THEN CONTINUE; END IF; + EXECUTE replace(ix.indexdef, ' ON public.', ' ON t_s5a.'); + END LOOP; + END LOOP; +END $$; +CREATE INDEX idx_node_iatas_iata ON t_s5a.node_iatas (iata); +VACUUM ANALYZE t_s5a.nodes; +VACUUM ANALYZE t_s5a.node_iatas; +VACUUM ANALYZE t_s5a.observers; +SELECT c.relname, pg_size_pretty(pg_relation_size(c.oid)) FROM pg_class c WHERE c.relnamespace = 't_s5a'::regnamespace ORDER BY 1; +SELECT tablename, indexname FROM pg_indexes WHERE schemaname = 't_s5a' ORDER BY 1, 2; +SELECT count(*) AS inverted_rows FROM t_s5a.packets WHERE (first_heard_at > last_heard_at) IS TRUE; +SELECT pg_size_pretty(pg_total_relation_size('t_s5a.packets')) AS t_s5a_packets_total; diff --git a/db/planguard/sql/oracle/OraclePacketsByScopesTypes.sql b/db/planguard/sql/oracle/OraclePacketsByScopesTypes.sql new file mode 100644 index 0000000..bfe1d42 --- /dev/null +++ b/db/planguard/sql/oracle/OraclePacketsByScopesTypes.sql @@ -0,0 +1,20 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- name: OraclePacketsByScopesTypes +-- The global /packets filter as the single ListPackets statement applied it before the per-shape statements +-- (an empty or NULL list, since or until is no filter), in the exact (last_heard_at, packet_hash) order and from an +-- exact row cursor: the truth a cursor walk is compared with. Its params sit where +-- ListPacketsByScopesTypes has its cursor and limit: +-- $1 ts, $2 hash, $3 pt, $4 rt, $5 limit, $6 scopes, $7 since, $8 until. +SELECT p.packet_hash, p.last_heard_at +FROM packets p +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +WHERE (COALESCE(cardinality($3::smallint[]), 0) = 0 OR p.payload_type = ANY($3::smallint[])) + AND (COALESCE(cardinality($4::smallint[]), 0) = 0 OR p.route_type = ANY($4::smallint[])) + AND ($7::timestamptz IS NULL OR p.first_heard_at >= $7::timestamptz) + AND ($8::timestamptz IS NULL OR p.first_heard_at <= $8::timestamptz) + AND (p.last_heard_at, p.packet_hash) < ($1::timestamptz, $2::bytea) + AND (COALESCE(cardinality($6::text[]), 0) = 0 OR ts.name = ANY($6::text[])) +ORDER BY p.last_heard_at DESC, p.packet_hash DESC +LIMIT $5::int diff --git a/db/planguard/sql/oracle/OraclePacketsByTypes.sql b/db/planguard/sql/oracle/OraclePacketsByTypes.sql new file mode 100644 index 0000000..d27f0c7 --- /dev/null +++ b/db/planguard/sql/oracle/OraclePacketsByTypes.sql @@ -0,0 +1,20 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- name: OraclePacketsByTypes +-- The global /packets filter as the single ListPackets statement applied it before the per-shape statements +-- (an empty or NULL list, since or until is no filter), in the exact (last_heard_at, packet_hash) order and from an +-- exact row cursor: the truth a cursor walk is compared with. Its params sit where +-- ListPacketsByTypes has its cursor and limit: +-- $1 pt, $2 rt, $3 ts, $4 hash, $5 limit, $6 scopes, $7 since, $8 until. +SELECT p.packet_hash, p.last_heard_at +FROM packets p +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +WHERE (COALESCE(cardinality($1::smallint[]), 0) = 0 OR p.payload_type = ANY($1::smallint[])) + AND (COALESCE(cardinality($2::smallint[]), 0) = 0 OR p.route_type = ANY($2::smallint[])) + AND ($7::timestamptz IS NULL OR p.first_heard_at >= $7::timestamptz) + AND ($8::timestamptz IS NULL OR p.first_heard_at <= $8::timestamptz) + AND (p.last_heard_at, p.packet_hash) < ($3::timestamptz, $4::bytea) + AND (COALESCE(cardinality($6::text[]), 0) = 0 OR ts.name = ANY($6::text[])) +ORDER BY p.last_heard_at DESC, p.packet_hash DESC +LIMIT $5::int diff --git a/db/planguard/sql/oracle/OraclePacketsPage.sql b/db/planguard/sql/oracle/OraclePacketsPage.sql new file mode 100644 index 0000000..ee3ccdd --- /dev/null +++ b/db/planguard/sql/oracle/OraclePacketsPage.sql @@ -0,0 +1,20 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- name: OraclePacketsPage +-- The global /packets filter as the single ListPackets statement applied it before the per-shape statements +-- (an empty or NULL list, since or until is no filter), in the exact (last_heard_at, packet_hash) order and from an +-- exact row cursor: the truth a cursor walk is compared with. Its params sit where +-- ListPacketsPage and ListPacketsByScopes have their cursor and limit: +-- $1 ts, $2 hash, $3 limit, $4 scopes, $5 pt, $6 rt, $7 since, $8 until. +SELECT p.packet_hash, p.last_heard_at +FROM packets p +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +WHERE (COALESCE(cardinality($5::smallint[]), 0) = 0 OR p.payload_type = ANY($5::smallint[])) + AND (COALESCE(cardinality($6::smallint[]), 0) = 0 OR p.route_type = ANY($6::smallint[])) + AND ($7::timestamptz IS NULL OR p.first_heard_at >= $7::timestamptz) + AND ($8::timestamptz IS NULL OR p.first_heard_at <= $8::timestamptz) + AND (p.last_heard_at, p.packet_hash) < ($1::timestamptz, $2::bytea) + AND (COALESCE(cardinality($4::text[]), 0) = 0 OR ts.name = ANY($4::text[])) +ORDER BY p.last_heard_at DESC, p.packet_hash DESC +LIMIT $3::int diff --git a/db/planguard/sql/oracle/OraclePacketsWindow.sql b/db/planguard/sql/oracle/OraclePacketsWindow.sql new file mode 100644 index 0000000..d7e50c2 --- /dev/null +++ b/db/planguard/sql/oracle/OraclePacketsWindow.sql @@ -0,0 +1,20 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- name: OraclePacketsWindow +-- The global /packets filter as the single ListPackets statement applied it before the per-shape statements +-- (an empty or NULL list, since or until is no filter), in the exact (last_heard_at, packet_hash) order and from an +-- exact row cursor: the truth a cursor walk is compared with. Its params sit where +-- ListPacketsWindow and ListPacketsWindowScoped have their cursor and limit: +-- $1 since, $2 ts, $3 hash, $4 until, $5 pt, $6 rt, $7 limit, $8 scopes. +SELECT p.packet_hash, p.last_heard_at +FROM packets p +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +WHERE (COALESCE(cardinality($5::smallint[]), 0) = 0 OR p.payload_type = ANY($5::smallint[])) + AND (COALESCE(cardinality($6::smallint[]), 0) = 0 OR p.route_type = ANY($6::smallint[])) + AND ($1::timestamptz IS NULL OR p.first_heard_at >= $1::timestamptz) + AND ($4::timestamptz IS NULL OR p.first_heard_at <= $4::timestamptz) + AND (p.last_heard_at, p.packet_hash) < ($2::timestamptz, $3::bytea) + AND (COALESCE(cardinality($8::text[]), 0) = 0 OR ts.name = ANY($8::text[])) +ORDER BY p.last_heard_at DESC, p.packet_hash DESC +LIMIT $7::int diff --git a/db/planguard/sql/pending/ListNodes.sql b/db/planguard/sql/pending/ListNodes.sql deleted file mode 100644 index 1d0e462..0000000 --- a/db/planguard/sql/pending/ListNodes.sql +++ /dev/null @@ -1,42 +0,0 @@ --- Copyright 2026 Beacon Contributors --- SPDX-License-Identifier: AGPL-3.0-or-later - --- name: ListNodes :many -WITH page AS ( -SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_seen, - n.radio_freq_mhz, n.radio_sf, n.radio_bw_khz, ts.name AS default_scope_name -FROM nodes n -LEFT JOIN transport_scopes ts ON ts.id = n.default_scope_id -WHERE - ($1 = 0 OR n.node_type = $1) - AND (COALESCE(cardinality($2::bpchar[]), 0) = 0 OR n.id IN (SELECT node_id FROM node_iatas WHERE iata = ANY($2::bpchar[]))) - AND ( - $3::text = 'any' - OR ($3::text = 'true' AND n.supports_multibyte_paths = TRUE) - OR ($3::text = 'false' AND n.supports_multibyte_paths = FALSE) - ) - AND ( - $4::text = 'any' - OR ($4::text = 'true' AND n.supports_multibyte_traces = TRUE) - OR ($4::text = 'false' AND n.supports_multibyte_traces = FALSE) - ) - AND ($5::bytea IS NULL OR n.public_key = $5) - AND ($6 = '' OR n.name ILIKE '%' || $6 || '%') - AND (n.last_seen, n.id) < ($7::timestamptz, $12::uuid) - AND ($9::text = '' OR ts.name = $9::text) - AND ($11::text = '' OR encode(n.public_key, 'hex') ILIKE $11 || '%') -ORDER BY n.last_seen DESC, n.id DESC -LIMIT $8 -) -SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_seen, - n.radio_freq_mhz, n.radio_sf, n.radio_bw_khz, n.default_scope_name, - (SELECT json_agg(json_build_object('iata', ni.iata, 'lastHeard', (extract(epoch from ni.last_heard) * 1000)::bigint) ORDER BY ni.last_heard DESC) - FROM node_iatas ni WHERE ni.node_id = n.id) AS iatas, - EXISTS (SELECT 1 FROM observers o WHERE o.public_key = n.public_key) AS is_observer, - (SELECT o.id FROM observers o WHERE o.public_key = n.public_key LIMIT 1) AS observer_id, - (SELECT COUNT(DISTINCT nn.neighbor_id) FROM node_neighbors nn WHERE nn.node_id = n.id)::bigint AS known_neighbor_count, - (CASE WHEN $10::bool THEN - (SELECT COALESCE(array_agg(DISTINCT nn.neighbor_id), '{}'::uuid[]) FROM node_neighbors nn WHERE nn.node_id = n.id) - ELSE NULL END)::uuid[] AS neighbor_ids -FROM page n -ORDER BY n.last_seen DESC, n.id DESC diff --git a/db/planguard/sql/pending/ListObservers.sql b/db/planguard/sql/pending/ListObservers.sql deleted file mode 100644 index 7dc6bfe..0000000 --- a/db/planguard/sql/pending/ListObservers.sql +++ /dev/null @@ -1,41 +0,0 @@ --- Copyright 2026 Beacon Contributors --- SPDX-License-Identifier: AGPL-3.0-or-later - --- name: ListObservers :many -SELECT - o.id, - o.display_name, - o.observer_type, - o.last_status_at, - o.radio_freq_mhz, - o.radio_sf, - o.radio_bw_khz, - array_remove(array_agg(DISTINCT ts.name ORDER BY ts.name), NULL)::text[] AS scopes, -COALESCE(CASE - WHEN GREATEST(COALESCE(o.last_status_at, o.last_seen), o.last_seen) > NOW() - INTERVAL '5 minutes' THEN 'online' - ELSE 'offline' -END, 'offline')::text AS status, -COALESCE(o.last_iata, '')::text AS iata, -o.last_seen -FROM observers o -LEFT JOIN observer_brokers ob ON ob.observer_id = o.id -LEFT JOIN observer_scopes os ON os.observer_id = o.id -LEFT JOIN transport_scopes ts ON ts.id = os.scope_id -WHERE - (COALESCE(cardinality($1::bpchar[]), 0) = 0 OR o.last_iata = ANY($1::bpchar[])) - AND ($2 = '' OR o.observer_type = $2) - AND ($3 = '' OR ob.broker_name = $3) - AND ($4 = '' OR CASE - WHEN GREATEST(COALESCE(o.last_status_at, o.last_seen), o.last_seen) > NOW() - INTERVAL '5 minutes' THEN 'online' - ELSE 'offline' - END = $4) - AND ($5 = '' OR o.display_name ILIKE '%' || $5 || '%') - AND (o.last_seen, o.id) < ($6::timestamptz, $9::uuid) - AND ($8::text = '' OR EXISTS ( - SELECT 1 FROM observer_scopes os2 - JOIN transport_scopes ts2 ON ts2.id = os2.scope_id - WHERE os2.observer_id = o.id AND ts2.name = $8::text - )) -GROUP BY o.id -ORDER BY o.last_seen DESC, o.id DESC -LIMIT $7 diff --git a/db/planguard/sql/pending/ListPacketsByScopes.sql b/db/planguard/sql/pending/ListPacketsByScopes.sql deleted file mode 100644 index 85424b4..0000000 --- a/db/planguard/sql/pending/ListPacketsByScopes.sql +++ /dev/null @@ -1,48 +0,0 @@ --- Copyright 2026 Beacon Contributors --- SPDX-License-Identifier: AGPL-3.0-or-later - --- name: ListPacketsByScopes :many -WITH page AS MATERIALIZED ( - SELECT x.packet_hash, x.last_heard_at AS k - FROM transport_scopes s - CROSS JOIN LATERAL ( - SELECT p.packet_hash, p.last_heard_at FROM packets p - WHERE p.scope_id = s.id - AND (p.last_heard_at, p.packet_hash) < ($1::timestamptz, $2::bytea) - ORDER BY p.last_heard_at DESC, p.packet_hash DESC - LIMIT $3) x - WHERE s.name = ANY($4::text[]) - ORDER BY x.last_heard_at DESC, x.packet_hash DESC - LIMIT $3) -SELECT p.packet_hash, p.payload_type, - COALESCE(CASE - WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' - THEN p.parsed_payload #>> '{appData,name}' - WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' - AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' - AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' - THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') - WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') - AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' - AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' - THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') - END, '')::text AS summary, - p.route_type, - p.first_heard_at, p.last_heard_at, p.scope_id, ts.name AS scope_name, - p.observation_count AS observation_count, - po.observer_id AS latest_observer_id, o.display_name AS latest_observer_name, - COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, - COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, - COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, - COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, - po.path_bytes AS latest_observer_path_bytes, p.raw_payload, p.origin_pubkey -FROM page pg -JOIN packets p ON p.packet_hash = pg.packet_hash -LEFT JOIN LATERAL ( - SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes - FROM packet_observations WHERE packet_hash = p.packet_hash - ORDER BY heard_at DESC, id DESC - LIMIT 1) po ON true -LEFT JOIN observers o ON o.id = po.observer_id -LEFT JOIN transport_scopes ts ON ts.id = p.scope_id -ORDER BY pg.k DESC, pg.packet_hash DESC diff --git a/db/planguard/sql/pending/ListPacketsByScopesTypes.sql b/db/planguard/sql/pending/ListPacketsByScopesTypes.sql deleted file mode 100644 index a44c867..0000000 --- a/db/planguard/sql/pending/ListPacketsByScopesTypes.sql +++ /dev/null @@ -1,49 +0,0 @@ --- Copyright 2026 Beacon Contributors --- SPDX-License-Identifier: AGPL-3.0-or-later - --- name: ListPacketsByScopesTypes :many -WITH page AS MATERIALIZED ( - SELECT x.packet_hash, x.last_heard_at AS k - FROM transport_scopes s - CROSS JOIN LATERAL ( - SELECT p.packet_hash, p.last_heard_at FROM packets p - WHERE p.scope_id = s.id - AND (p.last_heard_at, p.packet_hash) < ($1::timestamptz, $2::bytea) - AND p.payload_type = ANY($5::smallint[]) AND p.route_type = ANY($6::smallint[]) - ORDER BY p.last_heard_at DESC, p.packet_hash DESC - LIMIT $3) x - WHERE s.name = ANY($4::text[]) - ORDER BY x.last_heard_at DESC, x.packet_hash DESC - LIMIT $3) -SELECT p.packet_hash, p.payload_type, - COALESCE(CASE - WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' - THEN p.parsed_payload #>> '{appData,name}' - WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' - AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' - AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' - THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') - WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') - AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' - AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' - THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') - END, '')::text AS summary, - p.route_type, - p.first_heard_at, p.last_heard_at, p.scope_id, ts.name AS scope_name, - p.observation_count AS observation_count, - po.observer_id AS latest_observer_id, o.display_name AS latest_observer_name, - COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, - COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, - COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, - COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, - po.path_bytes AS latest_observer_path_bytes, p.raw_payload, p.origin_pubkey -FROM page pg -JOIN packets p ON p.packet_hash = pg.packet_hash -LEFT JOIN LATERAL ( - SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes - FROM packet_observations WHERE packet_hash = p.packet_hash - ORDER BY heard_at DESC, id DESC - LIMIT 1) po ON true -LEFT JOIN observers o ON o.id = po.observer_id -LEFT JOIN transport_scopes ts ON ts.id = p.scope_id -ORDER BY pg.k DESC, pg.packet_hash DESC diff --git a/db/planguard/sql/pending/ListPacketsByTypes.sql b/db/planguard/sql/pending/ListPacketsByTypes.sql deleted file mode 100644 index cf64167..0000000 --- a/db/planguard/sql/pending/ListPacketsByTypes.sql +++ /dev/null @@ -1,48 +0,0 @@ --- Copyright 2026 Beacon Contributors --- SPDX-License-Identifier: AGPL-3.0-or-later - --- name: ListPacketsByTypes :many -WITH page AS MATERIALIZED ( - SELECT x.packet_hash, x.last_heard_at AS k - FROM (SELECT DISTINCT unnest($4::smallint[])) AS pt(t) - CROSS JOIN (SELECT DISTINCT unnest($5::smallint[])) AS rt(t) - CROSS JOIN LATERAL ( - SELECT p.packet_hash, p.last_heard_at FROM packets p - WHERE p.payload_type = pt.t AND p.route_type = rt.t - AND (p.last_heard_at, p.packet_hash) < ($1::timestamptz, $2::bytea) - ORDER BY p.last_heard_at DESC, p.packet_hash DESC - LIMIT $3) x - ORDER BY x.last_heard_at DESC, x.packet_hash DESC - LIMIT $3) -SELECT p.packet_hash, p.payload_type, - COALESCE(CASE - WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' - THEN p.parsed_payload #>> '{appData,name}' - WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' - AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' - AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' - THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') - WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') - AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' - AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' - THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') - END, '')::text AS summary, - p.route_type, - p.first_heard_at, p.last_heard_at, p.scope_id, ts.name AS scope_name, - p.observation_count AS observation_count, - po.observer_id AS latest_observer_id, o.display_name AS latest_observer_name, - COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, - COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, - COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, - COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, - po.path_bytes AS latest_observer_path_bytes, p.raw_payload, p.origin_pubkey -FROM page pg -JOIN packets p ON p.packet_hash = pg.packet_hash -LEFT JOIN LATERAL ( - SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes - FROM packet_observations WHERE packet_hash = p.packet_hash - ORDER BY heard_at DESC, id DESC - LIMIT 1) po ON true -LEFT JOIN observers o ON o.id = po.observer_id -LEFT JOIN transport_scopes ts ON ts.id = p.scope_id -ORDER BY pg.k DESC, pg.packet_hash DESC diff --git a/db/planguard/sql/pending/ListPacketsPage.sql b/db/planguard/sql/pending/ListPacketsPage.sql deleted file mode 100644 index 1921717..0000000 --- a/db/planguard/sql/pending/ListPacketsPage.sql +++ /dev/null @@ -1,41 +0,0 @@ --- Copyright 2026 Beacon Contributors --- SPDX-License-Identifier: AGPL-3.0-or-later - --- name: ListPacketsPage :many -WITH page AS MATERIALIZED ( - SELECT p.packet_hash, p.last_heard_at AS k FROM packets p - WHERE (p.last_heard_at, p.packet_hash) < ($1::timestamptz, $2::bytea) - ORDER BY p.last_heard_at DESC, p.packet_hash DESC - LIMIT $3) -SELECT p.packet_hash, p.payload_type, - COALESCE(CASE - WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' - THEN p.parsed_payload #>> '{appData,name}' - WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' - AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' - AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' - THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') - WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') - AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' - AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' - THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') - END, '')::text AS summary, - p.route_type, - p.first_heard_at, p.last_heard_at, p.scope_id, ts.name AS scope_name, - p.observation_count AS observation_count, - po.observer_id AS latest_observer_id, o.display_name AS latest_observer_name, - COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, - COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, - COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, - COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, - po.path_bytes AS latest_observer_path_bytes, p.raw_payload, p.origin_pubkey -FROM page pg -JOIN packets p ON p.packet_hash = pg.packet_hash -LEFT JOIN LATERAL ( - SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes - FROM packet_observations WHERE packet_hash = p.packet_hash - ORDER BY heard_at DESC, id DESC - LIMIT 1) po ON true -LEFT JOIN observers o ON o.id = po.observer_id -LEFT JOIN transport_scopes ts ON ts.id = p.scope_id -ORDER BY pg.k DESC, pg.packet_hash DESC diff --git a/db/planguard/sql/pending/ListPacketsWindow.sql b/db/planguard/sql/pending/ListPacketsWindow.sql deleted file mode 100644 index e1d42d8..0000000 --- a/db/planguard/sql/pending/ListPacketsWindow.sql +++ /dev/null @@ -1,55 +0,0 @@ --- Copyright 2026 Beacon Contributors --- SPDX-License-Identifier: AGPL-3.0-or-later - --- name: ListPacketsWindow :many -WITH page AS MATERIALIZED ( - SELECT u.packet_hash, u.k FROM ( - (SELECT p.packet_hash, p.last_heard_at AS k FROM packets p - WHERE p.last_heard_at >= $6::timestamptz - AND (p.last_heard_at, p.packet_hash) < ($1::timestamptz, $2::bytea) - AND (p.first_heard_at >= $6::timestamptz AND p.first_heard_at <= $7::timestamptz) IS TRUE - AND p.payload_type = ANY($4::smallint[]) AND p.route_type = ANY($5::smallint[]) - ORDER BY p.last_heard_at DESC, p.packet_hash DESC - LIMIT $3) - UNION ALL - (SELECT p.packet_hash, p.last_heard_at AS k FROM packets p - WHERE (p.first_heard_at > p.last_heard_at) IS TRUE - AND (p.first_heard_at >= $6::timestamptz AND p.first_heard_at <= $7::timestamptz - AND p.last_heard_at < $6::timestamptz - AND (p.last_heard_at, p.packet_hash) < ($1::timestamptz, $2::bytea) - AND p.payload_type = ANY($4::smallint[]) AND p.route_type = ANY($5::smallint[])) IS TRUE) - ) u - ORDER BY u.k DESC, u.packet_hash DESC - LIMIT $3) -SELECT p.packet_hash, p.payload_type, - COALESCE(CASE - WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' - THEN p.parsed_payload #>> '{appData,name}' - WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' - AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' - AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' - THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') - WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') - AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' - AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' - THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') - END, '')::text AS summary, - p.route_type, - p.first_heard_at, p.last_heard_at, p.scope_id, ts.name AS scope_name, - p.observation_count AS observation_count, - po.observer_id AS latest_observer_id, o.display_name AS latest_observer_name, - COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, - COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, - COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, - COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, - po.path_bytes AS latest_observer_path_bytes, p.raw_payload, p.origin_pubkey -FROM page pg -JOIN packets p ON p.packet_hash = pg.packet_hash -LEFT JOIN LATERAL ( - SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes - FROM packet_observations WHERE packet_hash = p.packet_hash - ORDER BY heard_at DESC, id DESC - LIMIT 1) po ON true -LEFT JOIN observers o ON o.id = po.observer_id -LEFT JOIN transport_scopes ts ON ts.id = p.scope_id -ORDER BY pg.k DESC, pg.packet_hash DESC diff --git a/db/planguard/sql/pending/ListPacketsWindowScoped.sql b/db/planguard/sql/pending/ListPacketsWindowScoped.sql deleted file mode 100644 index e0463cb..0000000 --- a/db/planguard/sql/pending/ListPacketsWindowScoped.sql +++ /dev/null @@ -1,61 +0,0 @@ --- Copyright 2026 Beacon Contributors --- SPDX-License-Identifier: AGPL-3.0-or-later - --- name: ListPacketsWindowScoped :many -WITH page AS MATERIALIZED ( - SELECT u.packet_hash, u.k FROM ( - (SELECT x.packet_hash, x.last_heard_at AS k - FROM transport_scopes s - CROSS JOIN LATERAL ( - SELECT p.packet_hash, p.last_heard_at FROM packets p - WHERE p.scope_id = s.id - AND p.last_heard_at >= $7::timestamptz - AND (p.last_heard_at, p.packet_hash) < ($1::timestamptz, $2::bytea) - AND (p.first_heard_at >= $7::timestamptz AND p.first_heard_at <= $8::timestamptz) IS TRUE - AND p.payload_type = ANY($4::smallint[]) AND p.route_type = ANY($5::smallint[]) - ORDER BY p.last_heard_at DESC, p.packet_hash DESC - LIMIT $3) x - WHERE s.name = ANY($6::text[])) - UNION ALL - (SELECT p.packet_hash, p.last_heard_at AS k FROM packets p - WHERE (p.first_heard_at > p.last_heard_at) IS TRUE - AND (p.first_heard_at >= $7::timestamptz AND p.first_heard_at <= $8::timestamptz - AND p.last_heard_at < $7::timestamptz - AND (p.last_heard_at, p.packet_hash) < ($1::timestamptz, $2::bytea) - AND p.payload_type = ANY($4::smallint[]) AND p.route_type = ANY($5::smallint[]) - AND p.scope_id = ANY(ARRAY(SELECT id FROM transport_scopes WHERE name = ANY($6::text[])))) IS TRUE) - ) u - ORDER BY u.k DESC, u.packet_hash DESC - LIMIT $3) -SELECT p.packet_hash, p.payload_type, - COALESCE(CASE - WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' - THEN p.parsed_payload #>> '{appData,name}' - WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' - AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' - AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' - THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') - WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') - AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' - AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' - THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') - END, '')::text AS summary, - p.route_type, - p.first_heard_at, p.last_heard_at, p.scope_id, ts.name AS scope_name, - p.observation_count AS observation_count, - po.observer_id AS latest_observer_id, o.display_name AS latest_observer_name, - COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, - COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, - COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, - COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, - po.path_bytes AS latest_observer_path_bytes, p.raw_payload, p.origin_pubkey -FROM page pg -JOIN packets p ON p.packet_hash = pg.packet_hash -LEFT JOIN LATERAL ( - SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes - FROM packet_observations WHERE packet_hash = p.packet_hash - ORDER BY heard_at DESC, id DESC - LIMIT 1) po ON true -LEFT JOIN observers o ON o.id = po.observer_id -LEFT JOIN transport_scopes ts ON ts.id = p.scope_id -ORDER BY pg.k DESC, pg.packet_hash DESC diff --git a/db/planguard/storediff/packets_global.yaml b/db/planguard/storediff/packets_global.yaml new file mode 100644 index 0000000..a9262f3 --- /dev/null +++ b/db/planguard/storediff/packets_global.yaml @@ -0,0 +1,109 @@ +# Copyright 2026 Beacon Contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +# Global /packets pages and the /nodes and /observers keysets: two read-only servers on one database (the build +# before the per-shape packets statements as old, this build as new), walked page by page with the numeric cursor +# a client of either build follows, and compared as whole JSON. +# +# planguard storediff --old http://127.0.0.1:8481 --new http://127.0.0.1:8482 \ +# --cases db/planguard/storediff/packets_global.yaml --walk 1,2,3,web +# +# Both servers read public (not the overlay: its packets have no parsed_payload, so summary would differ). +# +# Allowed differences, each a named class: nextPageCursor is new on every page (additive_keys); rows tied on their +# sort key come out in a fixed order (tie_order, keyed by the item's time field); the latest observer of a packet +# whose newest observations share heard_at is the highest observation id (latest_observer_tie). Node and observer +# items carry no time field, so their ties cannot be keyed and any reorder reads as a difference. +# +# /observers: nextCursor now comes from last_seen, the order column, and is present whenever hasMore; it came from +# last_status_at and was absent when that was NULL. So page 1 differs at $.nextCursor by design and the case +# compares page 1 only: its one expected difference is "page 1: $.nextCursor". +# +# since and until are epoch milliseconds around the copy's snapshot time T0 = 2026-10-18 17:00 UTC. +cases: + - id: packets + path: /api/v1/packets + follow: &cursor + - {param: cursor, from: nextCursor} + allowed: &packets [tie_order, additive_keys, latest_observer_tie] + key: &heard [lastHeardAt] + omit: &observer [latestObserver] + - id: packets_pt4 + path: /api/v1/packets + query: {payloadTypes: "4"} + follow: *cursor + allowed: *packets + key: *heard + omit: *observer + - id: packets_rt0_window + # The pathfinder's request: route type 0 over the 32 minutes before T0. + path: /api/v1/packets + query: {routeTypes: "0", since: "1792340880000", until: "1792342800000"} + follow: *cursor + allowed: *packets + key: *heard + omit: *observer + - id: packets_scope_nl + path: /api/v1/packets + query: {scopes: "#nl"} + follow: *cursor + allowed: *packets + key: *heard + omit: *observer + - id: packets_dashboard + path: /api/v1/packets + query: {scopes: "#pnw,#wa,#w-wa,#thurston,#pierce,#oly,#rainier,#tumwater", payloadTypes: "4"} + follow: *cursor + allowed: *packets + key: *heard + omit: *observer + - id: packets_scope_grr + # A scope no packet carries: an empty page. + path: /api/v1/packets + query: {scope: "#GRR"} + follow: *cursor + allowed: *packets + key: *heard + omit: *observer + - id: packets_iata_sea + # The region path: unchanged, and it emits no nextPageCursor. + path: /api/v1/packets + query: {iatas: SEA} + follow: *cursor + allowed: *packets + key: *heard + omit: *observer + - id: packets_hash_prefix + # The hash prefix search: unchanged, and it emits no nextPageCursor. + path: /api/v1/packets + query: {hashPrefix: ab12} + follow: *cursor + allowed: *packets + key: *heard + omit: *observer + - id: nodes + path: /api/v1/nodes + web_limit: 200 + follow: *cursor + allowed: &additive [additive_keys] + - id: nodes_iata_sea + path: /api/v1/nodes + query: {iatas: SEA} + web_limit: 200 + follow: *cursor + allowed: *additive + - id: nodes_type2 + path: /api/v1/nodes + query: {type: "2"} + web_limit: 200 + follow: *cursor + allowed: *additive + - id: observers + path: /api/v1/observers + pages: 1 + allowed: *additive + - id: observers_online + path: /api/v1/observers + query: {status: online} + pages: 1 + allowed: *additive diff --git a/db/planguard/storediff/smoke.yaml b/db/planguard/storediff/smoke.yaml index 18e52e9..a7ce43e 100644 --- a/db/planguard/storediff/smoke.yaml +++ b/db/planguard/storediff/smoke.yaml @@ -8,6 +8,12 @@ # # /routes answers a bare array: it has more while a page is full, and the next page starts at the last route's # lastSeen and id. +# +# /packets, /nodes and /observers add nextPageCursor to every page (additive_keys). /packets also orders rows tied on +# lastHeardAt by hash (tie_order) and picks the highest observation id as the latest observer on a heard_at tie +# (latest_observer_tie). /observers takes nextCursor from last_seen now, so its first page differs at $.nextCursor +# by design; storediff/packets_global.yaml holds the full set of these cases. The nodes_type cases filter, so their +# walks reach the last page at the web limits 50 and 3. cases: - id: routes_sea path: /api/v1/routes @@ -24,14 +30,41 @@ cases: path: /api/v1/nodes follow: - {param: cursor, from: nextCursor} + allowed: [additive_keys] + - id: nodes_type1 + path: /api/v1/nodes + query: {type: "1"} + pages: 500 + follow: + - {param: cursor, from: nextCursor} + allowed: [additive_keys] + - id: nodes_type3 + path: /api/v1/nodes + query: {type: "3"} + pages: 100 + follow: + - {param: cursor, from: nextCursor} + allowed: [additive_keys] + - id: nodes_type3_l3 + path: /api/v1/nodes + query: {type: "3"} + web_limit: 3 + pages: 1200 + follow: + - {param: cursor, from: nextCursor} + allowed: [additive_keys] - id: observers path: /api/v1/observers follow: - {param: cursor, from: nextCursor} + allowed: [additive_keys] - id: packets path: /api/v1/packets follow: - {param: cursor, from: nextCursor} + allowed: [tie_order, additive_keys, latest_observer_tie] + key: [lastHeardAt] + omit: [latestObserver] - id: channel_messages # The channel heard most recently: /channels is ordered by last seen. path: /api/v1/channels/{channel}/messages diff --git a/db/planpolicy/cached_gen.go b/db/planpolicy/cached_gen.go index b97291d..9ab25ea 100644 --- a/db/planpolicy/cached_gen.go +++ b/db/planpolicy/cached_gen.go @@ -10,4 +10,6 @@ var Cached = map[string]bool{ "GetNodeByID": true, "ListKnownRoutesByIATA": true, "ListKnownRoutesGlobal": true, + "ListPacketsByTypes": true, + "ListPacketsPage": true, } diff --git a/db/planpolicy/planpolicy.go b/db/planpolicy/planpolicy.go index 7736baa..21a807e 100644 --- a/db/planpolicy/planpolicy.go +++ b/db/planpolicy/planpolicy.go @@ -46,7 +46,6 @@ const ( // Legacy lists the statements not yet rewritten. Each is removed in the change that replaces it. var Legacy = map[string]bool{ - "ListPackets": true, "ListPacketsByIATAs": true, "ListChannelMessages": true, "ListAllChannelMessages": true, diff --git a/db/planpolicy/planpolicy_test.go b/db/planpolicy/planpolicy_test.go index 333bdd4..a02b228 100644 --- a/db/planpolicy/planpolicy_test.go +++ b/db/planpolicy/planpolicy_test.go @@ -24,7 +24,7 @@ func TestPlanPolicyNamesExist(t *testing.T) { } } } - if len(planpolicy.Legacy) != 10 || planpolicy.ClassOf("ListPackets") != planpolicy.ClassLegacy || + if len(planpolicy.Legacy) != 9 || planpolicy.ClassOf("ListPacketsByIATAs") != planpolicy.ClassLegacy || planpolicy.ClassOf("ListKnownRoutesGlobalHop") != planpolicy.ClassC || planpolicy.ClassOf("GetNodeByID") != planpolicy.ClassG || planpolicy.ClassOf("") != planpolicy.ClassC { t.Fatal("ClassOf") diff --git a/db/planrouter_test.go b/db/planrouter_test.go index f520e19..e570e7f 100644 --- a/db/planrouter_test.go +++ b/db/planrouter_test.go @@ -270,7 +270,7 @@ func runVia(ctx context.Context, r *PlanRouter, method, sql string, args ...any) func TestPlanRouterRoutesByClass(t *testing.T) { classOf := func(name string) planpolicy.Class { - return map[string]planpolicy.Class{"ListPackets": planpolicy.ClassLegacy, "ProbeG": planpolicy.ClassG}[name] // else ClassC + return map[string]planpolicy.Class{"ListPacketsByIATAs": planpolicy.ClassLegacy, "ProbeG": planpolicy.ClassG}[name] // else ClassC } for _, c := range []struct { layout planpolicy.Layout @@ -278,13 +278,13 @@ func TestPlanRouterRoutesByClass(t *testing.T) { describe bool admits int }{ - {planpolicy.CustomAll, "ListPackets", PoolAPILegacy, false, 0}, + {planpolicy.CustomAll, "ListPacketsByIATAs", PoolAPILegacy, false, 0}, {planpolicy.CustomAll, "ProbeG", PoolAPICustom, false, 1}, {planpolicy.CustomAll, "ProbeC", PoolAPICustom, true, 1}, - {planpolicy.Split, "ListPackets", PoolAPILegacy, false, 0}, + {planpolicy.Split, "ListPacketsByIATAs", PoolAPILegacy, false, 0}, {planpolicy.Split, "ProbeG", PoolAPICached, false, 1}, {planpolicy.Split, "ProbeC", PoolAPICustom, true, 1}, - {planpolicy.Single, "ListPackets", PoolAPILegacy, false, 0}, + {planpolicy.Single, "ListPacketsByIATAs", PoolAPILegacy, false, 0}, {planpolicy.Single, "ProbeG", PoolAPILegacy, false, 1}, {planpolicy.Single, "ProbeC", PoolAPILegacy, true, 1}, } { @@ -324,7 +324,7 @@ func TestPlanRouterRoutesByClass(t *testing.T) { // A class C statement never reaches api_cached, and nothing does outside split. for _, layout := range []planpolicy.Layout{planpolicy.CustomAll, planpolicy.Split, planpolicy.Single} { f := newRouterFixture(layout, nil) // the real policy - for _, stmt := range []string{"ListPackets", "ListChannels", "ListKnownRoutesByIATAHop", "ListKnownRoutesGlobalHop", ""} { + for _, stmt := range []string{"ListPacketsByIATAs", "ListChannels", "ListKnownRoutesByIATAHop", "ListKnownRoutesGlobalHop", ""} { sql := "SELECT 1" if stmt != "" { sql = "-- name: " + stmt + " :one\nSELECT 1" @@ -403,7 +403,7 @@ func TestPlanRouterRoutesByClass(t *testing.T) { {planpolicy.Single, &Pools{APILegacy: unconnected()}}, } { r := NewPlanRouter(tc.pools, tc.layout, nil) - for _, stmt := range []string{"ListPackets", "GetNodeByID"} { + for _, stmt := range []string{"ListPacketsByIATAs", "GetNodeByID"} { for _, method := range routerMethods { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) err := runVia(ctx, r, method, "-- name: "+stmt+" :one\nSELECT 1") @@ -544,7 +544,7 @@ func TestLegacyStatementNeverCut(t *testing.T) { r := newPlanRouter(legacy, custom, nil, planpolicy.CustomAll, g, nil) start := time.Now() - if err := runVia(ctx, r, "Query", "-- name: ListPackets :many\nSELECT 1"); err != nil { + if err := runVia(ctx, r, "Query", "-- name: ListPacketsByIATAs :many\nSELECT 1"); err != nil { t.Fatalf("legacy statement: %v", err) } if d := time.Since(start); d != 8*time.Second { diff --git a/db/queries/queries.sql b/db/queries/queries.sql index 74422dc..8c415de 100644 --- a/db/queries/queries.sql +++ b/db/queries/queries.sql @@ -171,8 +171,7 @@ WHERE observer_id = $1 ORDER BY last_seen DESC; -- name: ListObservers :many --- Pass cursor=0 to start from the beginning, or the last seen observer's rownum for pagination. --- Note: observers use UUID PKs so we order by last_seen and use a keyset on last_seen+id. +-- Observers newest last_seen first; the cursor is the (last_seen, id) of the last row. SELECT o.id, o.display_name, @@ -186,7 +185,8 @@ COALESCE(CASE WHEN GREATEST(COALESCE(o.last_status_at, o.last_seen), o.last_seen) > NOW() - INTERVAL '5 minutes' THEN 'online' ELSE 'offline' END, 'offline')::text AS status, -COALESCE(o.last_iata, '')::text AS iata +COALESCE(o.last_iata, '')::text AS iata, +o.last_seen FROM observers o LEFT JOIN observer_brokers ob ON ob.observer_id = o.id LEFT JOIN observer_scopes os ON os.observer_id = o.id @@ -200,14 +200,14 @@ WHERE ELSE 'offline' END = $4) AND ($5 = '' OR o.display_name ILIKE '%' || $5 || '%') - AND ($6::timestamptz IS NULL OR o.last_seen < $6) + AND (o.last_seen, o.id) < ($6::timestamptz, $9::uuid) AND ($8::text = '' OR EXISTS ( SELECT 1 FROM observer_scopes os2 JOIN transport_scopes ts2 ON ts2.id = os2.scope_id WHERE os2.observer_id = o.id AND ts2.name = $8::text )) GROUP BY o.id -ORDER BY o.last_seen DESC +ORDER BY o.last_seen DESC, o.id DESC LIMIT $7; -- name: GetObserverLastIATA :one @@ -473,10 +473,14 @@ LEFT JOIN transport_scopes ts ON ts.id = p.scope_id WHERE p.trace_tag = decode($1, 'hex') ORDER BY p.first_heard_at ASC; --- name: ListPackets :many --- Returns packets with the latest observation rolled in for display. --- Pass cursor=0 to start from the beginning. IATA-filtered requests are --- served by ListPacketsByIATAs instead. +-- name: ListPacketsPage :many +-- Unfiltered packet page: newest (last_heard_at, packet_hash) first. +WITH page AS MATERIALIZED ( + SELECT p.packet_hash, p.last_heard_at AS k FROM packets p + WHERE (p.last_heard_at, p.packet_hash) < (@cursor_ts::timestamptz, @cursor_hash::bytea) + ORDER BY p.last_heard_at DESC, p.packet_hash DESC + LIMIT @page_limit +) SELECT p.packet_hash, p.payload_type, @@ -508,25 +512,338 @@ SELECT po.path_bytes AS latest_observer_path_bytes, p.raw_payload, p.origin_pubkey -FROM packets p +FROM page pg +JOIN packets p ON p.packet_hash = pg.packet_hash LEFT JOIN LATERAL ( SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes FROM packet_observations WHERE packet_hash = p.packet_hash - ORDER BY heard_at DESC + ORDER BY heard_at DESC, id DESC LIMIT 1 ) po ON true LEFT JOIN observers o ON o.id = po.observer_id LEFT JOIN transport_scopes ts ON ts.id = p.scope_id -WHERE - (COALESCE(cardinality($1::smallint[]), 0) = 0 OR p.payload_type = ANY($1::smallint[])) - AND (COALESCE(cardinality($2::smallint[]), 0) = 0 OR p.route_type = ANY($2::smallint[])) - AND ($3::timestamptz IS NULL OR p.first_heard_at >= $3) - AND ($4::timestamptz IS NULL OR p.first_heard_at <= $4) - AND ($5::timestamptz IS NULL OR p.last_heard_at < $5) - AND (COALESCE(cardinality($7::text[]), 0) = 0 OR ts.name = ANY($7::text[])) -ORDER BY p.last_heard_at DESC -LIMIT $6; +ORDER BY pg.k DESC, pg.packet_hash DESC; + +-- name: ListPacketsByTypes :many +-- One range per (payload type, route type) pair; DISTINCT keeps a repeated type from returning rows twice. +WITH page AS MATERIALIZED ( + SELECT x.packet_hash, x.last_heard_at AS k + FROM (SELECT DISTINCT unnest(@payload_types::smallint[])) pt(t) + CROSS JOIN (SELECT DISTINCT unnest(@route_types::smallint[])) rt(t) + CROSS JOIN LATERAL ( + SELECT p.packet_hash, p.last_heard_at FROM packets p + WHERE p.payload_type = pt.t AND p.route_type = rt.t + AND (p.last_heard_at, p.packet_hash) < (@cursor_ts::timestamptz, @cursor_hash::bytea) + ORDER BY p.last_heard_at DESC, p.packet_hash DESC + LIMIT @page_limit + ) x + ORDER BY x.last_heard_at DESC, x.packet_hash DESC + LIMIT @page_limit +) +SELECT + p.packet_hash, + p.payload_type, + COALESCE(CASE + WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' + THEN p.parsed_payload #>> '{appData,name}' + WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' + AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' + AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' + THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') + WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') + AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' + AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' + THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') + END, '')::text AS summary, + p.route_type, + p.first_heard_at, + p.last_heard_at, + p.scope_id, + ts.name AS scope_name, + p.observation_count AS observation_count, + -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. + po.observer_id AS latest_observer_id, + o.display_name AS latest_observer_name, + COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, + COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, + COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, + COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, + po.path_bytes AS latest_observer_path_bytes, + p.raw_payload, + p.origin_pubkey +FROM page pg +JOIN packets p ON p.packet_hash = pg.packet_hash +LEFT JOIN LATERAL ( + SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes + FROM packet_observations + WHERE packet_hash = p.packet_hash + ORDER BY heard_at DESC, id DESC + LIMIT 1 +) po ON true +LEFT JOIN observers o ON o.id = po.observer_id +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +ORDER BY pg.k DESC, pg.packet_hash DESC; + +-- name: ListPacketsByScopes :many +-- One range per named scope; a zero-hit scope costs a single index probe. +WITH page AS MATERIALIZED ( + SELECT x.packet_hash, x.last_heard_at AS k + FROM transport_scopes s + CROSS JOIN LATERAL ( + SELECT p.packet_hash, p.last_heard_at FROM packets p + WHERE p.scope_id = s.id + AND (p.last_heard_at, p.packet_hash) < (@cursor_ts::timestamptz, @cursor_hash::bytea) + ORDER BY p.last_heard_at DESC, p.packet_hash DESC + LIMIT @page_limit + ) x + WHERE s.name = ANY(@scope_names::text[]) + ORDER BY x.last_heard_at DESC, x.packet_hash DESC + LIMIT @page_limit +) +SELECT + p.packet_hash, + p.payload_type, + COALESCE(CASE + WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' + THEN p.parsed_payload #>> '{appData,name}' + WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' + AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' + AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' + THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') + WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') + AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' + AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' + THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') + END, '')::text AS summary, + p.route_type, + p.first_heard_at, + p.last_heard_at, + p.scope_id, + ts.name AS scope_name, + p.observation_count AS observation_count, + -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. + po.observer_id AS latest_observer_id, + o.display_name AS latest_observer_name, + COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, + COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, + COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, + COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, + po.path_bytes AS latest_observer_path_bytes, + p.raw_payload, + p.origin_pubkey +FROM page pg +JOIN packets p ON p.packet_hash = pg.packet_hash +LEFT JOIN LATERAL ( + SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes + FROM packet_observations + WHERE packet_hash = p.packet_hash + ORDER BY heard_at DESC, id DESC + LIMIT 1 +) po ON true +LEFT JOIN observers o ON o.id = po.observer_id +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +ORDER BY pg.k DESC, pg.packet_hash DESC; + +-- name: ListPacketsByScopesTypes :many +-- ListPacketsByScopes with the type tests inside each scope range (index-only through the included columns). +WITH page AS MATERIALIZED ( + SELECT x.packet_hash, x.last_heard_at AS k + FROM transport_scopes s + CROSS JOIN LATERAL ( + SELECT p.packet_hash, p.last_heard_at FROM packets p + WHERE p.scope_id = s.id + AND (p.last_heard_at, p.packet_hash) < (@cursor_ts::timestamptz, @cursor_hash::bytea) + AND p.payload_type = ANY(@payload_types::smallint[]) AND p.route_type = ANY(@route_types::smallint[]) + ORDER BY p.last_heard_at DESC, p.packet_hash DESC + LIMIT @page_limit + ) x + WHERE s.name = ANY(@scope_names::text[]) + ORDER BY x.last_heard_at DESC, x.packet_hash DESC + LIMIT @page_limit +) +SELECT + p.packet_hash, + p.payload_type, + COALESCE(CASE + WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' + THEN p.parsed_payload #>> '{appData,name}' + WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' + AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' + AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' + THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') + WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') + AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' + AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' + THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') + END, '')::text AS summary, + p.route_type, + p.first_heard_at, + p.last_heard_at, + p.scope_id, + ts.name AS scope_name, + p.observation_count AS observation_count, + -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. + po.observer_id AS latest_observer_id, + o.display_name AS latest_observer_name, + COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, + COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, + COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, + COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, + po.path_bytes AS latest_observer_path_bytes, + p.raw_payload, + p.origin_pubkey +FROM page pg +JOIN packets p ON p.packet_hash = pg.packet_hash +LEFT JOIN LATERAL ( + SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes + FROM packet_observations + WHERE packet_hash = p.packet_hash + ORDER BY heard_at DESC, id DESC + LIMIT 1 +) po ON true +LEFT JOIN observers o ON o.id = po.observer_id +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +ORDER BY pg.k DESC, pg.packet_hash DESC; + +-- name: ListPacketsWindow :many +-- first_heard_at window. Branch 1 walks last_heard_at down to since; branch 2 adds the rows a concurrent +-- upsert left with last_heard_at below first_heard_at, which that bound cannot see. +WITH page AS MATERIALIZED ( + SELECT u.packet_hash, u.k FROM ( + (SELECT p.packet_hash, p.last_heard_at AS k FROM packets p + WHERE p.last_heard_at >= @since_ts::timestamptz + AND (p.last_heard_at, p.packet_hash) < (@cursor_ts::timestamptz, @cursor_hash::bytea) + AND (p.first_heard_at >= @since_ts AND p.first_heard_at <= @until_ts::timestamptz) IS TRUE + AND p.payload_type = ANY(@payload_types::smallint[]) AND p.route_type = ANY(@route_types::smallint[]) + ORDER BY p.last_heard_at DESC, p.packet_hash DESC + LIMIT @page_limit) + UNION ALL + (SELECT p.packet_hash, p.last_heard_at AS k FROM packets p + WHERE (p.first_heard_at > p.last_heard_at) IS TRUE + AND (p.first_heard_at >= @since_ts AND p.first_heard_at <= @until_ts + AND p.last_heard_at < @since_ts + AND (p.last_heard_at, p.packet_hash) < (@cursor_ts, @cursor_hash) + AND p.payload_type = ANY(@payload_types) AND p.route_type = ANY(@route_types)) IS TRUE) + ) u + ORDER BY u.k DESC, u.packet_hash DESC + LIMIT @page_limit +) +SELECT + p.packet_hash, + p.payload_type, + COALESCE(CASE + WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' + THEN p.parsed_payload #>> '{appData,name}' + WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' + AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' + AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' + THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') + WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') + AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' + AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' + THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') + END, '')::text AS summary, + p.route_type, + p.first_heard_at, + p.last_heard_at, + p.scope_id, + ts.name AS scope_name, + p.observation_count AS observation_count, + -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. + po.observer_id AS latest_observer_id, + o.display_name AS latest_observer_name, + COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, + COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, + COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, + COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, + po.path_bytes AS latest_observer_path_bytes, + p.raw_payload, + p.origin_pubkey +FROM page pg +JOIN packets p ON p.packet_hash = pg.packet_hash +LEFT JOIN LATERAL ( + SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes + FROM packet_observations + WHERE packet_hash = p.packet_hash + ORDER BY heard_at DESC, id DESC + LIMIT 1 +) po ON true +LEFT JOIN observers o ON o.id = po.observer_id +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +ORDER BY pg.k DESC, pg.packet_hash DESC; + +-- name: ListPacketsWindowScoped :many +-- ListPacketsWindow with branch 1 run once per named scope and branch 2 limited to those scopes. +WITH page AS MATERIALIZED ( + SELECT u.packet_hash, u.k FROM ( + (SELECT x.packet_hash, x.last_heard_at AS k + FROM transport_scopes s + CROSS JOIN LATERAL ( + SELECT p.packet_hash, p.last_heard_at FROM packets p + WHERE p.scope_id = s.id + AND p.last_heard_at >= @since_ts::timestamptz + AND (p.last_heard_at, p.packet_hash) < (@cursor_ts::timestamptz, @cursor_hash::bytea) + AND (p.first_heard_at >= @since_ts AND p.first_heard_at <= @until_ts::timestamptz) IS TRUE + AND p.payload_type = ANY(@payload_types::smallint[]) AND p.route_type = ANY(@route_types::smallint[]) + ORDER BY p.last_heard_at DESC, p.packet_hash DESC + LIMIT @page_limit) x + WHERE s.name = ANY(@scope_names::text[])) + UNION ALL + (SELECT p.packet_hash, p.last_heard_at AS k FROM packets p + WHERE (p.first_heard_at > p.last_heard_at) IS TRUE + AND (p.first_heard_at >= @since_ts AND p.first_heard_at <= @until_ts + AND p.last_heard_at < @since_ts + AND (p.last_heard_at, p.packet_hash) < (@cursor_ts, @cursor_hash) + AND p.payload_type = ANY(@payload_types) AND p.route_type = ANY(@route_types) + AND p.scope_id = ANY(ARRAY(SELECT id FROM transport_scopes WHERE name = ANY(@scope_names)))) IS TRUE) + ) u + ORDER BY u.k DESC, u.packet_hash DESC + LIMIT @page_limit +) +SELECT + p.packet_hash, + p.payload_type, + COALESCE(CASE + WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' + THEN p.parsed_payload #>> '{appData,name}' + WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' + AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' + AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' + THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') + WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') + AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' + AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' + THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') + END, '')::text AS summary, + p.route_type, + p.first_heard_at, + p.last_heard_at, + p.scope_id, + ts.name AS scope_name, + p.observation_count AS observation_count, + -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. + po.observer_id AS latest_observer_id, + o.display_name AS latest_observer_name, + COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, + COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, + COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, + COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, + po.path_bytes AS latest_observer_path_bytes, + p.raw_payload, + p.origin_pubkey +FROM page pg +JOIN packets p ON p.packet_hash = pg.packet_hash +LEFT JOIN LATERAL ( + SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes + FROM packet_observations + WHERE packet_hash = p.packet_hash + ORDER BY heard_at DESC, id DESC + LIMIT 1 +) po ON true +LEFT JOIN observers o ON o.id = po.observer_id +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +ORDER BY pg.k DESC, pg.packet_hash DESC; -- name: ListPacketsByIATAs :many -- IATA-filtered packet list, driven from idx_observations_iata_heard. @@ -968,10 +1285,10 @@ WHERE ) AND ($5::bytea IS NULL OR n.public_key = $5) AND ($6 = '' OR n.name ILIKE '%' || $6 || '%') - AND ($7::timestamptz IS NULL OR n.last_seen < $7) + AND (n.last_seen, n.id) < ($7::timestamptz, $12::uuid) AND ($9::text = '' OR ts.name = $9::text) AND ($11::text = '' OR encode(n.public_key, 'hex') ILIKE $11 || '%') -ORDER BY n.last_seen DESC +ORDER BY n.last_seen DESC, n.id DESC LIMIT $8 ) SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_seen, @@ -985,7 +1302,7 @@ SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_ (SELECT COALESCE(array_agg(DISTINCT nn.neighbor_id), '{}'::uuid[]) FROM node_neighbors nn WHERE nn.node_id = n.id) ELSE NULL END)::uuid[] AS neighbor_ids FROM page n -ORDER BY n.last_seen DESC; +ORDER BY n.last_seen DESC, n.id DESC; -- name: ListNodeObservations :many SELECT po.id, encode(po.packet_hash, 'hex') AS packet_hash_hex, diff --git a/db/small_tables_integration_test.go b/db/small_tables_integration_test.go index b03fb71..5b90d41 100644 --- a/db/small_tables_integration_test.go +++ b/db/small_tables_integration_test.go @@ -296,7 +296,7 @@ func TestNodeReadersAfterIndexDropsPostgres(t *testing.T) { nodeKey := func(n api.NodeSummary) string { return lastSeen[n.ID] } nodeID := func(n api.NodeSummary) string { return n.ID.String() } normNodes := func(v any) any { - p := v.(api.Page[api.NodeSummary]) + p := v.(api.NodePage) p.Items = sortTieRuns(p.Items, nodeKey, nodeID) return p } @@ -306,7 +306,7 @@ func TestNodeReadersAfterIndexDropsPostgres(t *testing.T) { ms = cursor.UnixMilli() } return readerShape{name: name, norm: normNodes, run: func() (any, error) { - return store.ListNodes(ctx, nodeType, iatas, mbPaths, mbTraces, nil, prefix, nameFilter, "", ms, limit, neighbors) + return store.ListNodes(ctx, nodeType, iatas, mbPaths, mbTraces, nil, prefix, nameFilter, "", ms, nil, limit, neighbors) }} } var none time.Time @@ -348,7 +348,7 @@ func TestNodeReadersAfterIndexDropsPostgres(t *testing.T) { before := runShapes(t, ctx, tx, shapes) for _, r := range before["type9"] { - if items := r.val.(api.Page[api.NodeSummary]).Items; items == nil || len(items) != 0 { + if items := r.val.(api.NodePage).Items; items == nil || len(items) != 0 { t.Fatalf("type9 items = %#v, want empty non-nil", items) } } @@ -369,7 +369,7 @@ func TestNodeReadersAfterIndexDropsPostgres(t *testing.T) { // LIMIT 150 splits the 25-node tie run at ranks 140-164: rows 1-139 are fixed, the last 11 are any 11 of the run. for i := range planSettings { for _, r := range []shapeResult{before["global l150"][i], after["global l150"][i]} { - p := r.val.(api.Page[api.NodeSummary]) + p := r.val.(api.NodePage) if len(p.Items) != 150 || !p.HasMore || p.NextCursor == nil { t.Fatalf("global l150 under %q: %d items, hasMore %v", planSettings[i], len(p.Items), p.HasMore) } @@ -381,7 +381,7 @@ func TestNodeReadersAfterIndexDropsPostgres(t *testing.T) { seen[n.ID] = true } } - b, a := before["global l150"][i].val.(api.Page[api.NodeSummary]), after["global l150"][i].val.(api.Page[api.NodeSummary]) + b, a := before["global l150"][i].val.(api.NodePage), after["global l150"][i].val.(api.NodePage) if !reflect.DeepEqual(a.Items[:139], b.Items[:139]) || *a.NextCursor != *b.NextCursor { t.Errorf("global l150 under %q: first 139 rows or cursor differ after the drops", planSettings[i]) } @@ -431,7 +431,7 @@ func TestObserverChannelReadersAfterIndexDropsPostgres(t *testing.T) { t.Fatal(err) } normObservers := func(v any) any { - p := v.(api.Page[api.ObserverSummary]) + p := v.(api.ObserverPage) p.Items = sortTieRuns(p.Items, func(o api.ObserverSummary) string { return lastSeen[o.ID] }, func(o api.ObserverSummary) string { return o.ID.String() }) return p @@ -442,7 +442,7 @@ func TestObserverChannelReadersAfterIndexDropsPostgres(t *testing.T) { ms = cursor.UnixMilli() } return readerShape{name: name, norm: normObservers, run: func() (any, error) { - return store.ListObservers(ctx, iatas, observerType, broker, status, nameFilter, scope, ms, limit) + return store.ListObservers(ctx, iatas, observerType, broker, status, nameFilter, scope, ms, nil, limit) }} } exact := func(v any) any { return v } @@ -474,10 +474,10 @@ func TestObserverChannelReadersAfterIndexDropsPostgres(t *testing.T) { } before := runShapes(t, ctx, tx, shapes) - if n := len(before["observers first l201"][0].val.(api.Page[api.ObserverSummary]).Items); n != 201 { + if n := len(before["observers first l201"][0].val.(api.ObserverPage).Items); n != 201 { t.Fatalf("observers first page has %d rows, want 201", n) } - if n := len(before["observers scope #test"][0].val.(api.Page[api.ObserverSummary]).Items); n != 1 { + if n := len(before["observers scope #test"][0].val.(api.ObserverPage).Items); n != 1 { t.Fatalf("observers in #test = %d, want 1", n) } if n := len(before["channels hash 05"][0].val.(api.ChannelPage).Items); n != 3 { diff --git a/db/sqlc/mock/querier.go b/db/sqlc/mock/querier.go index 143eea6..f04f86c 100644 --- a/db/sqlc/mock/querier.go +++ b/db/sqlc/mock/querier.go @@ -1629,21 +1629,6 @@ func (mr *MockQuerierMockRecorder) ListObservers(ctx, arg any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObservers", reflect.TypeOf((*MockQuerier)(nil).ListObservers), ctx, arg) } -// ListPackets mocks base method. -func (m *MockQuerier) ListPackets(ctx context.Context, arg db.ListPacketsParams) ([]db.ListPacketsRow, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListPackets", ctx, arg) - ret0, _ := ret[0].([]db.ListPacketsRow) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListPackets indicates an expected call of ListPackets. -func (mr *MockQuerierMockRecorder) ListPackets(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPackets", reflect.TypeOf((*MockQuerier)(nil).ListPackets), ctx, arg) -} - // ListPacketsAfterID mocks base method. func (m *MockQuerier) ListPacketsAfterID(ctx context.Context, arg db.ListPacketsAfterIDParams) ([]db.ListPacketsAfterIDRow, error) { m.ctrl.T.Helper() @@ -1674,6 +1659,96 @@ func (mr *MockQuerierMockRecorder) ListPacketsByIATAs(ctx, arg any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPacketsByIATAs", reflect.TypeOf((*MockQuerier)(nil).ListPacketsByIATAs), ctx, arg) } +// ListPacketsByScopes mocks base method. +func (m *MockQuerier) ListPacketsByScopes(ctx context.Context, arg db.ListPacketsByScopesParams) ([]db.ListPacketsByScopesRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPacketsByScopes", ctx, arg) + ret0, _ := ret[0].([]db.ListPacketsByScopesRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPacketsByScopes indicates an expected call of ListPacketsByScopes. +func (mr *MockQuerierMockRecorder) ListPacketsByScopes(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPacketsByScopes", reflect.TypeOf((*MockQuerier)(nil).ListPacketsByScopes), ctx, arg) +} + +// ListPacketsByScopesTypes mocks base method. +func (m *MockQuerier) ListPacketsByScopesTypes(ctx context.Context, arg db.ListPacketsByScopesTypesParams) ([]db.ListPacketsByScopesTypesRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPacketsByScopesTypes", ctx, arg) + ret0, _ := ret[0].([]db.ListPacketsByScopesTypesRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPacketsByScopesTypes indicates an expected call of ListPacketsByScopesTypes. +func (mr *MockQuerierMockRecorder) ListPacketsByScopesTypes(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPacketsByScopesTypes", reflect.TypeOf((*MockQuerier)(nil).ListPacketsByScopesTypes), ctx, arg) +} + +// ListPacketsByTypes mocks base method. +func (m *MockQuerier) ListPacketsByTypes(ctx context.Context, arg db.ListPacketsByTypesParams) ([]db.ListPacketsByTypesRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPacketsByTypes", ctx, arg) + ret0, _ := ret[0].([]db.ListPacketsByTypesRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPacketsByTypes indicates an expected call of ListPacketsByTypes. +func (mr *MockQuerierMockRecorder) ListPacketsByTypes(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPacketsByTypes", reflect.TypeOf((*MockQuerier)(nil).ListPacketsByTypes), ctx, arg) +} + +// ListPacketsPage mocks base method. +func (m *MockQuerier) ListPacketsPage(ctx context.Context, arg db.ListPacketsPageParams) ([]db.ListPacketsPageRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPacketsPage", ctx, arg) + ret0, _ := ret[0].([]db.ListPacketsPageRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPacketsPage indicates an expected call of ListPacketsPage. +func (mr *MockQuerierMockRecorder) ListPacketsPage(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPacketsPage", reflect.TypeOf((*MockQuerier)(nil).ListPacketsPage), ctx, arg) +} + +// ListPacketsWindow mocks base method. +func (m *MockQuerier) ListPacketsWindow(ctx context.Context, arg db.ListPacketsWindowParams) ([]db.ListPacketsWindowRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPacketsWindow", ctx, arg) + ret0, _ := ret[0].([]db.ListPacketsWindowRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPacketsWindow indicates an expected call of ListPacketsWindow. +func (mr *MockQuerierMockRecorder) ListPacketsWindow(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPacketsWindow", reflect.TypeOf((*MockQuerier)(nil).ListPacketsWindow), ctx, arg) +} + +// ListPacketsWindowScoped mocks base method. +func (m *MockQuerier) ListPacketsWindowScoped(ctx context.Context, arg db.ListPacketsWindowScopedParams) ([]db.ListPacketsWindowScopedRow, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPacketsWindowScoped", ctx, arg) + ret0, _ := ret[0].([]db.ListPacketsWindowScopedRow) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPacketsWindowScoped indicates an expected call of ListPacketsWindowScoped. +func (mr *MockQuerierMockRecorder) ListPacketsWindowScoped(ctx, arg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPacketsWindowScoped", reflect.TypeOf((*MockQuerier)(nil).ListPacketsWindowScoped), ctx, arg) +} + // ListRegionState mocks base method. func (m *MockQuerier) ListRegionState(ctx context.Context) ([]db.ListRegionStateRow, error) { m.ctrl.T.Helper() diff --git a/db/sqlc/querier.go b/db/sqlc/querier.go index b01660f..03e59f4 100644 --- a/db/sqlc/querier.go +++ b/db/sqlc/querier.go @@ -245,13 +245,8 @@ type Querier interface { // Copyright 2026 Beacon Contributors // SPDX-License-Identifier: AGPL-3.0-or-later ListObserverDirectory(ctx context.Context, arg ListObserverDirectoryParams) (string, error) - // Pass cursor=0 to start from the beginning, or the last seen observer's rownum for pagination. - // Note: observers use UUID PKs so we order by last_seen and use a keyset on last_seen+id. + // Observers newest last_seen first; the cursor is the (last_seen, id) of the last row. ListObservers(ctx context.Context, arg ListObserversParams) ([]ListObserversRow, error) - // Returns packets with the latest observation rolled in for display. - // Pass cursor=0 to start from the beginning. IATA-filtered requests are - // served by ListPacketsByIATAs instead. - ListPackets(ctx context.Context, arg ListPacketsParams) ([]ListPacketsRow, error) // Returns packets with observations after the given observation ID, ordered oldest first. // Used for WS reconnect backfill. Pass afterObservationId=0 to start from the beginning. ListPacketsAfterID(ctx context.Context, arg ListPacketsAfterIDParams) ([]ListPacketsAfterIDRow, error) @@ -269,6 +264,19 @@ type Querier interface { // A site that filled scan_depth still has unread history below its floor. // The newest such floor is the point above which every site is covered. ListPacketsByIATAs(ctx context.Context, arg ListPacketsByIATAsParams) ([]ListPacketsByIATAsRow, error) + // One range per named scope; a zero-hit scope costs a single index probe. + ListPacketsByScopes(ctx context.Context, arg ListPacketsByScopesParams) ([]ListPacketsByScopesRow, error) + // ListPacketsByScopes with the type tests inside each scope range (index-only through the included columns). + ListPacketsByScopesTypes(ctx context.Context, arg ListPacketsByScopesTypesParams) ([]ListPacketsByScopesTypesRow, error) + // One range per (payload type, route type) pair; DISTINCT keeps a repeated type from returning rows twice. + ListPacketsByTypes(ctx context.Context, arg ListPacketsByTypesParams) ([]ListPacketsByTypesRow, error) + // Unfiltered packet page: newest (last_heard_at, packet_hash) first. + ListPacketsPage(ctx context.Context, arg ListPacketsPageParams) ([]ListPacketsPageRow, error) + // first_heard_at window. Branch 1 walks last_heard_at down to since; branch 2 adds the rows a concurrent + // upsert left with last_heard_at below first_heard_at, which that bound cannot see. + ListPacketsWindow(ctx context.Context, arg ListPacketsWindowParams) ([]ListPacketsWindowRow, error) + // ListPacketsWindow with branch 1 run once per named scope and branch 2 limited to those scopes. + ListPacketsWindowScoped(ctx context.Context, arg ListPacketsWindowScopedParams) ([]ListPacketsWindowScopedRow, error) // Every region with its members, for reconciling imported MeshMapper groups. ListRegionState(ctx context.Context) ([]ListRegionStateRow, error) // ============================================================ diff --git a/db/sqlc/queries.sql.go b/db/sqlc/queries.sql.go index d0e8455..2fe93dd 100644 --- a/db/sqlc/queries.sql.go +++ b/db/sqlc/queries.sql.go @@ -3527,10 +3527,10 @@ WHERE ) AND ($5::bytea IS NULL OR n.public_key = $5) AND ($6 = '' OR n.name ILIKE '%' || $6 || '%') - AND ($7::timestamptz IS NULL OR n.last_seen < $7) + AND (n.last_seen, n.id) < ($7::timestamptz, $12::uuid) AND ($9::text = '' OR ts.name = $9::text) AND ($11::text = '' OR encode(n.public_key, 'hex') ILIKE $11 || '%') -ORDER BY n.last_seen DESC +ORDER BY n.last_seen DESC, n.id DESC LIMIT $8 ) SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_seen, @@ -3544,7 +3544,7 @@ SELECT n.id, n.public_key, n.node_type, n.name, n.latitude, n.longitude, n.last_ (SELECT COALESCE(array_agg(DISTINCT nn.neighbor_id), '{}'::uuid[]) FROM node_neighbors nn WHERE nn.node_id = n.id) ELSE NULL END)::uuid[] AS neighbor_ids FROM page n -ORDER BY n.last_seen DESC +ORDER BY n.last_seen DESC, n.id DESC ` type ListNodesParams struct { @@ -3559,6 +3559,7 @@ type ListNodesParams struct { Column9 string `json:"column_9"` Column10 bool `json:"column_10"` Column11 string `json:"column_11"` + Column12 uuid.UUID `json:"column_12"` } type ListNodesRow struct { @@ -3594,6 +3595,7 @@ func (q *Queries) ListNodes(ctx context.Context, arg ListNodesParams) ([]ListNod arg.Column9, arg.Column10, arg.Column11, + arg.Column12, ) if err != nil { return nil, err @@ -3791,7 +3793,8 @@ COALESCE(CASE WHEN GREATEST(COALESCE(o.last_status_at, o.last_seen), o.last_seen) > NOW() - INTERVAL '5 minutes' THEN 'online' ELSE 'offline' END, 'offline')::text AS status, -COALESCE(o.last_iata, '')::text AS iata +COALESCE(o.last_iata, '')::text AS iata, +o.last_seen FROM observers o LEFT JOIN observer_brokers ob ON ob.observer_id = o.id LEFT JOIN observer_scopes os ON os.observer_id = o.id @@ -3805,14 +3808,14 @@ WHERE ELSE 'offline' END = $4) AND ($5 = '' OR o.display_name ILIKE '%' || $5 || '%') - AND ($6::timestamptz IS NULL OR o.last_seen < $6) + AND (o.last_seen, o.id) < ($6::timestamptz, $9::uuid) AND ($8::text = '' OR EXISTS ( SELECT 1 FROM observer_scopes os2 JOIN transport_scopes ts2 ON ts2.id = os2.scope_id WHERE os2.observer_id = o.id AND ts2.name = $8::text )) GROUP BY o.id -ORDER BY o.last_seen DESC +ORDER BY o.last_seen DESC, o.id DESC LIMIT $7 ` @@ -3825,6 +3828,7 @@ type ListObserversParams struct { Column6 pgtype.Timestamptz `json:"column_6"` Limit int32 `json:"limit"` Column8 string `json:"column_8"` + Column9 uuid.UUID `json:"column_9"` } type ListObserversRow struct { @@ -3838,10 +3842,10 @@ type ListObserversRow struct { Scopes []string `json:"scopes"` Status string `json:"status"` Iata string `json:"iata"` + LastSeen pgtype.Timestamptz `json:"last_seen"` } -// Pass cursor=0 to start from the beginning, or the last seen observer's rownum for pagination. -// Note: observers use UUID PKs so we order by last_seen and use a keyset on last_seen+id. +// Observers newest last_seen first; the cursor is the (last_seen, id) of the last row. func (q *Queries) ListObservers(ctx context.Context, arg ListObserversParams) ([]ListObserversRow, error) { rows, err := q.db.Query(ctx, listObservers, arg.Column1, @@ -3852,6 +3856,7 @@ func (q *Queries) ListObservers(ctx context.Context, arg ListObserversParams) ([ arg.Column6, arg.Limit, arg.Column8, + arg.Column9, ) if err != nil { return nil, err @@ -3871,140 +3876,7 @@ func (q *Queries) ListObservers(ctx context.Context, arg ListObserversParams) ([ &i.Scopes, &i.Status, &i.Iata, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const listPackets = `-- name: ListPackets :many -SELECT - p.packet_hash, - p.payload_type, - COALESCE(CASE - WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' - THEN p.parsed_payload #>> '{appData,name}' - WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' - AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' - AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' - THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') - WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') - AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' - AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' - THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') - END, '')::text AS summary, - p.route_type, - p.first_heard_at, - p.last_heard_at, - p.scope_id, - ts.name AS scope_name, - p.observation_count AS observation_count, - -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. - po.observer_id AS latest_observer_id, - o.display_name AS latest_observer_name, - COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, - COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, - COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, - COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, - po.path_bytes AS latest_observer_path_bytes, - p.raw_payload, - p.origin_pubkey -FROM packets p -LEFT JOIN LATERAL ( - SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes - FROM packet_observations - WHERE packet_hash = p.packet_hash - ORDER BY heard_at DESC - LIMIT 1 -) po ON true -LEFT JOIN observers o ON o.id = po.observer_id -LEFT JOIN transport_scopes ts ON ts.id = p.scope_id -WHERE - (COALESCE(cardinality($1::smallint[]), 0) = 0 OR p.payload_type = ANY($1::smallint[])) - AND (COALESCE(cardinality($2::smallint[]), 0) = 0 OR p.route_type = ANY($2::smallint[])) - AND ($3::timestamptz IS NULL OR p.first_heard_at >= $3) - AND ($4::timestamptz IS NULL OR p.first_heard_at <= $4) - AND ($5::timestamptz IS NULL OR p.last_heard_at < $5) - AND (COALESCE(cardinality($7::text[]), 0) = 0 OR ts.name = ANY($7::text[])) -ORDER BY p.last_heard_at DESC -LIMIT $6 -` - -type ListPacketsParams struct { - Column1 []int16 `json:"column_1"` - Column2 []int16 `json:"column_2"` - Column3 pgtype.Timestamptz `json:"column_3"` - Column4 pgtype.Timestamptz `json:"column_4"` - Column5 pgtype.Timestamptz `json:"column_5"` - Limit int32 `json:"limit"` - Column7 []string `json:"column_7"` -} - -type ListPacketsRow struct { - PacketHash []byte `json:"packet_hash"` - PayloadType int16 `json:"payload_type"` - Summary string `json:"summary"` - RouteType int16 `json:"route_type"` - FirstHeardAt pgtype.Timestamptz `json:"first_heard_at"` - LastHeardAt pgtype.Timestamptz `json:"last_heard_at"` - ScopeID *int32 `json:"scope_id"` - ScopeName *string `json:"scope_name"` - ObservationCount int64 `json:"observation_count"` - LatestObserverID uuid.UUID `json:"latest_observer_id"` - LatestObserverName *string `json:"latest_observer_name"` - LatestObserverIata string `json:"latest_observer_iata"` - LatestObserverPathLengthByte int16 `json:"latest_observer_path_length_byte"` - LatestObserverHashSize int16 `json:"latest_observer_hash_size"` - LatestObserverHopCount int16 `json:"latest_observer_hop_count"` - LatestObserverPathBytes []byte `json:"latest_observer_path_bytes"` - RawPayload []byte `json:"raw_payload"` - OriginPubkey []byte `json:"origin_pubkey"` -} - -// Returns packets with the latest observation rolled in for display. -// Pass cursor=0 to start from the beginning. IATA-filtered requests are -// served by ListPacketsByIATAs instead. -func (q *Queries) ListPackets(ctx context.Context, arg ListPacketsParams) ([]ListPacketsRow, error) { - rows, err := q.db.Query(ctx, listPackets, - arg.Column1, - arg.Column2, - arg.Column3, - arg.Column4, - arg.Column5, - arg.Limit, - arg.Column7, - ) - if err != nil { - return nil, err - } - defer rows.Close() - items := []ListPacketsRow{} - for rows.Next() { - var i ListPacketsRow - if err := rows.Scan( - &i.PacketHash, - &i.PayloadType, - &i.Summary, - &i.RouteType, - &i.FirstHeardAt, - &i.LastHeardAt, - &i.ScopeID, - &i.ScopeName, - &i.ObservationCount, - &i.LatestObserverID, - &i.LatestObserverName, - &i.LatestObserverIata, - &i.LatestObserverPathLengthByte, - &i.LatestObserverHashSize, - &i.LatestObserverHopCount, - &i.LatestObserverPathBytes, - &i.RawPayload, - &i.OriginPubkey, + &i.LastSeen, ); err != nil { return nil, err } @@ -4329,6 +4201,830 @@ func (q *Queries) ListPacketsByIATAs(ctx context.Context, arg ListPacketsByIATAs return items, nil } +const listPacketsByScopes = `-- name: ListPacketsByScopes :many +WITH page AS MATERIALIZED ( + SELECT x.packet_hash, x.last_heard_at AS k + FROM transport_scopes s + CROSS JOIN LATERAL ( + SELECT p.packet_hash, p.last_heard_at FROM packets p + WHERE p.scope_id = s.id + AND (p.last_heard_at, p.packet_hash) < ($1::timestamptz, $2::bytea) + ORDER BY p.last_heard_at DESC, p.packet_hash DESC + LIMIT $3 + ) x + WHERE s.name = ANY($4::text[]) + ORDER BY x.last_heard_at DESC, x.packet_hash DESC + LIMIT $3 +) +SELECT + p.packet_hash, + p.payload_type, + COALESCE(CASE + WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' + THEN p.parsed_payload #>> '{appData,name}' + WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' + AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' + AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' + THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') + WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') + AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' + AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' + THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') + END, '')::text AS summary, + p.route_type, + p.first_heard_at, + p.last_heard_at, + p.scope_id, + ts.name AS scope_name, + p.observation_count AS observation_count, + -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. + po.observer_id AS latest_observer_id, + o.display_name AS latest_observer_name, + COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, + COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, + COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, + COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, + po.path_bytes AS latest_observer_path_bytes, + p.raw_payload, + p.origin_pubkey +FROM page pg +JOIN packets p ON p.packet_hash = pg.packet_hash +LEFT JOIN LATERAL ( + SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes + FROM packet_observations + WHERE packet_hash = p.packet_hash + ORDER BY heard_at DESC, id DESC + LIMIT 1 +) po ON true +LEFT JOIN observers o ON o.id = po.observer_id +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +ORDER BY pg.k DESC, pg.packet_hash DESC +` + +type ListPacketsByScopesParams struct { + CursorTs pgtype.Timestamptz `json:"cursor_ts"` + CursorHash []byte `json:"cursor_hash"` + PageLimit int32 `json:"page_limit"` + ScopeNames []string `json:"scope_names"` +} + +type ListPacketsByScopesRow struct { + PacketHash []byte `json:"packet_hash"` + PayloadType int16 `json:"payload_type"` + Summary string `json:"summary"` + RouteType int16 `json:"route_type"` + FirstHeardAt pgtype.Timestamptz `json:"first_heard_at"` + LastHeardAt pgtype.Timestamptz `json:"last_heard_at"` + ScopeID *int32 `json:"scope_id"` + ScopeName *string `json:"scope_name"` + ObservationCount int64 `json:"observation_count"` + LatestObserverID uuid.UUID `json:"latest_observer_id"` + LatestObserverName *string `json:"latest_observer_name"` + LatestObserverIata string `json:"latest_observer_iata"` + LatestObserverPathLengthByte int16 `json:"latest_observer_path_length_byte"` + LatestObserverHashSize int16 `json:"latest_observer_hash_size"` + LatestObserverHopCount int16 `json:"latest_observer_hop_count"` + LatestObserverPathBytes []byte `json:"latest_observer_path_bytes"` + RawPayload []byte `json:"raw_payload"` + OriginPubkey []byte `json:"origin_pubkey"` +} + +// One range per named scope; a zero-hit scope costs a single index probe. +func (q *Queries) ListPacketsByScopes(ctx context.Context, arg ListPacketsByScopesParams) ([]ListPacketsByScopesRow, error) { + rows, err := q.db.Query(ctx, listPacketsByScopes, + arg.CursorTs, + arg.CursorHash, + arg.PageLimit, + arg.ScopeNames, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListPacketsByScopesRow{} + for rows.Next() { + var i ListPacketsByScopesRow + if err := rows.Scan( + &i.PacketHash, + &i.PayloadType, + &i.Summary, + &i.RouteType, + &i.FirstHeardAt, + &i.LastHeardAt, + &i.ScopeID, + &i.ScopeName, + &i.ObservationCount, + &i.LatestObserverID, + &i.LatestObserverName, + &i.LatestObserverIata, + &i.LatestObserverPathLengthByte, + &i.LatestObserverHashSize, + &i.LatestObserverHopCount, + &i.LatestObserverPathBytes, + &i.RawPayload, + &i.OriginPubkey, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listPacketsByScopesTypes = `-- name: ListPacketsByScopesTypes :many +WITH page AS MATERIALIZED ( + SELECT x.packet_hash, x.last_heard_at AS k + FROM transport_scopes s + CROSS JOIN LATERAL ( + SELECT p.packet_hash, p.last_heard_at FROM packets p + WHERE p.scope_id = s.id + AND (p.last_heard_at, p.packet_hash) < ($1::timestamptz, $2::bytea) + AND p.payload_type = ANY($3::smallint[]) AND p.route_type = ANY($4::smallint[]) + ORDER BY p.last_heard_at DESC, p.packet_hash DESC + LIMIT $5 + ) x + WHERE s.name = ANY($6::text[]) + ORDER BY x.last_heard_at DESC, x.packet_hash DESC + LIMIT $5 +) +SELECT + p.packet_hash, + p.payload_type, + COALESCE(CASE + WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' + THEN p.parsed_payload #>> '{appData,name}' + WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' + AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' + AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' + THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') + WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') + AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' + AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' + THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') + END, '')::text AS summary, + p.route_type, + p.first_heard_at, + p.last_heard_at, + p.scope_id, + ts.name AS scope_name, + p.observation_count AS observation_count, + -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. + po.observer_id AS latest_observer_id, + o.display_name AS latest_observer_name, + COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, + COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, + COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, + COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, + po.path_bytes AS latest_observer_path_bytes, + p.raw_payload, + p.origin_pubkey +FROM page pg +JOIN packets p ON p.packet_hash = pg.packet_hash +LEFT JOIN LATERAL ( + SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes + FROM packet_observations + WHERE packet_hash = p.packet_hash + ORDER BY heard_at DESC, id DESC + LIMIT 1 +) po ON true +LEFT JOIN observers o ON o.id = po.observer_id +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +ORDER BY pg.k DESC, pg.packet_hash DESC +` + +type ListPacketsByScopesTypesParams struct { + CursorTs pgtype.Timestamptz `json:"cursor_ts"` + CursorHash []byte `json:"cursor_hash"` + PayloadTypes []int16 `json:"payload_types"` + RouteTypes []int16 `json:"route_types"` + PageLimit int32 `json:"page_limit"` + ScopeNames []string `json:"scope_names"` +} + +type ListPacketsByScopesTypesRow struct { + PacketHash []byte `json:"packet_hash"` + PayloadType int16 `json:"payload_type"` + Summary string `json:"summary"` + RouteType int16 `json:"route_type"` + FirstHeardAt pgtype.Timestamptz `json:"first_heard_at"` + LastHeardAt pgtype.Timestamptz `json:"last_heard_at"` + ScopeID *int32 `json:"scope_id"` + ScopeName *string `json:"scope_name"` + ObservationCount int64 `json:"observation_count"` + LatestObserverID uuid.UUID `json:"latest_observer_id"` + LatestObserverName *string `json:"latest_observer_name"` + LatestObserverIata string `json:"latest_observer_iata"` + LatestObserverPathLengthByte int16 `json:"latest_observer_path_length_byte"` + LatestObserverHashSize int16 `json:"latest_observer_hash_size"` + LatestObserverHopCount int16 `json:"latest_observer_hop_count"` + LatestObserverPathBytes []byte `json:"latest_observer_path_bytes"` + RawPayload []byte `json:"raw_payload"` + OriginPubkey []byte `json:"origin_pubkey"` +} + +// ListPacketsByScopes with the type tests inside each scope range (index-only through the included columns). +func (q *Queries) ListPacketsByScopesTypes(ctx context.Context, arg ListPacketsByScopesTypesParams) ([]ListPacketsByScopesTypesRow, error) { + rows, err := q.db.Query(ctx, listPacketsByScopesTypes, + arg.CursorTs, + arg.CursorHash, + arg.PayloadTypes, + arg.RouteTypes, + arg.PageLimit, + arg.ScopeNames, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListPacketsByScopesTypesRow{} + for rows.Next() { + var i ListPacketsByScopesTypesRow + if err := rows.Scan( + &i.PacketHash, + &i.PayloadType, + &i.Summary, + &i.RouteType, + &i.FirstHeardAt, + &i.LastHeardAt, + &i.ScopeID, + &i.ScopeName, + &i.ObservationCount, + &i.LatestObserverID, + &i.LatestObserverName, + &i.LatestObserverIata, + &i.LatestObserverPathLengthByte, + &i.LatestObserverHashSize, + &i.LatestObserverHopCount, + &i.LatestObserverPathBytes, + &i.RawPayload, + &i.OriginPubkey, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listPacketsByTypes = `-- name: ListPacketsByTypes :many +WITH page AS MATERIALIZED ( + SELECT x.packet_hash, x.last_heard_at AS k + FROM (SELECT DISTINCT unnest($1::smallint[])) pt(t) + CROSS JOIN (SELECT DISTINCT unnest($2::smallint[])) rt(t) + CROSS JOIN LATERAL ( + SELECT p.packet_hash, p.last_heard_at FROM packets p + WHERE p.payload_type = pt.t AND p.route_type = rt.t + AND (p.last_heard_at, p.packet_hash) < ($3::timestamptz, $4::bytea) + ORDER BY p.last_heard_at DESC, p.packet_hash DESC + LIMIT $5 + ) x + ORDER BY x.last_heard_at DESC, x.packet_hash DESC + LIMIT $5 +) +SELECT + p.packet_hash, + p.payload_type, + COALESCE(CASE + WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' + THEN p.parsed_payload #>> '{appData,name}' + WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' + AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' + AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' + THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') + WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') + AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' + AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' + THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') + END, '')::text AS summary, + p.route_type, + p.first_heard_at, + p.last_heard_at, + p.scope_id, + ts.name AS scope_name, + p.observation_count AS observation_count, + -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. + po.observer_id AS latest_observer_id, + o.display_name AS latest_observer_name, + COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, + COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, + COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, + COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, + po.path_bytes AS latest_observer_path_bytes, + p.raw_payload, + p.origin_pubkey +FROM page pg +JOIN packets p ON p.packet_hash = pg.packet_hash +LEFT JOIN LATERAL ( + SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes + FROM packet_observations + WHERE packet_hash = p.packet_hash + ORDER BY heard_at DESC, id DESC + LIMIT 1 +) po ON true +LEFT JOIN observers o ON o.id = po.observer_id +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +ORDER BY pg.k DESC, pg.packet_hash DESC +` + +type ListPacketsByTypesParams struct { + PayloadTypes []int16 `json:"payload_types"` + RouteTypes []int16 `json:"route_types"` + CursorTs pgtype.Timestamptz `json:"cursor_ts"` + CursorHash []byte `json:"cursor_hash"` + PageLimit int32 `json:"page_limit"` +} + +type ListPacketsByTypesRow struct { + PacketHash []byte `json:"packet_hash"` + PayloadType int16 `json:"payload_type"` + Summary string `json:"summary"` + RouteType int16 `json:"route_type"` + FirstHeardAt pgtype.Timestamptz `json:"first_heard_at"` + LastHeardAt pgtype.Timestamptz `json:"last_heard_at"` + ScopeID *int32 `json:"scope_id"` + ScopeName *string `json:"scope_name"` + ObservationCount int64 `json:"observation_count"` + LatestObserverID uuid.UUID `json:"latest_observer_id"` + LatestObserverName *string `json:"latest_observer_name"` + LatestObserverIata string `json:"latest_observer_iata"` + LatestObserverPathLengthByte int16 `json:"latest_observer_path_length_byte"` + LatestObserverHashSize int16 `json:"latest_observer_hash_size"` + LatestObserverHopCount int16 `json:"latest_observer_hop_count"` + LatestObserverPathBytes []byte `json:"latest_observer_path_bytes"` + RawPayload []byte `json:"raw_payload"` + OriginPubkey []byte `json:"origin_pubkey"` +} + +// One range per (payload type, route type) pair; DISTINCT keeps a repeated type from returning rows twice. +func (q *Queries) ListPacketsByTypes(ctx context.Context, arg ListPacketsByTypesParams) ([]ListPacketsByTypesRow, error) { + rows, err := q.db.Query(ctx, listPacketsByTypes, + arg.PayloadTypes, + arg.RouteTypes, + arg.CursorTs, + arg.CursorHash, + arg.PageLimit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListPacketsByTypesRow{} + for rows.Next() { + var i ListPacketsByTypesRow + if err := rows.Scan( + &i.PacketHash, + &i.PayloadType, + &i.Summary, + &i.RouteType, + &i.FirstHeardAt, + &i.LastHeardAt, + &i.ScopeID, + &i.ScopeName, + &i.ObservationCount, + &i.LatestObserverID, + &i.LatestObserverName, + &i.LatestObserverIata, + &i.LatestObserverPathLengthByte, + &i.LatestObserverHashSize, + &i.LatestObserverHopCount, + &i.LatestObserverPathBytes, + &i.RawPayload, + &i.OriginPubkey, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listPacketsPage = `-- name: ListPacketsPage :many +WITH page AS MATERIALIZED ( + SELECT p.packet_hash, p.last_heard_at AS k FROM packets p + WHERE (p.last_heard_at, p.packet_hash) < ($1::timestamptz, $2::bytea) + ORDER BY p.last_heard_at DESC, p.packet_hash DESC + LIMIT $3 +) +SELECT + p.packet_hash, + p.payload_type, + COALESCE(CASE + WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' + THEN p.parsed_payload #>> '{appData,name}' + WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' + AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' + AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' + THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') + WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') + AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' + AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' + THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') + END, '')::text AS summary, + p.route_type, + p.first_heard_at, + p.last_heard_at, + p.scope_id, + ts.name AS scope_name, + p.observation_count AS observation_count, + -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. + po.observer_id AS latest_observer_id, + o.display_name AS latest_observer_name, + COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, + COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, + COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, + COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, + po.path_bytes AS latest_observer_path_bytes, + p.raw_payload, + p.origin_pubkey +FROM page pg +JOIN packets p ON p.packet_hash = pg.packet_hash +LEFT JOIN LATERAL ( + SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes + FROM packet_observations + WHERE packet_hash = p.packet_hash + ORDER BY heard_at DESC, id DESC + LIMIT 1 +) po ON true +LEFT JOIN observers o ON o.id = po.observer_id +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +ORDER BY pg.k DESC, pg.packet_hash DESC +` + +type ListPacketsPageParams struct { + CursorTs pgtype.Timestamptz `json:"cursor_ts"` + CursorHash []byte `json:"cursor_hash"` + PageLimit int32 `json:"page_limit"` +} + +type ListPacketsPageRow struct { + PacketHash []byte `json:"packet_hash"` + PayloadType int16 `json:"payload_type"` + Summary string `json:"summary"` + RouteType int16 `json:"route_type"` + FirstHeardAt pgtype.Timestamptz `json:"first_heard_at"` + LastHeardAt pgtype.Timestamptz `json:"last_heard_at"` + ScopeID *int32 `json:"scope_id"` + ScopeName *string `json:"scope_name"` + ObservationCount int64 `json:"observation_count"` + LatestObserverID uuid.UUID `json:"latest_observer_id"` + LatestObserverName *string `json:"latest_observer_name"` + LatestObserverIata string `json:"latest_observer_iata"` + LatestObserverPathLengthByte int16 `json:"latest_observer_path_length_byte"` + LatestObserverHashSize int16 `json:"latest_observer_hash_size"` + LatestObserverHopCount int16 `json:"latest_observer_hop_count"` + LatestObserverPathBytes []byte `json:"latest_observer_path_bytes"` + RawPayload []byte `json:"raw_payload"` + OriginPubkey []byte `json:"origin_pubkey"` +} + +// Unfiltered packet page: newest (last_heard_at, packet_hash) first. +func (q *Queries) ListPacketsPage(ctx context.Context, arg ListPacketsPageParams) ([]ListPacketsPageRow, error) { + rows, err := q.db.Query(ctx, listPacketsPage, arg.CursorTs, arg.CursorHash, arg.PageLimit) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListPacketsPageRow{} + for rows.Next() { + var i ListPacketsPageRow + if err := rows.Scan( + &i.PacketHash, + &i.PayloadType, + &i.Summary, + &i.RouteType, + &i.FirstHeardAt, + &i.LastHeardAt, + &i.ScopeID, + &i.ScopeName, + &i.ObservationCount, + &i.LatestObserverID, + &i.LatestObserverName, + &i.LatestObserverIata, + &i.LatestObserverPathLengthByte, + &i.LatestObserverHashSize, + &i.LatestObserverHopCount, + &i.LatestObserverPathBytes, + &i.RawPayload, + &i.OriginPubkey, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listPacketsWindow = `-- name: ListPacketsWindow :many +WITH page AS MATERIALIZED ( + SELECT u.packet_hash, u.k FROM ( + (SELECT p.packet_hash, p.last_heard_at AS k FROM packets p + WHERE p.last_heard_at >= $1::timestamptz + AND (p.last_heard_at, p.packet_hash) < ($2::timestamptz, $3::bytea) + AND (p.first_heard_at >= $1 AND p.first_heard_at <= $4::timestamptz) IS TRUE + AND p.payload_type = ANY($5::smallint[]) AND p.route_type = ANY($6::smallint[]) + ORDER BY p.last_heard_at DESC, p.packet_hash DESC + LIMIT $7) + UNION ALL + (SELECT p.packet_hash, p.last_heard_at AS k FROM packets p + WHERE (p.first_heard_at > p.last_heard_at) IS TRUE + AND (p.first_heard_at >= $1 AND p.first_heard_at <= $4 + AND p.last_heard_at < $1 + AND (p.last_heard_at, p.packet_hash) < ($2, $3) + AND p.payload_type = ANY($5) AND p.route_type = ANY($6)) IS TRUE) + ) u + ORDER BY u.k DESC, u.packet_hash DESC + LIMIT $7 +) +SELECT + p.packet_hash, + p.payload_type, + COALESCE(CASE + WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' + THEN p.parsed_payload #>> '{appData,name}' + WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' + AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' + AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' + THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') + WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') + AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' + AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' + THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') + END, '')::text AS summary, + p.route_type, + p.first_heard_at, + p.last_heard_at, + p.scope_id, + ts.name AS scope_name, + p.observation_count AS observation_count, + -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. + po.observer_id AS latest_observer_id, + o.display_name AS latest_observer_name, + COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, + COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, + COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, + COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, + po.path_bytes AS latest_observer_path_bytes, + p.raw_payload, + p.origin_pubkey +FROM page pg +JOIN packets p ON p.packet_hash = pg.packet_hash +LEFT JOIN LATERAL ( + SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes + FROM packet_observations + WHERE packet_hash = p.packet_hash + ORDER BY heard_at DESC, id DESC + LIMIT 1 +) po ON true +LEFT JOIN observers o ON o.id = po.observer_id +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +ORDER BY pg.k DESC, pg.packet_hash DESC +` + +type ListPacketsWindowParams struct { + SinceTs pgtype.Timestamptz `json:"since_ts"` + CursorTs pgtype.Timestamptz `json:"cursor_ts"` + CursorHash []byte `json:"cursor_hash"` + UntilTs pgtype.Timestamptz `json:"until_ts"` + PayloadTypes []int16 `json:"payload_types"` + RouteTypes []int16 `json:"route_types"` + PageLimit int32 `json:"page_limit"` +} + +type ListPacketsWindowRow struct { + PacketHash []byte `json:"packet_hash"` + PayloadType int16 `json:"payload_type"` + Summary string `json:"summary"` + RouteType int16 `json:"route_type"` + FirstHeardAt pgtype.Timestamptz `json:"first_heard_at"` + LastHeardAt pgtype.Timestamptz `json:"last_heard_at"` + ScopeID *int32 `json:"scope_id"` + ScopeName *string `json:"scope_name"` + ObservationCount int64 `json:"observation_count"` + LatestObserverID uuid.UUID `json:"latest_observer_id"` + LatestObserverName *string `json:"latest_observer_name"` + LatestObserverIata string `json:"latest_observer_iata"` + LatestObserverPathLengthByte int16 `json:"latest_observer_path_length_byte"` + LatestObserverHashSize int16 `json:"latest_observer_hash_size"` + LatestObserverHopCount int16 `json:"latest_observer_hop_count"` + LatestObserverPathBytes []byte `json:"latest_observer_path_bytes"` + RawPayload []byte `json:"raw_payload"` + OriginPubkey []byte `json:"origin_pubkey"` +} + +// first_heard_at window. Branch 1 walks last_heard_at down to since; branch 2 adds the rows a concurrent +// upsert left with last_heard_at below first_heard_at, which that bound cannot see. +func (q *Queries) ListPacketsWindow(ctx context.Context, arg ListPacketsWindowParams) ([]ListPacketsWindowRow, error) { + rows, err := q.db.Query(ctx, listPacketsWindow, + arg.SinceTs, + arg.CursorTs, + arg.CursorHash, + arg.UntilTs, + arg.PayloadTypes, + arg.RouteTypes, + arg.PageLimit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListPacketsWindowRow{} + for rows.Next() { + var i ListPacketsWindowRow + if err := rows.Scan( + &i.PacketHash, + &i.PayloadType, + &i.Summary, + &i.RouteType, + &i.FirstHeardAt, + &i.LastHeardAt, + &i.ScopeID, + &i.ScopeName, + &i.ObservationCount, + &i.LatestObserverID, + &i.LatestObserverName, + &i.LatestObserverIata, + &i.LatestObserverPathLengthByte, + &i.LatestObserverHashSize, + &i.LatestObserverHopCount, + &i.LatestObserverPathBytes, + &i.RawPayload, + &i.OriginPubkey, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listPacketsWindowScoped = `-- name: ListPacketsWindowScoped :many +WITH page AS MATERIALIZED ( + SELECT u.packet_hash, u.k FROM ( + (SELECT x.packet_hash, x.last_heard_at AS k + FROM transport_scopes s + CROSS JOIN LATERAL ( + SELECT p.packet_hash, p.last_heard_at FROM packets p + WHERE p.scope_id = s.id + AND p.last_heard_at >= $1::timestamptz + AND (p.last_heard_at, p.packet_hash) < ($2::timestamptz, $3::bytea) + AND (p.first_heard_at >= $1 AND p.first_heard_at <= $4::timestamptz) IS TRUE + AND p.payload_type = ANY($5::smallint[]) AND p.route_type = ANY($6::smallint[]) + ORDER BY p.last_heard_at DESC, p.packet_hash DESC + LIMIT $7) x + WHERE s.name = ANY($8::text[])) + UNION ALL + (SELECT p.packet_hash, p.last_heard_at AS k FROM packets p + WHERE (p.first_heard_at > p.last_heard_at) IS TRUE + AND (p.first_heard_at >= $1 AND p.first_heard_at <= $4 + AND p.last_heard_at < $1 + AND (p.last_heard_at, p.packet_hash) < ($2, $3) + AND p.payload_type = ANY($5) AND p.route_type = ANY($6) + AND p.scope_id = ANY(ARRAY(SELECT id FROM transport_scopes WHERE name = ANY($8)))) IS TRUE) + ) u + ORDER BY u.k DESC, u.packet_hash DESC + LIMIT $7 +) +SELECT + p.packet_hash, + p.payload_type, + COALESCE(CASE + WHEN p.payload_type = 4 AND jsonb_typeof(p.parsed_payload #> '{appData,name}') = 'string' + THEN p.parsed_payload #>> '{appData,name}' + WHEN p.payload_type = 3 AND p.parsed_payload ->> 'type' = 'ACK' + AND jsonb_typeof(p.parsed_payload -> 'checksum') = 'string' + AND p.parsed_payload ->> 'checksum' ~ '^[0-9a-fA-F]{8}$' + THEN 'ACK ' || lower(p.parsed_payload ->> 'checksum') + WHEN p.payload_type = 9 AND p.parsed_payload ->> 'type' IN ('TRACE', 'PING') + AND jsonb_typeof(p.parsed_payload -> 'traceTag') = 'string' + AND p.parsed_payload ->> 'traceTag' ~ '^[0-9a-fA-F]{8}$' + THEN (p.parsed_payload ->> 'type') || ' ' || lower(p.parsed_payload ->> 'traceTag') + END, '')::text AS summary, + p.route_type, + p.first_heard_at, + p.last_heard_at, + p.scope_id, + ts.name AS scope_name, + p.observation_count AS observation_count, + -- sqlc loses LATERAL nullability; these scalar defaults are ignored when observer_id is NULL. + po.observer_id AS latest_observer_id, + o.display_name AS latest_observer_name, + COALESCE(po.iata, ''::bpchar) AS latest_observer_iata, + COALESCE(po.path_length_byte, 0::smallint) AS latest_observer_path_length_byte, + COALESCE(po.hash_size, 0::smallint) AS latest_observer_hash_size, + COALESCE(po.hop_count, 0::smallint) AS latest_observer_hop_count, + po.path_bytes AS latest_observer_path_bytes, + p.raw_payload, + p.origin_pubkey +FROM page pg +JOIN packets p ON p.packet_hash = pg.packet_hash +LEFT JOIN LATERAL ( + SELECT observer_id, iata, path_length_byte, hash_size, hop_count, path_bytes + FROM packet_observations + WHERE packet_hash = p.packet_hash + ORDER BY heard_at DESC, id DESC + LIMIT 1 +) po ON true +LEFT JOIN observers o ON o.id = po.observer_id +LEFT JOIN transport_scopes ts ON ts.id = p.scope_id +ORDER BY pg.k DESC, pg.packet_hash DESC +` + +type ListPacketsWindowScopedParams struct { + SinceTs pgtype.Timestamptz `json:"since_ts"` + CursorTs pgtype.Timestamptz `json:"cursor_ts"` + CursorHash []byte `json:"cursor_hash"` + UntilTs pgtype.Timestamptz `json:"until_ts"` + PayloadTypes []int16 `json:"payload_types"` + RouteTypes []int16 `json:"route_types"` + PageLimit int32 `json:"page_limit"` + ScopeNames []string `json:"scope_names"` +} + +type ListPacketsWindowScopedRow struct { + PacketHash []byte `json:"packet_hash"` + PayloadType int16 `json:"payload_type"` + Summary string `json:"summary"` + RouteType int16 `json:"route_type"` + FirstHeardAt pgtype.Timestamptz `json:"first_heard_at"` + LastHeardAt pgtype.Timestamptz `json:"last_heard_at"` + ScopeID *int32 `json:"scope_id"` + ScopeName *string `json:"scope_name"` + ObservationCount int64 `json:"observation_count"` + LatestObserverID uuid.UUID `json:"latest_observer_id"` + LatestObserverName *string `json:"latest_observer_name"` + LatestObserverIata string `json:"latest_observer_iata"` + LatestObserverPathLengthByte int16 `json:"latest_observer_path_length_byte"` + LatestObserverHashSize int16 `json:"latest_observer_hash_size"` + LatestObserverHopCount int16 `json:"latest_observer_hop_count"` + LatestObserverPathBytes []byte `json:"latest_observer_path_bytes"` + RawPayload []byte `json:"raw_payload"` + OriginPubkey []byte `json:"origin_pubkey"` +} + +// ListPacketsWindow with branch 1 run once per named scope and branch 2 limited to those scopes. +func (q *Queries) ListPacketsWindowScoped(ctx context.Context, arg ListPacketsWindowScopedParams) ([]ListPacketsWindowScopedRow, error) { + rows, err := q.db.Query(ctx, listPacketsWindowScoped, + arg.SinceTs, + arg.CursorTs, + arg.CursorHash, + arg.UntilTs, + arg.PayloadTypes, + arg.RouteTypes, + arg.PageLimit, + arg.ScopeNames, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListPacketsWindowScopedRow{} + for rows.Next() { + var i ListPacketsWindowScopedRow + if err := rows.Scan( + &i.PacketHash, + &i.PayloadType, + &i.Summary, + &i.RouteType, + &i.FirstHeardAt, + &i.LastHeardAt, + &i.ScopeID, + &i.ScopeName, + &i.ObservationCount, + &i.LatestObserverID, + &i.LatestObserverName, + &i.LatestObserverIata, + &i.LatestObserverPathLengthByte, + &i.LatestObserverHashSize, + &i.LatestObserverHopCount, + &i.LatestObserverPathBytes, + &i.RawPayload, + &i.OriginPubkey, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listRegionState = `-- name: ListRegionState :many SELECT r.slug, r.name, COALESCE(r.display_order, 0)::int AS display_order, r.imported, r.center_lat, r.center_lng, COALESCE(array_agg(ri.iata::text ORDER BY ri.iata) FILTER (WHERE ri.iata IS NOT NULL), '{}')::text[] AS iatas diff --git a/docs/docs.go b/docs/docs.go index 52a6287..1f3a560 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1296,6 +1296,12 @@ const docTemplate = `{ "name": "cursor", "in": "query" }, + { + "type": "string", + "description": "Opaque nextPageCursor from a previous response; keeps millisecond ties. Wins over cursor when both are sent", + "name": "pageCursor", + "in": "query" + }, { "maximum": 200, "minimum": 1, @@ -1309,7 +1315,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_NodeSummary" + "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.NodePage" } }, "400": { @@ -1536,10 +1542,16 @@ const docTemplate = `{ }, { "type": "integer", - "description": "last_seen epoch ms of last item for pagination", + "description": "last_seen epoch ms of the last item", "name": "cursor", "in": "query" }, + { + "type": "string", + "description": "Opaque nextPageCursor from a previous response; keeps millisecond ties. Wins over cursor when both are sent", + "name": "pageCursor", + "in": "query" + }, { "maximum": 200, "minimum": 1, @@ -1553,7 +1565,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_ObserverSummary" + "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverPage" } }, "400": { @@ -2004,10 +2016,16 @@ const docTemplate = `{ }, { "type": "integer", - "description": "epoch ms of last item for pagination; last_heard_at, or site-local heard_at when iatas is set", + "description": "epoch ms of the last item; last_heard_at, or site-local heard_at when iatas is set. Prefer pageCursor where nextPageCursor is returned", "name": "cursor", "in": "query" }, + { + "type": "string", + "description": "Opaque nextPageCursor from a previous response; keeps millisecond ties. Wins over cursor when both are sent. Ignored with hashPrefix or an IATA or region filter, which page by cursor", + "name": "pageCursor", + "in": "query" + }, { "maximum": 200, "minimum": 1, @@ -2027,7 +2045,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "type": "object" + "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.PacketPage" } }, "400": { @@ -4581,6 +4599,26 @@ const docTemplate = `{ } } }, + "github_com_MeshCore-Beacon_beacon-server_internal_api.NodePage": { + "type": "object", + "properties": { + "hasMore": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.NodeSummary" + } + }, + "nextCursor": { + "type": "integer" + }, + "nextPageCursor": { + "type": "string" + } + } + }, "github_com_MeshCore-Beacon_beacon-server_internal_api.NodeSummary": { "type": "object", "properties": { @@ -5052,6 +5090,26 @@ const docTemplate = `{ } } }, + "github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverPage": { + "type": "object", + "properties": { + "hasMore": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverSummary" + } + }, + "nextCursor": { + "type": "integer" + }, + "nextPageCursor": { + "type": "string" + } + } + }, "github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverSummary": { "type": "object", "properties": { @@ -5366,6 +5424,26 @@ const docTemplate = `{ } } }, + "github_com_MeshCore-Beacon_beacon-server_internal_api.PacketPage": { + "type": "object", + "properties": { + "hasMore": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.PacketSummary" + } + }, + "nextCursor": { + "type": "integer" + }, + "nextPageCursor": { + "type": "string" + } + } + }, "github_com_MeshCore-Beacon_beacon-server_internal_api.PacketPathLength": { "type": "object", "properties": { @@ -5471,40 +5549,6 @@ const docTemplate = `{ } } }, - "github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_NodeSummary": { - "type": "object", - "properties": { - "hasMore": { - "type": "boolean" - }, - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.NodeSummary" - } - }, - "nextCursor": { - "type": "integer" - } - } - }, - "github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_ObserverSummary": { - "type": "object", - "properties": { - "hasMore": { - "type": "boolean" - }, - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverSummary" - } - }, - "nextCursor": { - "type": "integer" - } - } - }, "github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_PacketObservationSummary": { "type": "object", "properties": { @@ -6555,7 +6599,7 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "2.0.8", + Version: "2.1.0", Host: "localhost:8080", BasePath: "/api/v1", Schemes: []string{"http", "https"}, diff --git a/docs/swagger.json b/docs/swagger.json index 8bfac5c..8698d2b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -15,7 +15,7 @@ "license": { "name": "AGPL-3-or-later" }, - "version": "2.0.8" + "version": "2.1.0" }, "host": "localhost:8080", "basePath": "/api/v1", @@ -1294,6 +1294,12 @@ "name": "cursor", "in": "query" }, + { + "type": "string", + "description": "Opaque nextPageCursor from a previous response; keeps millisecond ties. Wins over cursor when both are sent", + "name": "pageCursor", + "in": "query" + }, { "maximum": 200, "minimum": 1, @@ -1307,7 +1313,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_NodeSummary" + "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.NodePage" } }, "400": { @@ -1534,10 +1540,16 @@ }, { "type": "integer", - "description": "last_seen epoch ms of last item for pagination", + "description": "last_seen epoch ms of the last item", "name": "cursor", "in": "query" }, + { + "type": "string", + "description": "Opaque nextPageCursor from a previous response; keeps millisecond ties. Wins over cursor when both are sent", + "name": "pageCursor", + "in": "query" + }, { "maximum": 200, "minimum": 1, @@ -1551,7 +1563,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_ObserverSummary" + "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverPage" } }, "400": { @@ -2002,10 +2014,16 @@ }, { "type": "integer", - "description": "epoch ms of last item for pagination; last_heard_at, or site-local heard_at when iatas is set", + "description": "epoch ms of the last item; last_heard_at, or site-local heard_at when iatas is set. Prefer pageCursor where nextPageCursor is returned", "name": "cursor", "in": "query" }, + { + "type": "string", + "description": "Opaque nextPageCursor from a previous response; keeps millisecond ties. Wins over cursor when both are sent. Ignored with hashPrefix or an IATA or region filter, which page by cursor", + "name": "pageCursor", + "in": "query" + }, { "maximum": 200, "minimum": 1, @@ -2025,7 +2043,7 @@ "200": { "description": "OK", "schema": { - "type": "object" + "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.PacketPage" } }, "400": { @@ -4579,6 +4597,26 @@ } } }, + "github_com_MeshCore-Beacon_beacon-server_internal_api.NodePage": { + "type": "object", + "properties": { + "hasMore": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.NodeSummary" + } + }, + "nextCursor": { + "type": "integer" + }, + "nextPageCursor": { + "type": "string" + } + } + }, "github_com_MeshCore-Beacon_beacon-server_internal_api.NodeSummary": { "type": "object", "properties": { @@ -5050,6 +5088,26 @@ } } }, + "github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverPage": { + "type": "object", + "properties": { + "hasMore": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverSummary" + } + }, + "nextCursor": { + "type": "integer" + }, + "nextPageCursor": { + "type": "string" + } + } + }, "github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverSummary": { "type": "object", "properties": { @@ -5364,6 +5422,26 @@ } } }, + "github_com_MeshCore-Beacon_beacon-server_internal_api.PacketPage": { + "type": "object", + "properties": { + "hasMore": { + "type": "boolean" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.PacketSummary" + } + }, + "nextCursor": { + "type": "integer" + }, + "nextPageCursor": { + "type": "string" + } + } + }, "github_com_MeshCore-Beacon_beacon-server_internal_api.PacketPathLength": { "type": "object", "properties": { @@ -5469,40 +5547,6 @@ } } }, - "github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_NodeSummary": { - "type": "object", - "properties": { - "hasMore": { - "type": "boolean" - }, - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.NodeSummary" - } - }, - "nextCursor": { - "type": "integer" - } - } - }, - "github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_ObserverSummary": { - "type": "object", - "properties": { - "hasMore": { - "type": "boolean" - }, - "items": { - "type": "array", - "items": { - "$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverSummary" - } - }, - "nextCursor": { - "type": "integer" - } - } - }, "github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_PacketObservationSummary": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 312edce..89e1942 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -589,6 +589,19 @@ definitions: snr: type: number type: object + github_com_MeshCore-Beacon_beacon-server_internal_api.NodePage: + properties: + hasMore: + type: boolean + items: + items: + $ref: '#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.NodeSummary' + type: array + nextCursor: + type: integer + nextPageCursor: + type: string + type: object github_com_MeshCore-Beacon_beacon-server_internal_api.NodeSummary: properties: defaultScope: @@ -927,6 +940,19 @@ definitions: description: '"online" or "offline" derived from last_status_at' type: string type: object + github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverPage: + properties: + hasMore: + type: boolean + items: + items: + $ref: '#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverSummary' + type: array + nextCursor: + type: integer + nextPageCursor: + type: string + type: object github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverSummary: properties: displayName: @@ -1152,6 +1178,19 @@ definitions: snr: type: number type: object + github_com_MeshCore-Beacon_beacon-server_internal_api.PacketPage: + properties: + hasMore: + type: boolean + items: + items: + $ref: '#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.PacketSummary' + type: array + nextCursor: + type: integer + nextPageCursor: + type: string + type: object github_com_MeshCore-Beacon_beacon-server_internal_api.PacketPathLength: properties: hashSize: @@ -1223,28 +1262,6 @@ definitions: nextCursor: type: integer type: object - github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_NodeSummary: - properties: - hasMore: - type: boolean - items: - items: - $ref: '#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.NodeSummary' - type: array - nextCursor: - type: integer - type: object - github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_ObserverSummary: - properties: - hasMore: - type: boolean - items: - items: - $ref: '#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverSummary' - type: array - nextCursor: - type: integer - type: object ? github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_PacketObservationSummary : properties: hasMore: @@ -1938,7 +1955,7 @@ info: name: AGPL-3-or-later termsOfService: https://github.com/MeshCore-Beacon/beacon-server title: MeshCore Beacon API - version: 2.0.8 + version: 2.1.0 paths: /admin/accounts: get: @@ -2814,6 +2831,11 @@ paths: in: query name: cursor type: integer + - description: Opaque nextPageCursor from a previous response; keeps millisecond + ties. Wins over cursor when both are sent + in: query + name: pageCursor + type: string - description: Max results (default 50); must be positive, values above 200 are clamped in: query @@ -2827,7 +2849,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_NodeSummary' + $ref: '#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.NodePage' "400": description: Bad Request schema: @@ -2973,10 +2995,15 @@ paths: in: query name: scope type: string - - description: last_seen epoch ms of last item for pagination + - description: last_seen epoch ms of the last item in: query name: cursor type: integer + - description: Opaque nextPageCursor from a previous response; keeps millisecond + ties. Wins over cursor when both are sent + in: query + name: pageCursor + type: string - description: Max results (default 50); must be positive, values above 200 are clamped in: query @@ -2990,7 +3017,7 @@ paths: "200": description: OK schema: - $ref: '#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.Page-github_com_MeshCore-Beacon_beacon-server_internal_api_ObserverSummary' + $ref: '#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.ObserverPage' "400": description: Bad Request schema: @@ -3298,11 +3325,17 @@ paths: in: query name: until type: integer - - description: epoch ms of last item for pagination; last_heard_at, or site-local - heard_at when iatas is set + - description: epoch ms of the last item; last_heard_at, or site-local heard_at + when iatas is set. Prefer pageCursor where nextPageCursor is returned in: query name: cursor type: integer + - description: Opaque nextPageCursor from a previous response; keeps millisecond + ties. Wins over cursor when both are sent. Ignored with hashPrefix or an + IATA or region filter, which page by cursor + in: query + name: pageCursor + type: string - description: Max results (default 50); must be positive, values above 200 are clamped in: query @@ -3321,7 +3354,7 @@ paths: "200": description: OK schema: - type: object + $ref: '#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.PacketPage' "400": description: Bad Request schema: diff --git a/internal/api/cursors.go b/internal/api/cursors.go new file mode 100644 index 0000000..a079243 --- /dev/null +++ b/internal/api/cursors.go @@ -0,0 +1,125 @@ +// Copyright 2026 Beacon Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +package api + +import ( + "encoding/hex" + "errors" + "fmt" + "strconv" + "strings" + "time" + + "github.com/google/uuid" +) + +// maxPageCursorLen bounds every pageCursor before it is split or decoded. +const maxPageCursorLen = 160 + +// maxPacketCursorHash is the longest packet hash a cursor may carry, in bytes. +const maxPacketCursorHash = 64 + +var errPageCursor = errors.New("invalid pageCursor") + +// PacketCursor is a boundary in (last_heard_at DESC, packet_hash DESC) order. +type PacketCursor struct { + At time.Time + Hash []byte +} + +func (c PacketCursor) String() string { + return fmt.Sprintf("v1:%d:%x", c.At.UnixMicro(), c.Hash) +} + +// ParsePacketCursor parses the opaque, versioned cursor returned by PacketPage. +// The returned Hash is never nil. +func ParsePacketCursor(raw string) (*PacketCursor, error) { + at, tail, err := splitPageCursor(raw) + if err != nil { + return nil, err + } + if tail == "" || len(tail) > 2*maxPacketCursorHash { + return nil, errPageCursor + } + hash, err := hex.DecodeString(tail) + if err != nil || len(hash) == 0 { + return nil, errPageCursor + } + return &PacketCursor{At: at, Hash: hash}, nil +} + +// NodeCursor is a boundary in (last_seen DESC, id DESC) order. +type NodeCursor struct { + LastSeen time.Time + ID uuid.UUID +} + +func (c NodeCursor) String() string { + return fmt.Sprintf("v1:%d:%s", c.LastSeen.UnixMicro(), c.ID) +} + +// ParseNodeCursor parses the opaque, versioned cursor returned by NodePage. +func ParseNodeCursor(raw string) (*NodeCursor, error) { + at, id, err := parseUUIDCursor(raw) + if err != nil { + return nil, err + } + return &NodeCursor{LastSeen: at, ID: id}, nil +} + +// ObserverCursor is a boundary in (last_seen DESC, id DESC) order. +type ObserverCursor struct { + LastSeen time.Time + ID uuid.UUID +} + +func (c ObserverCursor) String() string { + return fmt.Sprintf("v1:%d:%s", c.LastSeen.UnixMicro(), c.ID) +} + +// ParseObserverCursor parses the opaque, versioned cursor returned by ObserverPage. +func ParseObserverCursor(raw string) (*ObserverCursor, error) { + at, id, err := parseUUIDCursor(raw) + if err != nil { + return nil, err + } + return &ObserverCursor{LastSeen: at, ID: id}, nil +} + +// parseUUIDCursor accepts only the canonical 36-character hyphenated UUID form. +func parseUUIDCursor(raw string) (time.Time, uuid.UUID, error) { + at, tail, err := splitPageCursor(raw) + if err != nil { + return time.Time{}, uuid.Nil, err + } + if len(tail) != 36 { + return time.Time{}, uuid.Nil, errPageCursor + } + id, err := uuid.Parse(tail) + if err != nil { + return time.Time{}, uuid.Nil, errPageCursor + } + return at, id, nil +} + +// splitPageCursor checks the "v1::" frame, with micros in +// canonical decimal form, and returns the UTC time and the unparsed tail. +func splitPageCursor(raw string) (time.Time, string, error) { + if len(raw) > maxPageCursorLen { + return time.Time{}, "", errPageCursor + } + parts := strings.Split(raw, ":") + if len(parts) != 3 || parts[0] != "v1" { + return time.Time{}, "", errPageCursor + } + micros, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil || strconv.FormatInt(micros, 10) != parts[1] { + return time.Time{}, "", errPageCursor // also rejects "+1", "01", "-0" + } + at := time.UnixMicro(micros).UTC() + if at.Year() < 1 || at.Year() > 9999 { + return time.Time{}, "", errPageCursor + } + return at, parts[2], nil +} diff --git a/internal/api/cursors_test.go b/internal/api/cursors_test.go new file mode 100644 index 0000000..0ca990f --- /dev/null +++ b/internal/api/cursors_test.go @@ -0,0 +1,152 @@ +// Copyright 2026 Beacon Contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +package api + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + "time" + + "github.com/google/uuid" +) + +func TestPacketCursorRoundTrip(t *testing.T) { + c := PacketCursor{At: time.Date(2026, 10, 19, 20, 43, 45, 19_123_000, time.UTC), Hash: []byte{0xab, 0x01}} + if got, want := c.String(), "v1:1792442625019123:ab01"; got != want { + t.Fatalf("String() = %q, want %q", got, want) + } + got, err := ParsePacketCursor(c.String()) + if err != nil { + t.Fatalf("ParsePacketCursor: %v", err) + } + if !got.At.Equal(c.At) || !bytes.Equal(got.Hash, c.Hash) { + t.Fatalf("round trip = %+v, want %+v", got, c) + } + if got.String() != c.String() { + t.Fatalf("re-format = %q, want %q", got.String(), c.String()) + } + upper, err := ParsePacketCursor("v1:1792442625019123:AB01") + if err != nil || !bytes.Equal(upper.Hash, []byte{0xab, 0x01}) { + t.Fatalf("upper-case hash = %+v, %v", upper, err) + } + for _, at := range []time.Time{ + time.UnixMicro(0), time.UnixMicro(-1), + time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC), + time.Date(9999, 12, 31, 23, 59, 59, 999999000, time.UTC), + time.Date(2026, 9, 8, 12, 0, 0, 123456000, time.FixedZone("offset", 3600)), + } { + hash := bytes.Repeat([]byte{0xff}, 64) + got, err := ParsePacketCursor(PacketCursor{At: at, Hash: hash}.String()) + if err != nil || !got.At.Equal(at) || !bytes.Equal(got.Hash, hash) { + t.Fatalf("round trip at %v: %+v, %v", at, got, err) + } + } +} + +// A nil []byte goes to Postgres as NULL, and a row comparison against NULL +// matches nothing, so a parsed cursor must always carry a non-nil hash. +func TestParsePacketCursorHashNeverNil(t *testing.T) { + got, err := ParsePacketCursor("v1:1:00") + if err != nil { + t.Fatal(err) + } + if got.Hash == nil || !bytes.Equal(got.Hash, []byte{0}) { + t.Fatalf("hash = %#v, want non-nil []byte{0}", got.Hash) + } +} + +func TestParsePacketCursorRejects(t *testing.T) { + for _, raw := range []string{ + "", "v1", "v2:1:ab", "v1:x:ab", "v1:1:", "v1:1:abc", "v1:1:zz", + "v1:1:" + strings.Repeat("ab", 65), + "v1:253402300800000000:ab", "v1:-62135596800000001:ab", + "v1:1:ab:cd", strings.Repeat("9", 161), + "v1:1.5:ab", "v1: 1:ab", "v1:1: ab", "v1:9223372036854775808:ab", + "v1:+1:ab", "v1:01:ab", "v1:-0:ab", "v1::ab", + } { + got, err := ParsePacketCursor(raw) + if err == nil || got != nil { + t.Errorf("accepted invalid cursor %q: %+v", raw, got) + continue + } + if err.Error() != "invalid pageCursor" { + t.Errorf("%q: error %q, want %q", raw, err, "invalid pageCursor") + } + } + // Longest valid cursor sits well under the 160-character cap. + longest := "v1:-62135596800000000:" + strings.Repeat("ab", 64) + if _, err := ParsePacketCursor(longest); err != nil || len(longest) > 160 { + t.Fatalf("longest valid cursor (%d chars) rejected: %v", len(longest), err) + } +} + +func TestNodeAndObserverCursors(t *testing.T) { + id := uuid.MustParse("3f8a2c1e-9b7d-4e6f-a1b2-c3d4e5f60718") + at := time.Date(2026, 10, 19, 20, 43, 45, 19_123_000, time.UTC) + + n := NodeCursor{LastSeen: at, ID: id} + if got, want := n.String(), "v1:1792442625019123:3f8a2c1e-9b7d-4e6f-a1b2-c3d4e5f60718"; got != want { + t.Fatalf("NodeCursor.String() = %q, want %q", got, want) + } + gotN, err := ParseNodeCursor(n.String()) + if err != nil || !gotN.LastSeen.Equal(at) || gotN.ID != id || gotN.String() != n.String() { + t.Fatalf("node round trip = %+v, %v", gotN, err) + } + + o := ObserverCursor{LastSeen: at, ID: id} + if o.String() != n.String() { + t.Fatalf("ObserverCursor.String() = %q, want %q", o.String(), n.String()) + } + gotO, err := ParseObserverCursor(o.String()) + if err != nil || !gotO.LastSeen.Equal(at) || gotO.ID != id || gotO.String() != o.String() { + t.Fatalf("observer round trip = %+v, %v", gotO, err) + } + + nilRaw := "v1:1:" + uuid.Nil.String() + if c, err := ParseNodeCursor(nilRaw); err != nil || c.ID != uuid.Nil { + t.Fatalf("nil uuid node cursor: %+v, %v", c, err) + } + if c, err := ParseObserverCursor(nilRaw); err != nil || c.ID != uuid.Nil { + t.Fatalf("nil uuid observer cursor: %+v, %v", c, err) + } + + for _, raw := range []string{ + "", "v1", "v2:1:" + id.String(), "v1:x:" + id.String(), "v1:1:", + "v1:1:not-a-uuid", + "v1:1:{" + id.String() + "}", + "v1:1:3f8a2c1e9b7d4e6fa1b2c3d4e5f60718", + "v1:1:urn:uuid:" + id.String(), + "v1:253402300800000000:" + id.String(), + "v1:-62135596800000001:" + id.String(), + "v1:1:" + id.String() + ":x", + strings.Repeat("9", 161), + } { + if c, err := ParseNodeCursor(raw); err == nil || c != nil || err.Error() != "invalid pageCursor" { + t.Errorf("node accepted %q: %+v, %v", raw, c, err) + } + if c, err := ParseObserverCursor(raw); err == nil || c != nil || err.Error() != "invalid pageCursor" { + t.Errorf("observer accepted %q: %+v, %v", raw, c, err) + } + } +} + +func TestPageCursorJSON(t *testing.T) { + cur := "v1:1:ab" + for name, v := range map[string]any{ + "packet": PacketPage{Page: Page[PacketSummary]{Items: []PacketSummary{}}}, + "node": NodePage{Page: Page[NodeSummary]{Items: []NodeSummary{}}}, + "observer": ObserverPage{Page: Page[ObserverSummary]{Items: []ObserverSummary{}}}, + } { + b, err := json.Marshal(v) + if err != nil || string(b) != `{"items":[],"hasMore":false}` { + t.Errorf("%s empty page = %s, %v", name, b, err) + } + } + b, _ := json.Marshal(PacketPage{Page: Page[PacketSummary]{Items: []PacketSummary{}, HasMore: true}, NextPageCursor: &cur}) + if string(b) != `{"items":[],"hasMore":true,"nextPageCursor":"v1:1:ab"}` { + t.Errorf("packet page with cursor = %s", b) + } +} diff --git a/internal/api/handlers/limits_test.go b/internal/api/handlers/limits_test.go index 3766f81..5d52c38 100644 --- a/internal/api/handlers/limits_test.go +++ b/internal/api/handlers/limits_test.go @@ -39,30 +39,30 @@ func TestListLimits(t *testing.T) { got = limit return nil, nil }, - listObservers: func(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, limit int32) (api.Page[api.ObserverSummary], error) { + listObservers: func(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, pageCursor *api.ObserverCursor, limit int32) (api.ObserverPage, error) { calls++ got = limit - return api.Page[api.ObserverSummary]{}, nil + return api.ObserverPage{}, nil }, listObserverAdverts: func(ctx context.Context, observerID uuid.UUID, cursor int64, limit int32) (api.Page[api.AdvertObservation], error) { calls++ got = limit return api.Page[api.AdvertObservation]{}, nil }, - listNodes: func(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, limit int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) { + listNodes: func(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, pageCursor *api.NodeCursor, limit int32, includeNeighbors bool) (api.NodePage, error) { calls++ got = limit - return api.Page[api.NodeSummary]{}, nil + return api.NodePage{}, nil }, listNodeObservations: func(ctx context.Context, nodeID uuid.UUID, cursor int64, limit int32) (api.Page[api.PacketObservationSummary], error) { calls++ got = limit return api.Page[api.PacketObservationSummary]{}, nil }, - listPackets: func(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) { + listPackets: func(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, pageCursor *api.PacketCursor, limit int32) (api.PacketPage, error) { calls++ got = limit - return api.Page[api.PacketSummary]{}, nil + return api.PacketPage{Page: api.Page[api.PacketSummary]{}}, nil }, listPacketsAfterID: func(ctx context.Context, afterObservationID int64, payloadType, routeType int16, iatas []string, scope string, limit int32) ([]api.PacketSummary, error) { calls++ diff --git a/internal/api/handlers/nodes.go b/internal/api/handlers/nodes.go index 1d2f09a..2f531f0 100644 --- a/internal/api/handlers/nodes.go +++ b/internal/api/handlers/nodes.go @@ -51,8 +51,9 @@ func NodesRouter(reader api.Reader) http.Handler { // @Param supportsMultibyteTraces query bool false "Filter by multibyte trace support (true/false); omit for no filter" // @Param neighbors query bool false "Include each node's known neighbor IDs (neighborIds field). Bare ?neighbors or ?neighbors=true enables it; omit/false for none" // @Param cursor query int false "last_seen epoch ms of last item for pagination" +// @Param pageCursor query string false "Opaque nextPageCursor from a previous response; keeps millisecond ties. Wins over cursor when both are sent" // @Param limit query int false "Max results (default 50); must be positive, values above 200 are clamped" minimum(1) maximum(200) -// @Success 200 {object} api.Page[api.NodeSummary] +// @Success 200 {object} api.NodePage // @Failure 400 {object} handlers.APIError // @Failure 500 {object} handlers.APIError // @Router /nodes [get] @@ -83,6 +84,15 @@ func listNodes(reader api.Reader) http.HandlerFunc { } cursor = c } + var pageCursor *api.NodeCursor + if raw := r.URL.Query().Get("pageCursor"); raw != "" { + pc, err := api.ParseNodeCursor(raw) + if err != nil { + respondError(w, http.StatusBadRequest, "invalid pageCursor") + return + } + pageCursor = pc + } var pubkey []byte if pubkeyParam := strings.ToLower(r.URL.Query().Get("pubkey")); pubkeyParam != "" { b, err := hex.DecodeString(pubkeyParam) @@ -140,7 +150,7 @@ func listNodes(reader api.Reader) http.HandlerFunc { includeNeighbors = b } } - nodes, err := reader.ListNodes(r.Context(), nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, pubkeyPrefix, name, scope, cursor, limit, includeNeighbors) + nodes, err := reader.ListNodes(r.Context(), nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, pubkeyPrefix, name, scope, cursor, pageCursor, limit, includeNeighbors) if err != nil { respondError(w, http.StatusInternalServerError, "internal server error") return diff --git a/internal/api/handlers/nodes_test.go b/internal/api/handlers/nodes_test.go index c7ceebb..2d7bba1 100644 --- a/internal/api/handlers/nodes_test.go +++ b/internal/api/handlers/nodes_test.go @@ -129,8 +129,8 @@ func TestListNodes_OK(t *testing.T) { nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001") r := chi.NewRouter() r.Get("/nodes", listNodes(stubReader{ - listNodes: func(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _, _ string, _ int64, _ int32, _ bool) (api.Page[api.NodeSummary], error) { - return api.Page[api.NodeSummary]{Items: []api.NodeSummary{{ID: nodeID}}}, nil + listNodes: func(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _, _ string, _ int64, _ *api.NodeCursor, _ int32, _ bool) (api.NodePage, error) { + return api.NodePage{Page: api.Page[api.NodeSummary]{Items: []api.NodeSummary{{ID: nodeID}}}}, nil }, })) req := httptest.NewRequest(http.MethodGet, "/nodes", nil) @@ -147,9 +147,9 @@ func TestListNodes_NeighborsParam_PassedThrough(t *testing.T) { var gotIncludeNeighbors bool r := chi.NewRouter() r.Get("/nodes", listNodes(stubReader{ - listNodes: func(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _, _ string, _ int64, _ int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) { + listNodes: func(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _, _ string, _ int64, _ *api.NodeCursor, _ int32, includeNeighbors bool) (api.NodePage, error) { gotIncludeNeighbors = includeNeighbors - return api.Page[api.NodeSummary]{Items: []api.NodeSummary{{ID: nodeID, NeighborIDs: []uuid.UUID{neighborID}}}}, nil + return api.NodePage{Page: api.Page[api.NodeSummary]{Items: []api.NodeSummary{{ID: nodeID, NeighborIDs: []uuid.UUID{neighborID}}}}}, nil }, })) req := httptest.NewRequest(http.MethodGet, "/nodes?neighbors=true", nil) @@ -181,9 +181,9 @@ func TestListNodes_NeighborsParam_BareFlagMeansTrue(t *testing.T) { var gotIncludeNeighbors bool r := chi.NewRouter() r.Get("/nodes", listNodes(stubReader{ - listNodes: func(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _, _ string, _ int64, _ int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) { + listNodes: func(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _, _ string, _ int64, _ *api.NodeCursor, _ int32, includeNeighbors bool) (api.NodePage, error) { gotIncludeNeighbors = includeNeighbors - return api.Page[api.NodeSummary]{}, nil + return api.NodePage{}, nil }, })) req := httptest.NewRequest(http.MethodGet, "/nodes?neighbors", nil) @@ -201,9 +201,9 @@ func TestListNodes_PubkeyPrefixParam_PassedThrough(t *testing.T) { var gotPubkeyPrefix string r := chi.NewRouter() r.Get("/nodes", listNodes(stubReader{ - listNodes: func(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, pubkeyPrefix, _, _ string, _ int64, _ int32, _ bool) (api.Page[api.NodeSummary], error) { + listNodes: func(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, pubkeyPrefix, _, _ string, _ int64, _ *api.NodeCursor, _ int32, _ bool) (api.NodePage, error) { gotPubkeyPrefix = pubkeyPrefix - return api.Page[api.NodeSummary]{}, nil + return api.NodePage{}, nil }, })) req := httptest.NewRequest(http.MethodGet, "/nodes?pubkeyPrefix=AB12", nil) @@ -325,3 +325,46 @@ func TestListNodeObservations_OK(t *testing.T) { t.Errorf("expected 200, got %d", w.Code) } } + +func TestNodesCursorPrecedence(t *testing.T) { + const valid = "v1:1792442625019123:0b7e2c4a-5f1d-4c3e-9a2b-8d6f1e0c3a57" + var called bool + var gotCursor int64 + var gotPage *api.NodeCursor + next := "v1:1:0b7e2c4a-5f1d-4c3e-9a2b-8d6f1e0c3a57" + reader := stubReader{ + listNodes: func(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _, _ string, cursor int64, pageCursor *api.NodeCursor, _ int32, _ bool) (api.NodePage, error) { + called, gotCursor, gotPage = true, cursor, pageCursor + return api.NodePage{Page: api.Page[api.NodeSummary]{Items: []api.NodeSummary{}}, NextPageCursor: &next}, nil + }, + } + serve := func(url string) *httptest.ResponseRecorder { + called, gotCursor, gotPage = false, 0, nil + r := chi.NewRouter() + r.Get("/nodes", listNodes(reader)) + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, url, nil)) + return w + } + + w := serve("/nodes?pageCursor=" + valid + "&cursor=1792442625000") + if w.Code != http.StatusOK || !called || gotCursor != 1792442625000 || gotPage == nil || gotPage.String() != valid { + t.Fatalf("both sent: status %d, cursor %d, pageCursor %v", w.Code, gotCursor, gotPage) + } + if !strings.Contains(w.Body.String(), `"nextPageCursor":"`+next+`"`) { + t.Fatalf("body lacks nextPageCursor: %s", w.Body.String()) + } + + w = serve("/nodes?pageCursor=" + next) + if w.Code != http.StatusOK || gotPage == nil || gotPage.LastSeen.UnixMicro() != 1 || gotPage.ID != uuid.MustParse("0b7e2c4a-5f1d-4c3e-9a2b-8d6f1e0c3a57") { + t.Fatalf("v1:1:: status %d, pageCursor %v", w.Code, gotPage) + } + + for _, url := range []string{"/nodes?pageCursor=garbage", "/nodes?pageCursor=v1:1:ab01", "/nodes?pageCursor=garbage®ion=bc"} { + wantError(t, serve(url), http.StatusBadRequest, "invalid pageCursor") + if called { + t.Fatalf("%s: reader called", url) + } + } + wantError(t, serve("/nodes?cursor=abc&pageCursor="+valid), http.StatusBadRequest, "cursor must be an integer") +} diff --git a/internal/api/handlers/observers.go b/internal/api/handlers/observers.go index b3b0107..c2524c2 100644 --- a/internal/api/handlers/observers.go +++ b/internal/api/handlers/observers.go @@ -49,9 +49,10 @@ func ObserversRouter(reader api.Reader) http.Handler { // @Param status query string false "Filter by status (online or offline)" // @Param name query string false "Partial case-insensitive display name match" // @Param scope query string false "Filter by transport scope name e.g. %23bc (URL-encoded #bc)" -// @Param cursor query int false "last_seen epoch ms of last item for pagination" +// @Param cursor query int false "last_seen epoch ms of the last item" +// @Param pageCursor query string false "Opaque nextPageCursor from a previous response; keeps millisecond ties. Wins over cursor when both are sent" // @Param limit query int false "Max results (default 50); must be positive, values above 200 are clamped" minimum(1) maximum(200) -// @Success 200 {object} api.Page[api.ObserverSummary] +// @Success 200 {object} api.ObserverPage // @Failure 400 {object} handlers.APIError // @Failure 500 {object} handlers.APIError // @Router /observers [get] @@ -71,6 +72,15 @@ func listObservers(reader api.Reader) http.HandlerFunc { } cursor = c } + var pageCursor *api.ObserverCursor + if raw := r.URL.Query().Get("pageCursor"); raw != "" { + pc, err := api.ParseObserverCursor(raw) + if err != nil { + respondError(w, http.StatusBadRequest, "invalid pageCursor") + return + } + pageCursor = pc + } limit, err := parseLimit(r, 50) if err != nil { respondError(w, http.StatusBadRequest, err.Error()) @@ -85,7 +95,7 @@ func listObservers(reader api.Reader) http.HandlerFunc { } iatas = append(iatas, regionIATAs...) } - observers, err := reader.ListObservers(r.Context(), iatas, observerType, broker, status, name, scope, cursor, limit) + observers, err := reader.ListObservers(r.Context(), iatas, observerType, broker, status, name, scope, cursor, pageCursor, limit) if err != nil { respondError(w, http.StatusInternalServerError, "failed to get list of observers") return diff --git a/internal/api/handlers/observers_test.go b/internal/api/handlers/observers_test.go index 7344526..6c25ecc 100644 --- a/internal/api/handlers/observers_test.go +++ b/internal/api/handlers/observers_test.go @@ -10,6 +10,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -75,8 +76,8 @@ func TestListObservers_OK(t *testing.T) { observerID := uuid.MustParse("00000000-0000-0000-0000-000000000001") r := chi.NewRouter() r.Get("/observers", listObservers(stubReader{ - listObservers: func(_ context.Context, _ []string, _, _, _, _, _ string, _ int64, _ int32) (api.Page[api.ObserverSummary], error) { - return api.Page[api.ObserverSummary]{Items: []api.ObserverSummary{{ID: observerID}}}, nil + listObservers: func(_ context.Context, _ []string, _, _, _, _, _ string, _ int64, _ *api.ObserverCursor, _ int32) (api.ObserverPage, error) { + return api.ObserverPage{Page: api.Page[api.ObserverSummary]{Items: []api.ObserverSummary{{ID: observerID}}}}, nil }, })) req := httptest.NewRequest(http.MethodGet, "/observers", nil) @@ -432,3 +433,46 @@ func TestGetObserverActivity_NullsSerialise(t *testing.T) { t.Errorf("expected airtimeMs null, got %v", airtime) } } + +func TestObserversCursorPrecedence(t *testing.T) { + const valid = "v1:1792442625019123:0b7e2c4a-5f1d-4c3e-9a2b-8d6f1e0c3a57" + var called bool + var gotCursor int64 + var gotPage *api.ObserverCursor + next := "v1:1:0b7e2c4a-5f1d-4c3e-9a2b-8d6f1e0c3a57" + reader := stubReader{ + listObservers: func(_ context.Context, _ []string, _, _, _, _, _ string, cursor int64, pageCursor *api.ObserverCursor, _ int32) (api.ObserverPage, error) { + called, gotCursor, gotPage = true, cursor, pageCursor + return api.ObserverPage{Page: api.Page[api.ObserverSummary]{Items: []api.ObserverSummary{}}, NextPageCursor: &next}, nil + }, + } + serve := func(url string) *httptest.ResponseRecorder { + called, gotCursor, gotPage = false, 0, nil + r := chi.NewRouter() + r.Get("/observers", listObservers(reader)) + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, url, nil)) + return w + } + + w := serve("/observers?pageCursor=" + valid + "&cursor=1792442625000") + if w.Code != http.StatusOK || !called || gotCursor != 1792442625000 || gotPage == nil || gotPage.String() != valid { + t.Fatalf("both sent: status %d, cursor %d, pageCursor %v", w.Code, gotCursor, gotPage) + } + if !strings.Contains(w.Body.String(), `"nextPageCursor":"`+next+`"`) { + t.Fatalf("body lacks nextPageCursor: %s", w.Body.String()) + } + + w = serve("/observers?pageCursor=" + next) + if w.Code != http.StatusOK || gotPage == nil || gotPage.LastSeen.UnixMicro() != 1 || gotPage.ID != uuid.MustParse("0b7e2c4a-5f1d-4c3e-9a2b-8d6f1e0c3a57") { + t.Fatalf("v1:1:: status %d, pageCursor %v", w.Code, gotPage) + } + + for _, url := range []string{"/observers?pageCursor=garbage", "/observers?pageCursor=v1:1:ab01", "/observers?pageCursor=garbage®ion=bc"} { + wantError(t, serve(url), http.StatusBadRequest, "invalid pageCursor") + if called { + t.Fatalf("%s: reader called", url) + } + } + wantError(t, serve("/observers?cursor=abc&pageCursor="+valid), http.StatusBadRequest, "cursor must be an integer") +} diff --git a/internal/api/handlers/packets.go b/internal/api/handlers/packets.go index 1882382..e594c45 100644 --- a/internal/api/handlers/packets.go +++ b/internal/api/handlers/packets.go @@ -47,10 +47,11 @@ func PacketsRouter(reader api.Reader) http.Handler { // @Param region query string false "Filter by region slug, expands to member IATAs" // @Param since query int false "Filter by first_heard_at >= since (epoch ms)" // @Param until query int false "Filter by first_heard_at <= until (epoch ms)" -// @Param cursor query int false "epoch ms of last item for pagination; last_heard_at, or site-local heard_at when iatas is set" +// @Param cursor query int false "epoch ms of the last item; last_heard_at, or site-local heard_at when iatas is set. Prefer pageCursor where nextPageCursor is returned" +// @Param pageCursor query string false "Opaque nextPageCursor from a previous response; keeps millisecond ties. Wins over cursor when both are sent. Ignored with hashPrefix or an IATA or region filter, which page by cursor" // @Param limit query int false "Max results (default 50); must be positive, values above 200 are clamped" minimum(1) maximum(200) // @Param hashPrefix query string false "Match packets whose hash starts with this hex prefix (4-16 chars). Ordering and cursor use global last_heard_at even when iatas is set" -// @Success 200 {object} object +// @Success 200 {object} api.PacketPage // @Failure 400 {object} handlers.APIError // @Failure 500 {object} handlers.APIError // @Router /packets [get] @@ -97,6 +98,15 @@ func listPackets(reader api.Reader) http.HandlerFunc { } cursor = c } + var pageCursor *api.PacketCursor + if raw := r.URL.Query().Get("pageCursor"); raw != "" { + pc, err := api.ParsePacketCursor(raw) + if err != nil { + respondError(w, http.StatusBadRequest, "invalid pageCursor") + return + } + pageCursor = pc + } limit, err := parseLimit(r, 50) if err != nil { respondError(w, http.StatusBadRequest, err.Error()) @@ -112,16 +122,18 @@ func listPackets(reader api.Reader) http.HandlerFunc { iatas = append(iatas, regionIATAs...) } scopes := parseCSVOrSingle(r, "scopes", "scope") - var packets api.Page[api.PacketSummary] + var packets api.PacketPage if p := r.URL.Query().Get("hashPrefix"); p != "" { lo, hi, err := hashPrefixRange(p) if err != nil { respondError(w, http.StatusBadRequest, err.Error()) return } - packets, err = reader.SearchPacketsByHashPrefix(r.Context(), lo, hi, payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit) + var page api.Page[api.PacketSummary] + page, err = reader.SearchPacketsByHashPrefix(r.Context(), lo, hi, payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit) + packets = api.PacketPage{Page: page} } else { - packets, err = reader.ListPackets(r.Context(), payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit) + packets, err = reader.ListPackets(r.Context(), payloadTypes, routeTypes, iatas, scopes, since, until, cursor, pageCursor, limit) } if err != nil { respondError(w, http.StatusInternalServerError, "internal server error") diff --git a/internal/api/handlers/packets_test.go b/internal/api/handlers/packets_test.go index eed4ca7..d95c093 100644 --- a/internal/api/handlers/packets_test.go +++ b/internal/api/handlers/packets_test.go @@ -6,8 +6,10 @@ package handlers import ( "context" "encoding/hex" + "encoding/json" "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -128,8 +130,8 @@ func TestListPacketsBackfill_InvalidLimit(t *testing.T) { func TestListPackets_OK(t *testing.T) { r := chi.NewRouter() r.Get("/packets", listPackets(stubReader{ - listPackets: func(_ context.Context, _, _ []int16, _ []string, _ []string, _, _ time.Time, _ int64, _ int32) (api.Page[api.PacketSummary], error) { - return api.Page[api.PacketSummary]{Items: []api.PacketSummary{{PacketHash: "deadbeef"}}}, nil + listPackets: func(_ context.Context, _, _ []int16, _ []string, _ []string, _, _ time.Time, _ int64, _ *api.PacketCursor, _ int32) (api.PacketPage, error) { + return api.PacketPage{Page: api.Page[api.PacketSummary]{Items: []api.PacketSummary{{PacketHash: "deadbeef"}}}}, nil }, })) req := httptest.NewRequest(http.MethodGet, "/packets", nil) @@ -145,11 +147,11 @@ func TestListPackets_PluralParams_PassedThrough(t *testing.T) { var gotScopes []string r := chi.NewRouter() r.Get("/packets", listPackets(stubReader{ - listPackets: func(_ context.Context, payloadTypes, routeTypes []int16, _ []string, scopes []string, _, _ time.Time, _ int64, _ int32) (api.Page[api.PacketSummary], error) { + listPackets: func(_ context.Context, payloadTypes, routeTypes []int16, _ []string, scopes []string, _, _ time.Time, _ int64, _ *api.PacketCursor, _ int32) (api.PacketPage, error) { gotPayloadTypes = payloadTypes gotRouteTypes = routeTypes gotScopes = scopes - return api.Page[api.PacketSummary]{}, nil + return api.PacketPage{Page: api.Page[api.PacketSummary]{}}, nil }, })) req := httptest.NewRequest(http.MethodGet, "/packets?payloadTypes=2,4&routeTypes=0,1&scopes=%23bc,%23west", nil) @@ -174,11 +176,11 @@ func TestListPackets_SingularParams_StillWork(t *testing.T) { var gotScopes []string r := chi.NewRouter() r.Get("/packets", listPackets(stubReader{ - listPackets: func(_ context.Context, payloadTypes, routeTypes []int16, _ []string, scopes []string, _, _ time.Time, _ int64, _ int32) (api.Page[api.PacketSummary], error) { + listPackets: func(_ context.Context, payloadTypes, routeTypes []int16, _ []string, scopes []string, _, _ time.Time, _ int64, _ *api.PacketCursor, _ int32) (api.PacketPage, error) { gotPayloadTypes = payloadTypes gotRouteTypes = routeTypes gotScopes = scopes - return api.Page[api.PacketSummary]{}, nil + return api.PacketPage{Page: api.Page[api.PacketSummary]{}}, nil }, })) req := httptest.NewRequest(http.MethodGet, "/packets?payloadType=4&routeType=1&scope=%23bc", nil) @@ -204,9 +206,9 @@ func TestListPackets_PluralParams_TakePrecedenceOverSingular(t *testing.T) { var gotPayloadTypes []int16 r := chi.NewRouter() r.Get("/packets", listPackets(stubReader{ - listPackets: func(_ context.Context, payloadTypes, _ []int16, _ []string, _ []string, _, _ time.Time, _ int64, _ int32) (api.Page[api.PacketSummary], error) { + listPackets: func(_ context.Context, payloadTypes, _ []int16, _ []string, _ []string, _, _ time.Time, _ int64, _ *api.PacketCursor, _ int32) (api.PacketPage, error) { gotPayloadTypes = payloadTypes - return api.Page[api.PacketSummary]{}, nil + return api.PacketPage{Page: api.Page[api.PacketSummary]{}}, nil }, })) req := httptest.NewRequest(http.MethodGet, "/packets?payloadType=9&payloadTypes=2,4", nil) @@ -310,9 +312,9 @@ func TestListPackets_HashPrefix(t *testing.T) { var gotIATAs []string r := chi.NewRouter() r.Get("/packets", listPackets(stubReader{ - listPackets: func(_ context.Context, _, _ []int16, _ []string, _ []string, _, _ time.Time, _ int64, _ int32) (api.Page[api.PacketSummary], error) { + listPackets: func(_ context.Context, _, _ []int16, _ []string, _ []string, _, _ time.Time, _ int64, _ *api.PacketCursor, _ int32) (api.PacketPage, error) { t.Error("ListPackets called for a hashPrefix search") - return api.Page[api.PacketSummary]{}, nil + return api.PacketPage{Page: api.Page[api.PacketSummary]{}}, nil }, searchPacketsByHashPrefix: func(_ context.Context, lo, hi []byte, _, _ []int16, iatas []string, _ []string, _, _ time.Time, _ int64, _ int32) (api.Page[api.PacketSummary], error) { gotLo, gotHi, gotIATAs = lo, hi, iatas @@ -345,3 +347,112 @@ func TestListPackets_HashPrefixInvalid(t *testing.T) { } } } + +// wantError fails unless the response is status with exactly this error body. +func wantError(t *testing.T, w *httptest.ResponseRecorder, status int, message string) { + t.Helper() + if w.Code != status { + t.Fatalf("status = %d, want %d; body %s", w.Code, status, w.Body.String()) + } + var body map[string]APIError + if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { + t.Fatalf("decode %q: %v", w.Body.String(), err) + } + if got := body["error"].Message; got != message { + t.Fatalf("message = %q, want %q", got, message) + } +} + +func TestPacketsCursorPrecedence(t *testing.T) { + const valid = "v1:1792442625019123:ab01" + type call struct { + listed, searched bool + cursor int64 + pageCursor *api.PacketCursor + } + var got call + next := "v1:1792442625019000:cd02" + var withNext bool + reader := stubReader{ + listPackets: func(_ context.Context, _, _ []int16, _, _ []string, _, _ time.Time, cursor int64, pageCursor *api.PacketCursor, _ int32) (api.PacketPage, error) { + got = call{listed: true, cursor: cursor, pageCursor: pageCursor} + page := api.PacketPage{Page: api.Page[api.PacketSummary]{Items: []api.PacketSummary{}}} + if withNext { + page.NextPageCursor = &next + } + return page, nil + }, + searchPacketsByHashPrefix: func(_ context.Context, _, _ []byte, _, _ []int16, _, _ []string, _, _ time.Time, cursor int64, _ int32) (api.Page[api.PacketSummary], error) { + got = call{searched: true, cursor: cursor} + return api.Page[api.PacketSummary]{Items: []api.PacketSummary{}}, nil + }, + } + serve := func(url string) *httptest.ResponseRecorder { + got = call{} + r := chi.NewRouter() + r.Get("/packets", listPackets(reader)) + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, url, nil)) + return w + } + + w := serve("/packets?pageCursor=" + valid + "&cursor=1792442625000") + if w.Code != http.StatusOK { + t.Fatalf("both sent: status %d, body %s", w.Code, w.Body.String()) + } + if !got.listed || got.cursor != 1792442625000 || got.pageCursor == nil || got.pageCursor.String() != valid { + t.Fatalf("both sent: reader got %+v", got) + } + + for _, url := range []string{ + "/packets?pageCursor=garbage", + "/packets?pageCursor=garbage&hashPrefix=abcd", + "/packets?pageCursor=garbage&iatas=YVR", + "/packets?pageCursor=garbage®ion=bc", + "/packets?pageCursor=garbage®ionId=1", + "/packets?pageCursor=v1:1:", + "/packets?pageCursor=garbage&cursor=5", + } { + w := serve(url) + wantError(t, w, http.StatusBadRequest, "invalid pageCursor") + if got.listed || got.searched { + t.Fatalf("%s: reader called", url) + } + } + + w = serve("/packets?hashPrefix=abcd&pageCursor=" + valid + "&cursor=5") + if w.Code != http.StatusOK || !got.searched || got.cursor != 5 { + t.Fatalf("hashPrefix: status %d, reader got %+v", w.Code, got) + } + if strings.Contains(w.Body.String(), "nextPageCursor") { + t.Fatalf("hashPrefix body has nextPageCursor: %s", w.Body.String()) + } + + wantError(t, serve("/packets?cursor=abc&pageCursor="+valid), http.StatusBadRequest, "cursor must be an integer") + + for _, url := range []string{ + "/packets?cursor=9223372036854775807", + "/packets?cursor=-5", + "/packets?payloadTypes=,", + "/packets?scopes=%20", + "/packets?pageCursor=", + } { + if w := serve(url); w.Code != http.StatusOK { + t.Fatalf("%s: status %d, body %s", url, w.Code, w.Body.String()) + } + if got.pageCursor != nil { + t.Fatalf("%s: pageCursor %v, want nil", url, got.pageCursor) + } + } + + withNext = true + w = serve("/packets") + if !strings.Contains(w.Body.String(), `"nextPageCursor":"`+next+`"`) { + t.Fatalf("body lacks nextPageCursor: %s", w.Body.String()) + } + withNext = false + w = serve("/packets") + if strings.Contains(w.Body.String(), "nextPageCursor") { + t.Fatalf("body has nextPageCursor without one: %s", w.Body.String()) + } +} diff --git a/internal/api/handlers/stub_reader_test.go b/internal/api/handlers/stub_reader_test.go index d7a6481..7dfb038 100644 --- a/internal/api/handlers/stub_reader_test.go +++ b/internal/api/handlers/stub_reader_test.go @@ -31,19 +31,19 @@ type stubReader struct { listChannelMessages func(ctx context.Context, channelID *int32, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) listChannelMessagesByHash func(ctx context.Context, hash []byte, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) listMessagesAfterID func(ctx context.Context, afterID int64, iatas []string, scope string, limit int32) ([]api.ChannelMessage, error) - listObservers func(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, limit int32) (api.Page[api.ObserverSummary], error) + listObservers func(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, pageCursor *api.ObserverCursor, limit int32) (api.ObserverPage, error) getObserver func(ctx context.Context, observerID uuid.UUID) (*api.Observer, error) getObserverTelemetry func(ctx context.Context, observerID uuid.UUID, since, until time.Time, afterID int64) (*api.ObserverTelemetry, error) getObserverTelemetryBucketed func(ctx context.Context, observerID uuid.UUID, since, until time.Time, bucketHours int32) ([]api.ObserverTelemetryPoint, error) getObserverActivity func(ctx context.Context, observerID uuid.UUID, window, interval time.Duration, until time.Time) (*api.ObserverActivity, error) getObserverScopes func(ctx context.Context, observerID uuid.UUID) ([]string, error) listObserverAdverts func(ctx context.Context, observerID uuid.UUID, cursor int64, limit int32) (api.Page[api.AdvertObservation], error) - listNodes func(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, limit int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) + listNodes func(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, pageCursor *api.NodeCursor, limit int32, includeNeighbors bool) (api.NodePage, error) getNode func(ctx context.Context, nodeID uuid.UUID) (*api.Node, error) getNodeNeighbors func(ctx context.Context, nodeID uuid.UUID) ([]api.NodeNeighbor, error) listNodeObservations func(ctx context.Context, nodeID uuid.UUID, cursor int64, limit int32) (api.Page[api.PacketObservationSummary], error) searchPacketsByHashPrefix func(ctx context.Context, lo, hi []byte, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) - listPackets func(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) + listPackets func(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, pageCursor *api.PacketCursor, limit int32) (api.PacketPage, error) listPacketsAfterID func(ctx context.Context, afterObservationID int64, payloadType, routeType int16, iatas []string, scope string, limit int32) ([]api.PacketSummary, error) getPacket func(ctx context.Context, packetHash []byte) (*api.Packet, error) getRadioPresets func(ctx context.Context, preset string, iatas []string) ([]api.RadioPreset, error) @@ -166,11 +166,11 @@ func (s stubReader) ListMessagesAfterID(ctx context.Context, afterID int64, iata return nil, nil } -func (s stubReader) ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, limit int32) (api.Page[api.ObserverSummary], error) { +func (s stubReader) ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, pageCursor *api.ObserverCursor, limit int32) (api.ObserverPage, error) { if s.listObservers != nil { - return s.listObservers(ctx, iatas, observerType, broker, status, name, scope, cursor, limit) + return s.listObservers(ctx, iatas, observerType, broker, status, name, scope, cursor, pageCursor, limit) } - return api.Page[api.ObserverSummary]{}, nil + return api.ObserverPage{}, nil } func (s stubReader) GetObserver(ctx context.Context, observerID uuid.UUID) (*api.Observer, error) { @@ -215,11 +215,11 @@ func (s stubReader) ListObserverAdverts(ctx context.Context, observerID uuid.UUI return api.Page[api.AdvertObservation]{}, nil } -func (s stubReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, limit int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) { +func (s stubReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, pageCursor *api.NodeCursor, limit int32, includeNeighbors bool) (api.NodePage, error) { if s.listNodes != nil { - return s.listNodes(ctx, nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, pubkeyPrefix, name, scope, cursor, limit, includeNeighbors) + return s.listNodes(ctx, nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, pubkeyPrefix, name, scope, cursor, pageCursor, limit, includeNeighbors) } - return api.Page[api.NodeSummary]{}, nil + return api.NodePage{}, nil } func (s stubReader) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error) { @@ -243,11 +243,11 @@ func (s stubReader) ListNodeObservations(ctx context.Context, nodeID uuid.UUID, return api.Page[api.PacketObservationSummary]{}, nil } -func (s stubReader) ListPackets(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) { +func (s stubReader) ListPackets(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, pageCursor *api.PacketCursor, limit int32) (api.PacketPage, error) { if s.listPackets != nil { - return s.listPackets(ctx, payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit) + return s.listPackets(ctx, payloadTypes, routeTypes, iatas, scopes, since, until, cursor, pageCursor, limit) } - return api.Page[api.PacketSummary]{}, nil + return api.PacketPage{}, nil } func (s stubReader) SearchPacketsByHashPrefix(ctx context.Context, lo, hi []byte, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) { diff --git a/internal/api/local_borders.go b/internal/api/local_borders.go index 65e44a0..193d15a 100644 --- a/internal/api/local_borders.go +++ b/internal/api/local_borders.go @@ -26,8 +26,8 @@ type localBorderReader struct { local *borders.Live } -func (r *localBorderReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, paths, traces *bool, pubkey []byte, prefix, name, scope string, cursor int64, limit int32, neighbors bool) (Page[NodeSummary], error) { - page, err := r.Reader.ListNodes(ctx, nodeType, iatas, paths, traces, pubkey, prefix, name, scope, cursor, limit, neighbors) +func (r *localBorderReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, paths, traces *bool, pubkey []byte, prefix, name, scope string, cursor int64, pageCursor *NodeCursor, limit int32, neighbors bool) (NodePage, error) { + page, err := r.Reader.ListNodes(ctx, nodeType, iatas, paths, traces, pubkey, prefix, name, scope, cursor, pageCursor, limit, neighbors) if err != nil { return page, err } diff --git a/internal/api/local_borders_test.go b/internal/api/local_borders_test.go index d7f0b01..96222dd 100644 --- a/internal/api/local_borders_test.go +++ b/internal/api/local_borders_test.go @@ -22,7 +22,7 @@ import ( type borderReader struct { api.Reader node *api.Node - page api.Page[api.NodeSummary] + page api.NodePage getCalls, listCalls int err error } @@ -31,7 +31,7 @@ func (r *borderReader) GetNode(context.Context, uuid.UUID) (*api.Node, error) { r.getCalls++ return r.node, r.err } -func (r *borderReader) ListNodes(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _, _ string, _ int64, _ int32, _ bool) (api.Page[api.NodeSummary], error) { +func (r *borderReader) ListNodes(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _, _ string, _ int64, _ *api.NodeCursor, _ int32, _ bool) (api.NodePage, error) { r.listCalls++ return r.page, r.err } @@ -47,7 +47,7 @@ func TestLocalBordersAfterCache(t *testing.T) { t.Fatal(err) } node := &api.Node{NodeSummary: api.NodeSummary{ID: uuid.New(), NodeType: 2, Latitude: new(15.0), Longitude: new(15.0), IATAs: []api.NodeIATA{{IATA: "ZZZ", LastHeard: 1}}}} - base := &borderReader{node: node, page: api.Page[api.NodeSummary]{Items: []api.NodeSummary{node.NodeSummary}, HasMore: true, NextCursor: new(int64(123))}} + base := &borderReader{node: node, page: api.NodePage{Page: api.Page[api.NodeSummary]{Items: []api.NodeSummary{node.NodeSummary}, HasMore: true, NextCursor: new(int64(123))}, NextPageCursor: new("v1:123000:" + node.ID.String())}} mr := miniredis.RunT(t) client := cache.NewClient(mr.Addr(), "", 0) t.Cleanup(func() { client.Close() }) @@ -68,11 +68,11 @@ func TestLocalBordersAfterCache(t *testing.T) { if got.PossiblyForeign == nil || *got.PossiblyForeign != tc.want || got.IATAs[0].IATA != "ZZZ" { t.Fatalf("detail: %+v", got) } - page, err := tc.reader.ListNodes(ctx, 2, []string{"ZZZ"}, nil, nil, nil, "", "", "", 0, 50, false) + page, err := tc.reader.ListNodes(ctx, 2, []string{"ZZZ"}, nil, nil, nil, "", "", "", 0, nil, 50, false) if err != nil { t.Fatal(err) } - if !page.HasMore || *page.NextCursor != 123 || page.Items[0].PossiblyForeign == nil || *page.Items[0].PossiblyForeign != tc.want { + if !page.HasMore || *page.NextCursor != 123 || page.NextPageCursor == nil || *page.NextPageCursor != *base.page.NextPageCursor || page.Items[0].PossiblyForeign == nil || *page.Items[0].PossiblyForeign != tc.want { t.Fatalf("page: %+v", page) } data, _ := json.Marshal(got) @@ -114,7 +114,7 @@ func TestLocalBordersPreserveReaderErrors(t *testing.T) { if _, err := reader.GetNode(context.Background(), uuid.New()); !errors.Is(err, want) { t.Fatal(err) } - if _, err := reader.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, 1, false); !errors.Is(err, want) { + if _, err := reader.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, nil, 1, false); !errors.Is(err, want) { t.Fatal(err) } base.err = nil diff --git a/internal/api/nodes.go b/internal/api/nodes.go index 19e5f5b..604a403 100644 --- a/internal/api/nodes.go +++ b/internal/api/nodes.go @@ -32,6 +32,12 @@ type NodeIATA struct { LastHeard int64 `json:"lastHeard"` // epoch ms } +// NodePage adds the exact cursor while keeping the numeric one for older clients. +type NodePage struct { + Page[NodeSummary] + NextPageCursor *string `json:"nextPageCursor,omitempty"` +} + // NodeSummary is the minimal node representation used in list responses. type NodeSummary struct { ID uuid.UUID `json:"id"` diff --git a/internal/api/observers.go b/internal/api/observers.go index 995ef04..597adf8 100644 --- a/internal/api/observers.go +++ b/internal/api/observers.go @@ -9,6 +9,12 @@ import ( "github.com/google/uuid" ) +// ObserverPage adds the exact cursor while keeping the numeric one for older clients. +type ObserverPage struct { + Page[ObserverSummary] + NextPageCursor *string `json:"nextPageCursor,omitempty"` +} + // ObserverSummary is the minimal observer representation used in list responses. type ObserverSummary struct { ID uuid.UUID `json:"id"` diff --git a/internal/api/packets.go b/internal/api/packets.go index e5f47fe..82b4b20 100644 --- a/internal/api/packets.go +++ b/internal/api/packets.go @@ -27,6 +27,12 @@ type PacketLatestObserver struct { ResolvedDestination *ResolvedHop `json:"resolvedDestination,omitempty"` } +// PacketPage adds the exact cursor while keeping the numeric one for older clients. +type PacketPage struct { + Page[PacketSummary] + NextPageCursor *string `json:"nextPageCursor,omitempty"` +} + // PacketSummary is the minimal packet representation used in list responses. // Includes the latest observation rolled in for display purposes. type PacketSummary struct { diff --git a/internal/api/reader.go b/internal/api/reader.go index 150b52b..066d5d7 100644 --- a/internal/api/reader.go +++ b/internal/api/reader.go @@ -87,7 +87,8 @@ type Reader interface { // All filter params are optional — pass empty string or nil to skip a filter. // status is "online" or "offline" derived from last_status_at recency. // cursor is last_seen epoch ms of the last observer; pass 0 to start from the beginning. - ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, limit int32) (Page[ObserverSummary], error) + // pageCursor wins over cursor. + ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, pageCursor *ObserverCursor, limit int32) (ObserverPage, error) // GetObserver returns full detail for a single observer by UUID. // Returns nil, pgx.ErrNoRows if the observer is not found. @@ -117,7 +118,8 @@ type Reader interface { // When includeNeighbors is true, each NodeSummary's NeighborIDs field is // populated with the distinct set of neighbor node IDs (across all // IATAs); otherwise it's left nil to avoid the extra aggregation. - ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, limit int32, includeNeighbors bool) (Page[NodeSummary], error) + // cursor is last_seen epoch ms of the last node; pageCursor wins over it. + ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, pageCursor *NodeCursor, limit int32, includeNeighbors bool) (NodePage, error) // GetNode returns full detail for a single node by UUID. // Returns nil, pgx.ErrNoRows if the node is not found. @@ -138,7 +140,8 @@ type Reader interface { // cursor is epoch ms in either case; pass 0 to start from the beginning. // payloadTypes/routeTypes/scopes are OR'd within each filter, ANDed across filters; pass // nil/empty to skip a filter. - ListPackets(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (Page[PacketSummary], error) + // pageCursor wins over cursor on the global path; the IATA path ignores it. + ListPackets(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, pageCursor *PacketCursor, limit int32) (PacketPage, error) // SearchPacketsByHashPrefix lists packets whose hash is in [lo, hi) (nil hi is unbounded), // newest last_heard_at first. Other filters behave as in ListPackets; iatas only diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go index 1e59f13..488d486 100644 --- a/internal/cache/cache_test.go +++ b/internal/cache/cache_test.go @@ -176,16 +176,16 @@ func (s *stubReader) ListMessagesAfterID(_ context.Context, _ int64, _ []string, return nil, nil } -func (s *stubReader) ListNodes(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _, _ string, _ int64, _ int32, _ bool) (api.Page[api.NodeSummary], error) { - return api.Page[api.NodeSummary]{}, nil +func (s *stubReader) ListNodes(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _, _ string, _ int64, _ *api.NodeCursor, _ int32, _ bool) (api.NodePage, error) { + return api.NodePage{}, nil } func (s *stubReader) ListNodeObservations(_ context.Context, _ uuid.UUID, _ int64, _ int32) (api.Page[api.PacketObservationSummary], error) { return api.Page[api.PacketObservationSummary]{}, nil } -func (s *stubReader) ListObservers(_ context.Context, _ []string, _, _, _, _, _ string, _ int64, _ int32) (api.Page[api.ObserverSummary], error) { - return api.Page[api.ObserverSummary]{}, nil +func (s *stubReader) ListObservers(_ context.Context, _ []string, _, _, _, _, _ string, _ int64, _ *api.ObserverCursor, _ int32) (api.ObserverPage, error) { + return api.ObserverPage{}, nil } func (s *stubReader) ListObserverAdverts(_ context.Context, _ uuid.UUID, _ int64, _ int32) (api.Page[api.AdvertObservation], error) { @@ -196,8 +196,8 @@ func (s *stubReader) SearchPacketsByHashPrefix(_ context.Context, _, _ []byte, _ return api.Page[api.PacketSummary]{}, nil } -func (s *stubReader) ListPackets(_ context.Context, _, _ []int16, _ []string, _ []string, _, _ time.Time, _ int64, _ int32) (api.Page[api.PacketSummary], error) { - return api.Page[api.PacketSummary]{}, nil +func (s *stubReader) ListPackets(_ context.Context, _, _ []int16, _ []string, _ []string, _, _ time.Time, _ int64, _ *api.PacketCursor, _ int32) (api.PacketPage, error) { + return api.PacketPage{}, nil } func (s *stubReader) ListPacketsAfterID(_ context.Context, _ int64, _, _ int16, _ []string, _ string, _ int32) ([]api.PacketSummary, error) { diff --git a/internal/cache/reader.go b/internal/cache/reader.go index 5e4df4c..17d871d 100644 --- a/internal/cache/reader.go +++ b/internal/cache/reader.go @@ -320,8 +320,8 @@ func (cr *CachedReader) ListMessagesAfterID(ctx context.Context, afterID int64, } // ListNodes implements [api.Reader]. -func (cr *CachedReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, limit int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) { - return cr.inner.ListNodes(ctx, nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, pubkeyPrefix, name, scope, cursor, limit, includeNeighbors) +func (cr *CachedReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, pageCursor *api.NodeCursor, limit int32, includeNeighbors bool) (api.NodePage, error) { + return cr.inner.ListNodes(ctx, nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, pubkeyPrefix, name, scope, cursor, pageCursor, limit, includeNeighbors) } // ListNodeObservations implements [api.Reader]. @@ -330,8 +330,8 @@ func (cr *CachedReader) ListNodeObservations(ctx context.Context, nodeID uuid.UU } // ListObservers implements [api.Reader]. -func (cr *CachedReader) ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, limit int32) (api.Page[api.ObserverSummary], error) { - return cr.inner.ListObservers(ctx, iatas, observerType, broker, status, name, scope, cursor, limit) +func (cr *CachedReader) ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, pageCursor *api.ObserverCursor, limit int32) (api.ObserverPage, error) { + return cr.inner.ListObservers(ctx, iatas, observerType, broker, status, name, scope, cursor, pageCursor, limit) } // ListObserverAdverts implements [api.Reader]. @@ -345,8 +345,8 @@ func (cr *CachedReader) SearchPacketsByHashPrefix(ctx context.Context, lo, hi [] } // ListPackets implements [api.Reader]. -func (cr *CachedReader) ListPackets(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) { - return cr.inner.ListPackets(ctx, payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit) +func (cr *CachedReader) ListPackets(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, pageCursor *api.PacketCursor, limit int32) (api.PacketPage, error) { + return cr.inner.ListPackets(ctx, payloadTypes, routeTypes, iatas, scopes, since, until, cursor, pageCursor, limit) } // ListPacketsAfterID implements [api.Reader]. diff --git a/internal/planguard/diff_integration_test.go b/internal/planguard/diff_integration_test.go index 958d694..d0ec730 100644 --- a/internal/planguard/diff_integration_test.go +++ b/internal/planguard/diff_integration_test.go @@ -58,6 +58,13 @@ const diffCases = `batches: - id: novalue ref: {stmt: page, params: [{sql: "SELECT k FROM t WHERE false"}, 2147483647, 5]} cands: [{stmt: oracle, params: [30, 2147483647, 5]}] + - id: empty + ref: {stmt: page, params: [-1, -1, 5]} + cands: [{stmt: oracle, params: [-1, -1, 5]}] + key: [k] + allowed: [latest_observer_tie] + omit: [v] + expect: {oracle: identical} ` const writeCases = `batches: @@ -185,6 +192,18 @@ CREATE FUNCTION `+schema+`.write_row() RETURNS int LANGUAGE sql AS t.Errorf("pair whose resolver finds nothing: %+v (skipped %d), want one skip", r, len(rep.Skipped())) } + // An empty result keeps its columns, so the omit columns of a pair whose results are both empty are found. + if r := got["empty|oracle"]; r.Verdict != planguard.DiffIdentical || r.RefRows != 0 || !r.OK { + t.Errorf("empty pair: %+v, want identical", r) + } + empty, err := planguard.ReadCaseFile(filepath.Join(out, "snap", "empty.ref.page.jsonl")) + if err != nil { + t.Fatal(err) + } + if strings.Join(empty.Columns, ",") != "id,k,v" || len(empty.Rows) != 0 { + t.Errorf("empty result: columns %v, %d rows; want id,k,v and none", empty.Columns, len(empty.Rows)) + } + // One case file per (pair, role, alias), read back for the comparison. for _, f := range []string{"tie.ref.old", "tie.cand.new", "ident.ref.ident", "ident.cand.ident", "walk.ref.oracle", "walk.cand.page", "walk.walk7.ref.oracle", "walk.walk7.cand.page", "walk.walk50.ref.oracle", "walk.walk50.cand.page"} { diff --git a/internal/planguard/diff_test.go b/internal/planguard/diff_test.go index e1da28d..bb1e4e6 100644 --- a/internal/planguard/diff_test.go +++ b/internal/planguard/diff_test.go @@ -561,8 +561,9 @@ func TestRepoDiffCasesLoad(t *testing.T) { } for _, b := range spec.Batches { for alias, src := range b.Statements { + // old: names come from the --old-ref build and may be gone from this one; the run checks them. kind, name, _ := strings.Cut(src, ":") - if (kind == "old" || kind == "new") && sqlc[name] == "" { + if kind == "new" && sqlc[name] == "" { t.Errorf("%s: %s: %s names no sqlc statement", f, alias, src) } } diff --git a/internal/planguard/genpolicy_test.go b/internal/planguard/genpolicy_test.go index f3db415..8bf2a32 100644 --- a/internal/planguard/genpolicy_test.go +++ b/internal/planguard/genpolicy_test.go @@ -116,7 +116,7 @@ func TestGenPolicyMatchesCorpus(t *testing.T) { t.Errorf("db/planpolicy/cached_gen.go is stale; run go run ./cmd/planguard gen-policy. Generated:\n%s", src) } got := slices.Sorted(maps.Keys(planpolicy.Cached)) - if !slices.Equal(got, []string{"GetNodeByID", "ListKnownRoutesByIATA", "ListKnownRoutesGlobal"}) { + if !slices.Equal(got, []string{"GetNodeByID", "ListKnownRoutesByIATA", "ListKnownRoutesGlobal", "ListPacketsByTypes", "ListPacketsPage"}) { t.Errorf("planpolicy.Cached = %v", got) } for _, line := range []string{ diff --git a/internal/planguard/session.go b/internal/planguard/session.go index 86ac180..d75cd8b 100644 --- a/internal/planguard/session.go +++ b/internal/planguard/session.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "slices" "github.com/jackc/pgx/v5/pgconn" ) @@ -24,8 +25,19 @@ func NewSession(pc *pgconn.PgConn) Session { return pgSession{pc} } type pgSession struct{ pc *pgconn.PgConn } func (s pgSession) Exec(ctx context.Context, sql string) ([]*pgconn.Result, error) { - res, err := s.pc.Exec(ctx, sql).ReadAll() - if err != nil { + // As MultiResultReader.ReadAll, but a result without rows keeps its columns: Read fills FieldDescriptions + // from the first row only. + mrr := s.pc.Exec(ctx, sql) + var res []*pgconn.Result + for mrr.NextResult() { + rr := mrr.ResultReader() + r := rr.Read() + if r.FieldDescriptions == nil && rr.FieldDescriptions() != nil { + r.FieldDescriptions = slices.Clone(rr.FieldDescriptions()) + } + res = append(res, r) + } + if err := mrr.Close(); err != nil { return res, err } for _, r := range res { diff --git a/internal/planguard/storediff.go b/internal/planguard/storediff.go index 4c7a521..25c70aa 100644 --- a/internal/planguard/storediff.go +++ b/internal/planguard/storediff.go @@ -32,7 +32,8 @@ type StoreDiffSpec struct { // The whole JSON body of every page is compared. Each relaxation is a named class that must be listed in Allowed: // - tie_order: within each run of items with equal Key, the new items are a permutation of the old ones (same // count, keys equal position by position); -// - additive_keys: new objects may hold keys the old ones lack; each added path is listed. +// - additive_keys: new objects may hold keys the old ones lack; each added path is listed; +// - latest_observer_tie: the Omit fields of an item may differ; each such item is listed. // // Any other difference fails. type StoreCase struct { @@ -42,8 +43,9 @@ type StoreCase struct { WebLimit int `yaml:"web_limit"` // the client's page size; default 50 Pages int `yaml:"pages"` // walk cap per limit; default 50 Follow []Follow `yaml:"follow"` // param ← nextPageCursor | nextCursor | last. - Allowed []string `yaml:"allowed"` // tie_order (keyed by key) | additive_keys + Allowed []string `yaml:"allowed"` // tie_order (keyed by key) | additive_keys | latest_observer_tie Key []string `yaml:"key"` // item fields for tie_order + Omit []string `yaml:"omit"` // item fields for latest_observer_tie Vars map[string]StoreVar `yaml:"vars"` // path values read from another endpoint } @@ -91,7 +93,7 @@ var ( storeVarRe = regexp.MustCompile(`\{([a-z][a-z0-9_]*)\}`) storeParamRe = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*$`) storeFieldRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$`) - storeClassSet = map[string]bool{ClassTieOrder: true, ClassAdditiveKeys: true} + storeClassSet = map[string]bool{ClassTieOrder: true, ClassAdditiveKeys: true, ClassLatestObserverTie: true} ) // LoadStoreDiffSpec reads and validates a storediff case file. @@ -219,7 +221,7 @@ func (c StoreCase) validate() error { allowed := map[string]bool{} for _, a := range c.Allowed { if !storeClassSet[a] { - return fmt.Errorf("allowed %q: want tie_order or additive_keys", a) + return fmt.Errorf("allowed %q: want tie_order, additive_keys or latest_observer_tie", a) } allowed[a] = true } @@ -231,6 +233,17 @@ func (c StoreCase) validate() error { return fmt.Errorf("key %q is not a field name", k) } } + if allowed[ClassLatestObserverTie] != (len(c.Omit) > 0) { + return errors.New("omit and the class latest_observer_tie go together") + } + for _, f := range c.Omit { + if !storeFieldRe.MatchString(f) { + return fmt.Errorf("omit %q is not a field name", f) + } + if slices.Contains(c.Key, f) { + return fmt.Errorf("omit field %s is a key field", f) + } + } return nil } @@ -538,7 +551,7 @@ func (sd *storeDiff) walk(c StoreCase, path string, limit int, res *StoreCaseRes if allowed[ClassTieOrder] { var moved int var bad string - nb, moved, bad = alignTies(o.body, nb, c.Key, allowed[ClassAdditiveKeys]) + nb, moved, bad = alignTies(o.body, nb, c.Key, c.Omit, allowed[ClassAdditiveKeys]) if bad != "" { res.Verdict, res.Detail = DiffDifferent, fmt.Sprintf("page %d: tie_order: %s", page, bad) return nil @@ -548,6 +561,14 @@ func (sd *storeDiff) walk(c StoreCase, path string, limit int, res *StoreCaseRes notes = append(notes, fmt.Sprintf("page %d: tie_order: %d items hold another position within their run of equal %s", page, moved, strings.Join(c.Key, ", "))) } } + if allowed[ClassLatestObserverTie] { + var listed []string + nb, listed = alignOmitted(o.body, nb, c.Omit, allowed[ClassAdditiveKeys]) + if len(listed) > 0 { + counts[ClassLatestObserverTie] += len(listed) + notes = append(notes, fmt.Sprintf("page %d: latest_observer_tie: %s", page, strings.Join(listed, ", "))) + } + } var added []string var addedPtr *[]string if allowed[ClassAdditiveKeys] { @@ -611,7 +632,7 @@ func (sd *storeDiff) walk(c StoreCase, path string, limit int, res *StoreCaseRes seen[k] = true follow = next } - for _, cl := range []string{ClassTieOrder, ClassAdditiveKeys} { + for _, cl := range []string{ClassTieOrder, ClassLatestObserverTie, ClassAdditiveKeys} { if n := counts[cl]; n > 0 { res.Classes = append(res.Classes, fmt.Sprintf("%s(%d)", cl, n)) } @@ -634,9 +655,10 @@ func cursorText(v string, ok bool, err error) string { } // alignTies returns newBody with its items reordered to old's order within runs of equal key, when the items -// differ only that way, and the number of items that moved. Otherwise it returns newBody unchanged. bad names an -// item that lacks a key field: without it every such item would share one run and any order would pass. -func alignTies(oldBody, newBody any, key []string, additive bool) (out any, moved int, bad string) { +// differ only that way (and in their omit fields), and the number of items that moved. Otherwise it returns +// newBody unchanged. bad names an item that lacks a key field: without it every such item would share one run and +// any order would pass. +func alignTies(oldBody, newBody any, key, omit []string, additive bool) (out any, moved int, bad string) { oi, ok1 := pageItems(oldBody) ni, ok2 := pageItems(newBody) if !ok1 || !ok2 { @@ -673,14 +695,7 @@ func alignTies(oldBody, newBody any, key []string, additive bool) (out any, move if len(oi) != len(ni) || !slices.Equal(oldKeys, newKeys) { return newBody, 0, "" } - equal := func(a, b any) bool { - var added []string - var p *[]string - if additive { - p = &added - } - return jsonDiff(a, b, "$", p) == nil - } + equal := func(a, b any) bool { return sameExcept(a, b, omit, additive) } items := make([]any, len(ni)) for start := 0; start < len(oi); { end := start + 1 @@ -714,15 +729,118 @@ func alignTies(oldBody, newBody any, key []string, additive bool) (out any, move if moved == 0 { return newBody, 0, "" } - if _, isArray := newBody.([]any); isArray { - return items, moved, "" + return withItems(newBody, items), moved, "" +} + +// withItems returns body with its items replaced: items itself for a bare array, else a copy of the object. +func withItems(body any, items []any) any { + if _, isArray := body.([]any); isArray { + return items } m := map[string]any{} - for k, v := range newBody.(map[string]any) { + for k, v := range body.(map[string]any) { m[k] = v } m["items"] = items - return m, moved, "" + return m +} + +// sameExcept reports whether two items are equal once their omit fields are dropped. +func sameExcept(a, b any, omit []string, additive bool) bool { + var added []string + var p *[]string + if additive { + p = &added + } + return jsonDiff(dropFields(a, omit), dropFields(b, omit), "$", p) == nil +} + +// dropFields returns item without the (dotted) fields, copying only the objects on their paths. +func dropFields(item any, fields []string) any { + for _, f := range fields { + item = dropField(item, strings.Split(f, ".")) + } + return item +} + +func dropField(v any, path []string) any { + m, ok := v.(map[string]any) + if !ok { + return v + } + if _, ok := m[path[0]]; !ok { + return v + } + out := make(map[string]any, len(m)) + for k, x := range m { + out[k] = x + } + if len(path) == 1 { + delete(out, path[0]) + } else { + out[path[0]] = dropField(m[path[0]], path[1:]) + } + return out +} + +// takeFields returns dst with the (dotted) fields set to src's values, or dropped where src lacks them, copying +// only the objects on their paths. +func takeFields(dst, src any, fields []string) any { + for _, f := range fields { + dst = takeField(dst, src, strings.Split(f, ".")) + } + return dst +} + +func takeField(dst, src any, path []string) any { + d, ok := dst.(map[string]any) + if !ok { + return dst + } + s, _ := src.(map[string]any) + sv, inSrc := s[path[0]] + if _, inDst := d[path[0]]; len(path) > 1 && !inDst { + return dst + } + out := make(map[string]any, len(d)) + for k, x := range d { + out[k] = x + } + switch { + case len(path) > 1 && inSrc: + out[path[0]] = takeField(d[path[0]], sv, path[1:]) + case len(path) > 1: + out[path[0]] = dropField(d[path[0]], path[1:]) + case inSrc: + out[path[0]] = sv + default: + delete(out, path[0]) + } + return out +} + +// alignOmitted returns newBody with each item that differs from old's item at its position only in the omit +// fields given old's values for them, and the JSON paths of those items. Items that differ in anything else are left +// as they are, so the page comparison reports them. +func alignOmitted(oldBody, newBody any, omit []string, additive bool) (any, []string) { + oi, ok1 := pageItems(oldBody) + ni, ok2 := pageItems(newBody) + if !ok1 || !ok2 { + return newBody, nil + } + var listed []string + items := slices.Clone(ni) + for i := range min(len(oi), len(ni)) { + if jsonDiff(oi[i], ni[i], "$", nil) == nil || !sameExcept(oi[i], ni[i], omit, additive) { + continue + } + items[i] = takeFields(ni[i], oi[i], omit) + listed = append(listed, fmt.Sprintf("$.items[%d]", i)) + } + if len(listed) == 0 { + return newBody, nil + } + return withItems(newBody, items), listed } // jsonPathDiff is the first difference between two JSON values. diff --git a/internal/planguard/storediff_test.go b/internal/planguard/storediff_test.go index 477fa0a..8840240 100644 --- a/internal/planguard/storediff_test.go +++ b/internal/planguard/storediff_test.go @@ -9,6 +9,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "regexp" "strconv" "strings" @@ -250,7 +251,7 @@ func TestStoreDiffSpecValidation(t *testing.T) { {"cases:\n- id: a\n path: /api/v1/x\n query: {limit: \"5\"}\n", "query limit is set by the walk"}, {"cases:\n- id: a\n path: /api/v1/x\n follow: [{param: cursor, from: prevCursor}]\n", "want nextCursor, nextPageCursor or last."}, {"cases:\n- id: a\n path: /api/v1/x\n allowed: [tie_order]\n", "tie_order needs key"}, - {"cases:\n- id: a\n path: /api/v1/x\n allowed: [saturated_suffix]\n", "want tie_order or additive_keys"}, + {"cases:\n- id: a\n path: /api/v1/x\n allowed: [saturated_suffix]\n", "want tie_order, additive_keys or latest_observer_tie"}, {"cases:\n- id: a\n path: /api/v1/x/{id}\n", "vars does not define it"}, {"cases:\n- id: a\n path: /api/v1/x\n- id: a\n path: /api/v1/y\n", "case id a is used twice"}, } { @@ -300,12 +301,20 @@ func TestStoreDiffPathVars(t *testing.T) { // The repo's storediff case files load, and every query, follow and var param is a query param of its handler in // the generated Swagger. func TestRepoStoreDiffCasesLoad(t *testing.T) { - spec, err := planguard.LoadStoreDiffSpec("../../db/planguard/storediff/smoke.yaml") - if err != nil { - t.Fatal(err) + files, err := filepath.Glob("../../db/planguard/storediff/*.yaml") + if err != nil || len(files) < 2 { + t.Fatalf("storediff case files %v: %v", files, err) } - if len(spec.Cases) < 7 { - t.Errorf("smoke.yaml has %d cases, want at least 7", len(spec.Cases)) + spec := &planguard.StoreDiffSpec{} + for _, f := range files { + s, err := planguard.LoadStoreDiffSpec(f) + if err != nil { + t.Fatal(err) + } + if filepath.Base(f) == "smoke.yaml" && len(s.Cases) < 7 { + t.Errorf("smoke.yaml has %d cases, want at least 7", len(s.Cases)) + } + spec.Cases = append(spec.Cases, s.Cases...) } raw, err := os.ReadFile("../../docs/swagger.json") if err != nil { @@ -429,3 +438,96 @@ func TestStoreDiffTieOrderChecksEachBuildsCursor(t *testing.T) { t.Errorf("tie inside the page: got %+v, want allowed over 2 pages", c) } } + +// A top-level key the new build adds to every page is allowed under additive_keys; when both builds hold the key, +// it is compared. +func TestStoreDiffAdditiveKeys(t *testing.T) { + add := func(v any) func(page int, body map[string]any) { + return func(page int, body map[string]any) { body["nextPageCursor"] = v } + } + old := pageServer(t, nil, nil) + rep := runStoreDiff(t, old, pageServer(t, add("v1:x"), nil), thingsSpec([]string{planguard.ClassAdditiveKeys}, nil), "3") + if c := rep.Cases[0]; c.Verdict != planguard.DiffAllowed || len(c.Classes) != 1 || c.Classes[0] != "additive_keys(3)" || + !strings.Contains(c.Detail, "page 1: additive_keys: $.nextPageCursor") { + t.Errorf("added on every page: got %+v, want allowed additive_keys(3)", c) + } + rep = runStoreDiff(t, old, pageServer(t, add("v1:x"), nil), thingsSpec(nil, nil), "3") + if c := rep.Cases[0]; c.Verdict != planguard.DiffDifferent || c.Detail != `page 1: $.nextPageCursor: old (absent), new "v1:x"` { + t.Errorf("undeclared: got %+v, want different at $.nextPageCursor", c) + } + rep = runStoreDiff(t, pageServer(t, add("v1:a"), nil), pageServer(t, add("v1:b"), nil), thingsSpec([]string{planguard.ClassAdditiveKeys}, nil), "3") + if c := rep.Cases[0]; c.Verdict != planguard.DiffDifferent || c.Detail != `page 1: $.nextPageCursor: old "v1:a", new "v1:b"` { + t.Errorf("both hold the key: got %+v, want different at $.nextPageCursor", c) + } +} + +// latest_observer_tie lets the omit fields of an item differ, and lists each such item; any other field still +// counts. +func TestStoreDiffLatestObserverTie(t *testing.T) { + observer := func(page int, body map[string]any) { + for _, it := range body["items"].([]any) { + it.(map[string]any)["latestObserver"] = map[string]any{"name": "a"} + } + } + tie := func(field string) func(page int, body map[string]any) { + return func(page int, body map[string]any) { + observer(page, body) + if items := body["items"].([]any); page == 1 && len(items) > 3 { + if field == "latestObserver" { + items[3].(map[string]any)[field] = map[string]any{"name": "b"} + } else { + items[3].(map[string]any)[field] = "other" + } + } + } + } + spec := func(allowed []string, omit []string) *planguard.StoreDiffSpec { + s := thingsSpec(allowed, nil) + s.Cases[0].Omit = omit + return s + } + old := pageServer(t, observer, nil) + lot := []string{planguard.ClassLatestObserverTie} + + rep := runStoreDiff(t, old, pageServer(t, tie("latestObserver"), nil), spec(lot, []string{"latestObserver"}), "5") + if c := rep.Cases[0]; c.Verdict != planguard.DiffAllowed || len(c.Classes) != 1 || c.Classes[0] != "latest_observer_tie(1)" || + !strings.Contains(c.Detail, "page 1: latest_observer_tie: $.items[3]") { + t.Errorf("observer differs: got %+v, want allowed latest_observer_tie(1) listing $.items[3]", c) + } + rep = runStoreDiff(t, old, pageServer(t, tie("latestObserver"), nil), spec(nil, nil), "5") + if c := rep.Cases[0]; c.Verdict != planguard.DiffDifferent || c.Detail != `page 1: $.items[3].latestObserver.name: old "a", new "b"` { + t.Errorf("undeclared: got %+v, want different", c) + } + rep = runStoreDiff(t, old, pageServer(t, tie("name"), nil), spec(lot, []string{"latestObserver"}), "5") + if c := rep.Cases[0]; c.Verdict != planguard.DiffDifferent || c.Detail != `page 1: $.items[3].name: old "n4", new "other"` { + t.Errorf("another field differs: got %+v, want different at $.items[3].name", c) + } + + // With tie_order, an item whose observer differs still pairs within its run of equal keys. + swapTie := func(page int, body map[string]any) { + tie("latestObserver")(page, body) + if items := body["items"].([]any); page == 1 && len(items) > 3 { + items[2], items[3] = items[3], items[2] + } + } + both := spec([]string{planguard.ClassTieOrder, planguard.ClassLatestObserverTie}, []string{"latestObserver"}) + both.Cases[0].Key = []string{"group"} + rep = runStoreDiff(t, old, pageServer(t, swapTie, nil), both, "5") + if c := rep.Cases[0]; c.Verdict != planguard.DiffAllowed || strings.Join(c.Classes, " ") != "tie_order(2) latest_observer_tie(1)" { + t.Errorf("tie and observer: got %+v, want allowed tie_order(2) latest_observer_tie(1)", c) + } + + for _, tc := range []struct{ yaml, err string }{ + {"cases:\n- id: a\n path: /api/v1/x\n omit: [latestObserver]\n", "omit and the class latest_observer_tie go together"}, + {"cases:\n- id: a\n path: /api/v1/x\n allowed: [latest_observer_tie]\n", "omit and the class latest_observer_tie go together"}, + {"cases:\n- id: a\n path: /api/v1/x\n allowed: [tie_order, latest_observer_tie]\n key: [lastHeardAt]\n omit: [lastHeardAt]\n", "omit field lastHeardAt is a key field"}, + {"cases:\n- id: a\n path: /api/v1/x\n allowed: [latest_observer_tie]\n omit: [\"a b\"]\n", "is not a field name"}, + } { + if _, err := planguard.ParseStoreDiffSpec([]byte(tc.yaml), "t.yaml"); err == nil || !strings.Contains(err.Error(), tc.err) { + t.Errorf("%q: error %v, want %q", tc.yaml, err, tc.err) + } + } + if _, err := planguard.ParseStoreDiffSpec([]byte("cases:\n- id: a\n path: /api/v1/x\n allowed: [latest_observer_tie]\n omit: [latestObserver]\n"), "t.yaml"); err != nil { + t.Errorf("valid omit: %v", err) + } +}