From a761c582ce0bb7edc82e82488d3c9ed148a62c5b Mon Sep 17 00:00:00 2001 From: Stephen Hinck Date: Tue, 12 May 2026 10:28:12 -0700 Subject: [PATCH 01/12] feat: Add index maintenance support (adds .DS_Store ignore to prep branch) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b3e5e21..69d3491 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ integration/testdata/local/ # Local test and metric artifacts .coverage/ + +# Mac DS Store +.DS_Store From 55563fbb01d6a81dbbaa8396d9747b0edef06a8b Mon Sep 17 00:00:00 2001 From: Stephen Hinck Date: Tue, 12 May 2026 10:39:55 -0700 Subject: [PATCH 02/12] stub out the optimizer interface (AI commit) --- drivers/pg/optimize.go | 11 +++++++++++ drivers/pg/optimize_test.go | 17 +++++++++++++++++ graph/optimizer.go | 22 ++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 drivers/pg/optimize.go create mode 100644 drivers/pg/optimize_test.go create mode 100644 graph/optimizer.go diff --git a/drivers/pg/optimize.go b/drivers/pg/optimize.go new file mode 100644 index 0000000..db001cf --- /dev/null +++ b/drivers/pg/optimize.go @@ -0,0 +1,11 @@ +package pg + +import "context" + +// Optimize satisfies the graph.Optimizer interface. The body is currently a +// no-op stub; the actual index maintenance logic (assessment via pgstattuple +// followed by REINDEX CONCURRENTLY of bloated indexes) is implemented in a +// later phase. +func (s *Driver) Optimize(ctx context.Context) error { + return nil +} diff --git a/drivers/pg/optimize_test.go b/drivers/pg/optimize_test.go new file mode 100644 index 0000000..1e0e279 --- /dev/null +++ b/drivers/pg/optimize_test.go @@ -0,0 +1,17 @@ +package pg + +import ( + "context" + "testing" + + "github.com/specterops/dawgs/graph" + "github.com/stretchr/testify/require" +) + +// Compile-time assertion that *Driver implements graph.Optimizer. +var _ graph.Optimizer = (*Driver)(nil) + +func TestDriver_Optimize_NoopReturnsNil(t *testing.T) { + d := &Driver{} + require.NoError(t, d.Optimize(context.Background())) +} diff --git a/graph/optimizer.go b/graph/optimizer.go new file mode 100644 index 0000000..b17b231 --- /dev/null +++ b/graph/optimizer.go @@ -0,0 +1,22 @@ +package graph + +import "context" + +// Optimizer is an optional capability that drivers may implement to perform +// backend-specific maintenance work, such as rebuilding fragmented indexes. +// +// Optimize is intended to be called periodically by the consumer (e.g. after +// an analysis cycle completes). Implementations must: +// +// - Be safe to call repeatedly; consecutive calls on a healthy database +// should be inexpensive (or no-ops). +// - Honor ctx cancellation. Long-running maintenance must abort promptly +// when the context is done. +// - Avoid taking exclusive locks that would block normal read or write +// traffic against the database for any meaningful duration. +// +// Drivers that do not have a meaningful notion of optimization should simply +// not implement this interface; consumers must type-assert before calling. +type Optimizer interface { + Optimize(ctx context.Context) error +} From 10e07d1ad9e659bb6e693521d5951cd0f4a4d628 Mon Sep 17 00:00:00 2001 From: Stephen Hinck Date: Tue, 12 May 2026 11:18:51 -0700 Subject: [PATCH 03/12] Install pgstattuple on schema enforcement --- drivers/pg/query/sql/schema_up.sql | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/pg/query/sql/schema_up.sql b/drivers/pg/query/sql/schema_up.sql index 7a4f209..f7c5884 100644 --- a/drivers/pg/query/sql/schema_up.sql +++ b/drivers/pg/query/sql/schema_up.sql @@ -21,6 +21,11 @@ create extension if not exists pg_trgm; -- arrays for nodes. create extension if not exists intarray; +-- We need the pgstattuple extension to measure btree leaf density and fragmentation on node and edge indexes +-- during driver-managed index optimization. If this extension cannot be installed (e.g. a managed Postgres that +-- does not expose it), the optimizer logs a warning and skips the assessment rather than failing. +create extension if not exists pgstattuple; + -- This is an optional but useful extension for validating performance of queries -- create extension if not exists pg_stat_statements; -- From c73259c722e3a2a5d5fe48e5b5006c1c9a220df5 Mon Sep 17 00:00:00 2001 From: Stephen Hinck Date: Tue, 12 May 2026 11:41:22 -0700 Subject: [PATCH 04/12] Instantiate measurement and logging during optimization, but take no action. Update README. --- README.md | 19 ++++ drivers/pg/optimize.go | 161 ++++++++++++++++++++++++++++- drivers/pg/optimize_test.go | 106 ++++++++++++++++++- drivers/pg/query/sql/schema_up.sql | 16 ++- 4 files changed, 289 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a61ab7e..c35af5f 100644 --- a/README.md +++ b/README.md @@ -86,3 +86,22 @@ make metrics_check ``` The defaults can be adjusted with `CYCLO_TOP`, `CYCLO_OVER`, `CRAP_TOP`, and `CRAP_OVER`. +## PostgreSQL Extensions + +The PostgreSQL driver's schema bootstrap (`drivers/pg/query/sql/schema_up.sql`) installs the following extensions: + +| Extension | Required | Purpose | +|---------------|----------|----------------------------------------------------------------------------------------------------------| +| `pg_trgm` | yes | GIN trigram indexes for `contains` / `starts with` / `ends with` lookups on graph entity properties. | +| `intarray` | yes | Extended integer array operations used when maintaining node kind arrays. | +| `pgstattuple` | no | Measures btree leaf density and fragmentation for the driver-managed index optimization assessment. | + +`pgstattuple` is treated as best-effort. Installing it typically requires a superuser role and on some managed +Postgres deployments the contrib package is not exposed at all. Bootstrap wraps the install in an exception +handler that downgrades any failure to a `WARNING`, and the driver's `Optimize` implementation re-checks for the +extension at runtime and logs a warning when it is missing rather than failing the caller. To enable index +optimization in such environments, have an administrator install the extension out-of-band: + +```sql +create extension if not exists pgstattuple; +``` diff --git a/drivers/pg/optimize.go b/drivers/pg/optimize.go index db001cf..05d7746 100644 --- a/drivers/pg/optimize.go +++ b/drivers/pg/optimize.go @@ -1,11 +1,162 @@ package pg -import "context" +import ( + "context" + "fmt" + "log/slog" +) -// Optimize satisfies the graph.Optimizer interface. The body is currently a -// no-op stub; the actual index maintenance logic (assessment via pgstattuple -// followed by REINDEX CONCURRENTLY of bloated indexes) is implemented in a -// later phase. +const ( + // bloatedIndexLeafDensityThreshold is the average leaf-page fill percentage + // below which a btree index is considered dense enough that REINDEX would + // reclaim significant space. Calibrated against seven production tenant + // samples whose live indexes regularly fell to 21-55%, against a freshly + // rebuilt baseline (edge_1_pkey on a recently restored tenant) of 73.8%. + bloatedIndexLeafDensityThreshold = 60.0 + + // highIndexFragmentationThreshold is the leaf-page fragmentation percentage + // at or above which a btree index has accumulated enough out-of-order page + // splits to warrant a rebuild even when leaf density alone has not crossed + // its threshold. + highIndexFragmentationThreshold = 40.0 +) + +const ( + sqlPgstattupleInstalled = `select exists(select 1 from pg_extension where extname = 'pgstattuple')` + + sqlSelectGraphPartitionBtreeIndexes = ` + select i.oid as index_oid, + n.nspname as schema_name, + c.relname as table_name, + i.relname as index_name, + pg_relation_size(i.oid) as index_size_bytes + from pg_inherits inh + join pg_class p on p.oid = inh.inhparent + join pg_class c on c.oid = inh.inhrelid + join pg_namespace n on n.oid = c.relnamespace + join pg_index x on x.indrelid = c.oid + join pg_class i on i.oid = x.indexrelid + join pg_am a on a.oid = i.relam + where p.relname in ('node', 'edge') + and p.relnamespace = n.oid + and a.amname = 'btree' + order by c.relname, i.relname` + + sqlSelectIndexBloatMetrics = `select avg_leaf_density, leaf_fragmentation from pgstatindex($1::regclass)` +) + +// indexRow is a candidate index discovered by the listing query, prior to +// per-index pgstatindex assessment. +type indexRow struct { + oid uint32 + schema string + table string + index string + sizeBytes int64 +} + +// indexCandidate is an index whose pgstatindex measurement caused it to be +// flagged for rebuild by needsReindex. +type indexCandidate struct { + indexRow + leafDensity float64 + fragmentation float64 + reason string +} + +// Optimize satisfies the graph.Optimizer interface. The current phase performs +// a read-only assessment: it identifies btree indexes on partitions of the +// node and edge tables whose leaf density or fragmentation cross the rebuild +// thresholds and logs the candidates. No DDL is executed in this phase. func (s *Driver) Optimize(ctx context.Context) error { + if installed, err := s.pgstattupleInstalled(ctx); err != nil { + return fmt.Errorf("checking pgstattuple extension: %w", err) + } else if !installed { + slog.WarnContext(ctx, "Index optimization skipped: pgstattuple extension is not installed; verify the DAWGS schema bootstrap completed successfully") + return nil + } + + indexes, err := s.listGraphPartitionBtreeIndexes(ctx) + if err != nil { + return fmt.Errorf("listing graph partition btree indexes: %w", err) + } + + slog.InfoContext(ctx, fmt.Sprintf("Index optimization assessment starting: %d btree index(es) under consideration", len(indexes))) + + var ( + candidates []indexCandidate + totalBytes int64 + ) + for _, idx := range indexes { + density, fragmentation, err := s.measureIndexBloat(ctx, idx.oid) + if err != nil { + slog.WarnContext(ctx, fmt.Sprintf("Skipping bloat assessment for index %s.%s: %v", idx.schema, idx.index, err)) + continue + } + + candidate := indexCandidate{indexRow: idx, leafDensity: density, fragmentation: fragmentation} + if reason, flagged := needsReindex(candidate); flagged { + candidate.reason = reason + candidates = append(candidates, candidate) + totalBytes += candidate.sizeBytes + slog.InfoContext(ctx, fmt.Sprintf( + "Index optimization candidate: %s.%s on %s (size=%d bytes, leaf_density=%.1f%%, fragmentation=%.1f%%, reason=%s)", + candidate.schema, candidate.index, candidate.table, + candidate.sizeBytes, candidate.leafDensity, candidate.fragmentation, candidate.reason, + )) + } + } + + slog.InfoContext(ctx, fmt.Sprintf( + "Index optimization assessment complete: %d candidate(s) totaling %d bytes", + len(candidates), totalBytes, + )) return nil } + +// needsReindex applies the rebuild thresholds to a measured index and returns +// a short human-readable reason when the index is flagged. Pure function; +// unit-tested in optimize_test.go. +func needsReindex(c indexCandidate) (string, bool) { + if c.leafDensity < bloatedIndexLeafDensityThreshold { + return fmt.Sprintf("leaf density %.1f%% below %.1f%% threshold", c.leafDensity, bloatedIndexLeafDensityThreshold), true + } + if c.fragmentation >= highIndexFragmentationThreshold { + return fmt.Sprintf("fragmentation %.1f%% at or above %.1f%% threshold", c.fragmentation, highIndexFragmentationThreshold), true + } + return "", false +} + +func (s *Driver) pgstattupleInstalled(ctx context.Context) (bool, error) { + var installed bool + if err := s.pool.QueryRow(ctx, sqlPgstattupleInstalled).Scan(&installed); err != nil { + return false, err + } + return installed, nil +} + +func (s *Driver) listGraphPartitionBtreeIndexes(ctx context.Context) ([]indexRow, error) { + rows, err := s.pool.Query(ctx, sqlSelectGraphPartitionBtreeIndexes) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []indexRow + for rows.Next() { + var r indexRow + if err := rows.Scan(&r.oid, &r.schema, &r.table, &r.index, &r.sizeBytes); err != nil { + return nil, err + } + out = append(out, r) + } + return out, rows.Err() +} + +func (s *Driver) measureIndexBloat(ctx context.Context, indexOID uint32) (float64, float64, error) { + var density, fragmentation float64 + if err := s.pool.QueryRow(ctx, sqlSelectIndexBloatMetrics, indexOID).Scan(&density, &fragmentation); err != nil { + return 0, 0, err + } + return density, fragmentation, nil +} diff --git a/drivers/pg/optimize_test.go b/drivers/pg/optimize_test.go index 1e0e279..86fb4f1 100644 --- a/drivers/pg/optimize_test.go +++ b/drivers/pg/optimize_test.go @@ -1,17 +1,113 @@ package pg import ( - "context" + "strings" "testing" "github.com/specterops/dawgs/graph" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" ) // Compile-time assertion that *Driver implements graph.Optimizer. var _ graph.Optimizer = (*Driver)(nil) -func TestDriver_Optimize_NoopReturnsNil(t *testing.T) { - d := &Driver{} - require.NoError(t, d.Optimize(context.Background())) +// TestNeedsReindex exercises the threshold logic that decides whether a +// measured index is flagged as a rebuild candidate. The function is pure; +// integration coverage of the surrounding pg_extension / pg_inherits / +// pgstatindex queries lands with the Phase 5 REINDEX work. +func TestNeedsReindex(t *testing.T) { + cases := []struct { + name string + leafDensity float64 + fragmentation float64 + wantFlagged bool + wantReasonHas string + }{ + { + name: "healthy index is not flagged", + leafDensity: 85.0, + fragmentation: 5.0, + wantFlagged: false, + }, + { + name: "freshly built baseline (73.8%/low frag) is not flagged", + leafDensity: 73.8, + fragmentation: 2.5, + wantFlagged: false, + }, + { + name: "leaf density exactly at threshold is not flagged", + leafDensity: bloatedIndexLeafDensityThreshold, + fragmentation: 0, + wantFlagged: false, + }, + { + name: "leaf density just below threshold is flagged", + leafDensity: bloatedIndexLeafDensityThreshold - 0.01, + fragmentation: 0, + wantFlagged: true, + wantReasonHas: "leaf density", + }, + { + name: "deeply bloated production sample (21%) is flagged on density", + leafDensity: 21.0, + fragmentation: 8.0, + wantFlagged: true, + wantReasonHas: "leaf density", + }, + { + name: "fragmentation just below threshold is not flagged when density is healthy", + leafDensity: 85.0, + fragmentation: highIndexFragmentationThreshold - 0.01, + wantFlagged: false, + }, + { + name: "fragmentation exactly at threshold is flagged", + leafDensity: 85.0, + fragmentation: highIndexFragmentationThreshold, + wantFlagged: true, + wantReasonHas: "fragmentation", + }, + { + name: "fragmentation flag triggers when density alone would not", + leafDensity: 65.0, + fragmentation: 50.0, + wantFlagged: true, + wantReasonHas: "fragmentation", + }, + { + name: "density takes precedence in the reason when both cross", + leafDensity: 30.0, + fragmentation: 60.0, + wantFlagged: true, + wantReasonHas: "leaf density", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + candidate := indexCandidate{ + leafDensity: tc.leafDensity, + fragmentation: tc.fragmentation, + } + reason, flagged := needsReindex(candidate) + assert.Equal(t, tc.wantFlagged, flagged, "unexpected flagged result for %s", tc.name) + if tc.wantFlagged { + assert.True(t, strings.Contains(reason, tc.wantReasonHas), + "reason %q does not mention expected substring %q", reason, tc.wantReasonHas) + } else { + assert.Empty(t, reason, "expected empty reason when not flagged") + } + }) + } +} + +// TestThresholdsAreOrdered guards against accidental reordering of the +// calibrated thresholds. If these inequalities ever fail the calibration +// rationale documented on the constants must be revisited. +func TestThresholdsAreOrdered(t *testing.T) { + assert.Greater(t, bloatedIndexLeafDensityThreshold, 0.0, "leaf density threshold must be positive") + assert.Less(t, bloatedIndexLeafDensityThreshold, 100.0, "leaf density threshold must be a percentage") + assert.Greater(t, highIndexFragmentationThreshold, 0.0, "fragmentation threshold must be positive") + assert.Less(t, highIndexFragmentationThreshold, 100.0, "fragmentation threshold must be a percentage") } diff --git a/drivers/pg/query/sql/schema_up.sql b/drivers/pg/query/sql/schema_up.sql index f7c5884..12bef4c 100644 --- a/drivers/pg/query/sql/schema_up.sql +++ b/drivers/pg/query/sql/schema_up.sql @@ -22,9 +22,19 @@ create extension if not exists pg_trgm; create extension if not exists intarray; -- We need the pgstattuple extension to measure btree leaf density and fragmentation on node and edge indexes --- during driver-managed index optimization. If this extension cannot be installed (e.g. a managed Postgres that --- does not expose it), the optimizer logs a warning and skips the assessment rather than failing. -create extension if not exists pgstattuple; +-- during driver-managed index optimization. Installing pgstattuple typically requires a superuser role, and on +-- locked-down managed Postgres deployments (e.g. some RDS or Cloud SQL configurations) the contrib package may +-- not be exposed at all. We wrap the install in a DO block that downgrades any failure to a WARNING so schema +-- bootstrap continues to succeed; the optimizer performs its own runtime extension check and will skip the +-- assessment with a logged warning when pgstattuple is unavailable. +do $$ +begin + create extension if not exists pgstattuple; +exception + when others then + raise warning 'pgstattuple extension could not be installed (%); index optimization will be skipped at runtime', sqlerrm; +end +$$; -- This is an optional but useful extension for validating performance of queries -- create extension if not exists pg_stat_statements; From dd22e389c7bb045f7a5996814f3634cef6d70d03 Mon Sep 17 00:00:00 2001 From: Stephen Hinck Date: Tue, 12 May 2026 15:18:55 -0700 Subject: [PATCH 05/12] Pull optimizer through to DatabaseSwitch --- graph/optimizer.go | 6 +++ graph/switch.go | 15 +++++++ graph/switch_test.go | 100 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 graph/switch_test.go diff --git a/graph/optimizer.go b/graph/optimizer.go index b17b231..5eb77ac 100644 --- a/graph/optimizer.go +++ b/graph/optimizer.go @@ -17,6 +17,12 @@ import "context" // // Drivers that do not have a meaningful notion of optimization should simply // not implement this interface; consumers must type-assert before calling. +// +// Note that *DatabaseSwitch implements Optimizer as a transparent delegate: +// its Optimize method forwards to the currently active driver when that +// driver implements Optimizer, and is a no-op otherwise. As a consequence, +// a successful type assertion to Optimizer against a *DatabaseSwitch does +// not by itself imply that the underlying driver supports optimization. type Optimizer interface { Optimize(ctx context.Context) error } diff --git a/graph/switch.go b/graph/switch.go index 3c40272..ade6600 100644 --- a/graph/switch.go +++ b/graph/switch.go @@ -115,6 +115,21 @@ func (s *DatabaseSwitch) retireInternalContext(ctx context.Context) { } } +// Optimize satisfies the optional Optimizer capability by delegating to the +// currently active driver when it implements Optimizer; otherwise it returns +// nil. This keeps the wrapper transparent so callers can use the standard +// Optimizer type-assertion against a *DatabaseSwitch without having to peek +// through it manually. +func (s *DatabaseSwitch) Optimize(ctx context.Context) error { + s.currentDBLock.RLock() + defer s.currentDBLock.RUnlock() + + if optimizer, ok := s.currentDB.(Optimizer); ok { + return optimizer.Optimize(ctx) + } + return nil +} + func (s *DatabaseSwitch) ReadTransaction(ctx context.Context, txDelegate TransactionDelegate, options ...TransactionOption) error { if internalCtx, err := s.newInternalContext(ctx); err != nil { return err diff --git a/graph/switch_test.go b/graph/switch_test.go new file mode 100644 index 0000000..7b91448 --- /dev/null +++ b/graph/switch_test.go @@ -0,0 +1,100 @@ +package graph_test + +import ( + "context" + "errors" + "testing" + + "github.com/specterops/dawgs/graph" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// stubDatabase is a no-op Database implementation suitable for exercising +// behavior on *graph.DatabaseSwitch that does not depend on real driver +// semantics. Methods return zero values; tests that need richer behavior +// should embed this and override only what they need. +type stubDatabase struct{} + +func (stubDatabase) SetWriteFlushSize(int) {} +func (stubDatabase) SetBatchWriteSize(int) {} +func (stubDatabase) ReadTransaction(context.Context, graph.TransactionDelegate, ...graph.TransactionOption) error { + return nil +} +func (stubDatabase) WriteTransaction(context.Context, graph.TransactionDelegate, ...graph.TransactionOption) error { + return nil +} +func (stubDatabase) BatchOperation(context.Context, graph.BatchDelegate, ...graph.BatchOption) error { + return nil +} +func (stubDatabase) AssertSchema(context.Context, graph.Schema) error { return nil } +func (stubDatabase) SetDefaultGraph(context.Context, graph.Graph) error { return nil } +func (stubDatabase) Run(context.Context, string, map[string]any) error { return nil } +func (stubDatabase) Close(context.Context) error { return nil } +func (stubDatabase) FetchKinds(context.Context) (graph.Kinds, error) { return nil, nil } +func (stubDatabase) RefreshKinds(context.Context) error { return nil } + +// optimizingStubDatabase is a stubDatabase that additionally satisfies +// graph.Optimizer. Each call to Optimize increments calls and returns err. +type optimizingStubDatabase struct { + stubDatabase + calls int + err error +} + +func (s *optimizingStubDatabase) Optimize(context.Context) error { + s.calls++ + return s.err +} + +// TestDatabaseSwitch_Optimize_DelegatesWhenUnderlyingImplementsOptimizer +// verifies that *graph.DatabaseSwitch forwards Optimize to the active driver +// when that driver implements graph.Optimizer, and propagates its return. +func TestDatabaseSwitch_Optimize_DelegatesWhenUnderlyingImplementsOptimizer(t *testing.T) { + ctx := context.Background() + + driver := &optimizingStubDatabase{} + dbSwitch := graph.NewDatabaseSwitch(ctx, driver) + + require.NoError(t, dbSwitch.Optimize(ctx)) + assert.Equal(t, 1, driver.calls, "Optimize should be invoked exactly once on the underlying driver") + + driver.err = errors.New("optimizer reported failure") + err := dbSwitch.Optimize(ctx) + assert.ErrorIs(t, err, driver.err, "DatabaseSwitch must propagate the underlying optimizer error") + assert.Equal(t, 2, driver.calls) +} + +// TestDatabaseSwitch_Optimize_NoOpWhenUnderlyingDoesNotImplementOptimizer +// verifies that the wrapper returns nil without panicking when the active +// driver lacks an Optimize method. +func TestDatabaseSwitch_Optimize_NoOpWhenUnderlyingDoesNotImplementOptimizer(t *testing.T) { + ctx := context.Background() + dbSwitch := graph.NewDatabaseSwitch(ctx, stubDatabase{}) + + require.NoError(t, dbSwitch.Optimize(ctx)) +} + +// TestDatabaseSwitch_Optimize_FollowsActiveDriverAfterSwitch verifies that +// after Switch reassigns the active driver, Optimize routes to the new one. +func TestDatabaseSwitch_Optimize_FollowsActiveDriverAfterSwitch(t *testing.T) { + ctx := context.Background() + + first := &optimizingStubDatabase{} + second := &optimizingStubDatabase{} + + dbSwitch := graph.NewDatabaseSwitch(ctx, first) + require.NoError(t, dbSwitch.Optimize(ctx)) + assert.Equal(t, 1, first.calls) + assert.Equal(t, 0, second.calls) + + dbSwitch.Switch(second) + require.NoError(t, dbSwitch.Optimize(ctx)) + assert.Equal(t, 1, first.calls, "first driver should not be invoked after Switch") + assert.Equal(t, 1, second.calls, "Optimize should be routed to the new active driver") +} + +// Compile-time assertion that *graph.DatabaseSwitch satisfies graph.Optimizer. +// This keeps the wrapper's optional-capability contract enforced by the type +// system rather than relying on test discovery alone. +var _ graph.Optimizer = (*graph.DatabaseSwitch)(nil) From ff6aa7d3afcaf2819363a6eb675a05a949a7075f Mon Sep 17 00:00:00 2001 From: Stephen Hinck Date: Tue, 12 May 2026 15:45:21 -0700 Subject: [PATCH 06/12] Perform the REINDEX and cleanup failed previous attempts, add unit tests (AI commit) --- README.md | 23 +++++- drivers/pg/optimize.go | 142 +++++++++++++++++++++++++++++++++++- drivers/pg/optimize_test.go | 66 ++++++++++++++++- 3 files changed, 225 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c35af5f..2c65931 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ The PostgreSQL driver's schema bootstrap (`drivers/pg/query/sql/schema_up.sql`) |---------------|----------|----------------------------------------------------------------------------------------------------------| | `pg_trgm` | yes | GIN trigram indexes for `contains` / `starts with` / `ends with` lookups on graph entity properties. | | `intarray` | yes | Extended integer array operations used when maintaining node kind arrays. | -| `pgstattuple` | no | Measures btree leaf density and fragmentation for the driver-managed index optimization assessment. | +| `pgstattuple` | no | Measures btree leaf density and fragmentation for the driver-managed index optimization. | `pgstattuple` is treated as best-effort. Installing it typically requires a superuser role and on some managed Postgres deployments the contrib package is not exposed at all. Bootstrap wraps the install in an exception @@ -105,3 +105,24 @@ optimization in such environments, have an administrator install the extension o ```sql create extension if not exists pgstattuple; ``` + +### Index Optimization + +When `pgstattuple` is available the driver's `Optimize` method (exposed through the optional `graph.Optimizer` +interface) performs the following on each invocation against btree indexes on partitions of the `node` and `edge` +tables: + +1. Drops any `INVALID` indexes whose name matches the `_ccnew[N]` pattern, which Postgres leaves behind when a + prior `REINDEX CONCURRENTLY` was aborted. Cleanup failures are logged at `WARN` and never fatal; an orphan + wastes disk but does not block productive rebuilds. +2. Measures every candidate index with `pgstatindex` and flags those whose average leaf density falls below + `60%` or whose leaf-page fragmentation reaches `40%`. Thresholds are calibrated against production samples + and a freshly rebuilt baseline (~73.8% density). +3. Rebuilds each flagged index with `REINDEX INDEX CONCURRENTLY`, smallest first so that an early cancellation + still produces the maximum number of completed rebuilds. Per-index failures are logged at `WARN` and the + loop continues with the next candidate. Context cancellation aborts before the next candidate; an in-flight + `REINDEX CONCURRENTLY` runs to completion (interrupting it would leave a `_ccnew` artifact for the next pass + to reap). + +The pass is not bounded by wall-clock time or candidate count. Callers should serialize `Optimize` against +their own scheduling loop. diff --git a/drivers/pg/optimize.go b/drivers/pg/optimize.go index 05d7746..a59725a 100644 --- a/drivers/pg/optimize.go +++ b/drivers/pg/optimize.go @@ -4,6 +4,10 @@ import ( "context" "fmt" "log/slog" + "sort" + "time" + + "github.com/jackc/pgx/v5" ) const ( @@ -43,6 +47,28 @@ const ( order by c.relname, i.relname` sqlSelectIndexBloatMetrics = `select avg_leaf_density, leaf_fragmentation from pgstatindex($1::regclass)` + + // sqlSelectOrphanedReindexArtifacts identifies INVALID btree indexes left + // behind by aborted REINDEX CONCURRENTLY runs on partitions of the node + // and edge tables. Postgres names the in-progress copy _ccnew or + // _ccnew; on failure or cancellation those copies remain in + // pg_index with indisvalid = false until explicitly dropped. + sqlSelectOrphanedReindexArtifacts = ` + select n.nspname as schema_name, + i.relname as index_name + from pg_inherits inh + join pg_class p on p.oid = inh.inhparent + join pg_class c on c.oid = inh.inhrelid + join pg_namespace n on n.oid = c.relnamespace + join pg_index x on x.indrelid = c.oid + join pg_class i on i.oid = x.indexrelid + join pg_am a on a.oid = i.relam + where p.relname in ('node', 'edge') + and p.relnamespace = n.oid + and a.amname = 'btree' + and x.indisvalid = false + and i.relname ~ '_ccnew[0-9]*$' + order by n.nspname, i.relname` ) // indexRow is a candidate index discovered by the listing query, prior to @@ -64,10 +90,12 @@ type indexCandidate struct { reason string } -// Optimize satisfies the graph.Optimizer interface. The current phase performs -// a read-only assessment: it identifies btree indexes on partitions of the -// node and edge tables whose leaf density or fragmentation cross the rebuild -// thresholds and logs the candidates. No DDL is executed in this phase. +// Optimize satisfies the graph.Optimizer interface. It performs a pre-flight +// sweep of orphaned REINDEX CONCURRENTLY artifacts, identifies btree indexes +// on partitions of the node and edge tables whose leaf density or +// fragmentation cross the rebuild thresholds, and rebuilds each flagged index +// with REINDEX INDEX CONCURRENTLY. Per-candidate failures are logged and do +// not abort the pass; the loop honors ctx cancellation between candidates. func (s *Driver) Optimize(ctx context.Context) error { if installed, err := s.pgstattupleInstalled(ctx); err != nil { return fmt.Errorf("checking pgstattuple extension: %w", err) @@ -76,6 +104,8 @@ func (s *Driver) Optimize(ctx context.Context) error { return nil } + s.cleanupOrphanedReindexArtifacts(ctx) + indexes, err := s.listGraphPartitionBtreeIndexes(ctx) if err != nil { return fmt.Errorf("listing graph partition btree indexes: %w", err) @@ -111,6 +141,14 @@ func (s *Driver) Optimize(ctx context.Context) error { "Index optimization assessment complete: %d candidate(s) totaling %d bytes", len(candidates), totalBytes, )) + + // Process smallest candidates first so that a mid-pass ctx cancellation + // still results in the maximum number of completed rebuilds. + sort.SliceStable(candidates, func(i, j int) bool { + return candidates[i].sizeBytes < candidates[j].sizeBytes + }) + + s.reindexCandidates(ctx, candidates) return nil } @@ -160,3 +198,99 @@ func (s *Driver) measureIndexBloat(ctx context.Context, indexOID uint32) (float6 } return density, fragmentation, nil } + +// orphanedReindexArtifact is an INVALID btree index left behind by an aborted +// REINDEX CONCURRENTLY run on a node or edge partition. +type orphanedReindexArtifact struct { + schema string + name string +} + +// cleanupOrphanedReindexArtifacts scans for and drops INVALID _ccnew indexes +// left behind by previously aborted REINDEX CONCURRENTLY runs. Failures are +// logged at WARN and never fatal: an orphan wastes disk but does not block +// productive rebuilds, so a stuck cleanup must not gate the rest of the pass. +func (s *Driver) cleanupOrphanedReindexArtifacts(ctx context.Context) { + orphans, err := s.listOrphanedReindexArtifacts(ctx) + if err != nil { + slog.WarnContext(ctx, fmt.Sprintf("Index optimization cleanup: failed to scan for orphaned reindex artifacts; continuing: %v", err)) + return + } + if len(orphans) == 0 { + return + } + + slog.InfoContext(ctx, fmt.Sprintf("Index optimization cleanup: dropping %d orphaned reindex artifact(s)", len(orphans))) + for _, o := range orphans { + if _, err := s.pool.Exec(ctx, buildDropInvalidIndexSQL(o.schema, o.name)); err != nil { + slog.WarnContext(ctx, fmt.Sprintf("Index optimization cleanup: failed to drop orphaned reindex artifact %s.%s; continuing: %v", o.schema, o.name, err)) + continue + } + slog.InfoContext(ctx, fmt.Sprintf("Index optimization cleanup: dropped orphaned reindex artifact %s.%s", o.schema, o.name)) + } +} + +func (s *Driver) listOrphanedReindexArtifacts(ctx context.Context) ([]orphanedReindexArtifact, error) { + rows, err := s.pool.Query(ctx, sqlSelectOrphanedReindexArtifacts) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []orphanedReindexArtifact + for rows.Next() { + var o orphanedReindexArtifact + if err := rows.Scan(&o.schema, &o.name); err != nil { + return nil, err + } + out = append(out, o) + } + return out, rows.Err() +} + +// reindexCandidates rebuilds each flagged index with REINDEX INDEX +// CONCURRENTLY, in the order supplied (Optimize sorts ascending by size). +// Per-candidate failures are logged at WARN and the loop continues; ctx +// cancellation aborts further candidates but in-flight REINDEX statements +// must run to completion in Postgres to avoid leaving _ccnew artifacts. +func (s *Driver) reindexCandidates(ctx context.Context, candidates []indexCandidate) { + for _, c := range candidates { + if err := ctx.Err(); err != nil { + slog.WarnContext(ctx, fmt.Sprintf("Index optimization rebuild cancelled before processing %s.%s: %v", c.schema, c.index, err)) + return + } + + slog.InfoContext(ctx, fmt.Sprintf( + "Index optimization rebuild starting: %s.%s on %s (size=%d bytes, leaf_density=%.1f%%, fragmentation=%.1f%%)", + c.schema, c.index, c.table, c.sizeBytes, c.leafDensity, c.fragmentation, + )) + + started := time.Now() + if _, err := s.pool.Exec(ctx, buildReindexSQL(c.schema, c.index)); err != nil { + slog.WarnContext(ctx, fmt.Sprintf( + "Index optimization rebuild failed for %s.%s after %s; continuing with next candidate: %v", + c.schema, c.index, time.Since(started), err, + )) + continue + } + + slog.InfoContext(ctx, fmt.Sprintf( + "Index optimization rebuild complete: %s.%s in %s", + c.schema, c.index, time.Since(started), + )) + } +} + +// buildReindexSQL composes a REINDEX INDEX CONCURRENTLY statement with a +// safely quoted schema-qualified identifier. REINDEX does not accept query +// parameters; quoting is the only defense against malformed identifiers. +func buildReindexSQL(schema, name string) string { + return "reindex index concurrently " + pgx.Identifier{schema, name}.Sanitize() +} + +// buildDropInvalidIndexSQL composes a DROP INDEX CONCURRENTLY IF EXISTS +// statement for cleaning up an orphaned _ccnew artifact. IF EXISTS prevents +// a race where another session has already dropped the same orphan. +func buildDropInvalidIndexSQL(schema, name string) string { + return "drop index concurrently if exists " + pgx.Identifier{schema, name}.Sanitize() +} diff --git a/drivers/pg/optimize_test.go b/drivers/pg/optimize_test.go index 86fb4f1..53d73da 100644 --- a/drivers/pg/optimize_test.go +++ b/drivers/pg/optimize_test.go @@ -14,7 +14,8 @@ var _ graph.Optimizer = (*Driver)(nil) // TestNeedsReindex exercises the threshold logic that decides whether a // measured index is flagged as a rebuild candidate. The function is pure; // integration coverage of the surrounding pg_extension / pg_inherits / -// pgstatindex queries lands with the Phase 5 REINDEX work. +// pgstatindex queries and the REINDEX execution itself is exercised under +// make test_integration against a real Postgres backend. func TestNeedsReindex(t *testing.T) { cases := []struct { name string @@ -111,3 +112,66 @@ func TestThresholdsAreOrdered(t *testing.T) { assert.Greater(t, highIndexFragmentationThreshold, 0.0, "fragmentation threshold must be positive") assert.Less(t, highIndexFragmentationThreshold, 100.0, "fragmentation threshold must be a percentage") } + +// TestBuildReindexSQL verifies the REINDEX statement is produced with both +// schema and index name quoted via pgx.Identifier.Sanitize, including the +// edge case where an identifier itself contains a double quote. +func TestBuildReindexSQL(t *testing.T) { + cases := []struct { + name string + schema string + index string + want string + }{ + { + name: "ordinary identifiers are double-quoted", + schema: "graph", + index: "edge_1_pkey", + want: `reindex index concurrently "graph"."edge_1_pkey"`, + }, + { + name: "mixed-case identifiers preserve case under quoting", + schema: "Graph", + index: "Edge_1_PKey", + want: `reindex index concurrently "Graph"."Edge_1_PKey"`, + }, + { + name: "embedded double quote is escaped by doubling", + schema: `evil"schema`, + index: "edge_1_pkey", + want: `reindex index concurrently "evil""schema"."edge_1_pkey"`, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, buildReindexSQL(tc.schema, tc.index)) + }) + } +} + +// TestBuildDropInvalidIndexSQL verifies the orphan cleanup statement is +// produced with IF EXISTS and CONCURRENTLY guarding identifier handling. +func TestBuildDropInvalidIndexSQL(t *testing.T) { + got := buildDropInvalidIndexSQL("graph", "edge_1_pkey_ccnew") + assert.Equal(t, `drop index concurrently if exists "graph"."edge_1_pkey_ccnew"`, got) + + // Identifier sanitization must apply to the orphan name as well, since + // _ccnew suffixes are read directly from pg_class.relname. + got = buildDropInvalidIndexSQL("graph", `weird"_ccnew`) + assert.Equal(t, `drop index concurrently if exists "graph"."weird""_ccnew"`, got) +} + +// TestOrphanedReindexArtifactQuery_FiltersByValidityAndNameAndAm guards the +// SQL string that scans for cleanup candidates against accidental loosening +// of its filters, since this query controls the blast radius of DROP INDEX. +func TestOrphanedReindexArtifactQuery_FiltersByValidityAndNameAndAm(t *testing.T) { + for _, fragment := range []string{ + "x.indisvalid = false", + "a.amname = 'btree'", + "i.relname ~ '_ccnew[0-9]*$'", + "p.relname in ('node', 'edge')", + } { + assert.Contains(t, sqlSelectOrphanedReindexArtifacts, fragment, + "orphan-cleanup SQL is missing required filter %q; relaxing this filter risks dropping unrelated indexes", fragment) + } +} From f5b57beab7dd67c8854956aa345c3394ac5ce6ce Mon Sep 17 00:00:00 2001 From: Stephen Hinck Date: Tue, 12 May 2026 16:48:47 -0700 Subject: [PATCH 07/12] PFC --- go.mod | 71 +++++---- go.sum | 488 ++++----------------------------------------------------- 2 files changed, 69 insertions(+), 490 deletions(-) diff --git a/go.mod b/go.mod index 58534cd..d1e4112 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( cuelang.org/go v0.16.0 github.com/RoaringBitmap/roaring/v2 v2.16.0 github.com/antlr4-go/antlr/v4 v4.13.1 - github.com/aws/aws-sdk-go-v2/config v1.31.13 - github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.10 + github.com/aws/aws-sdk-go-v2/config v1.32.13 + github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.21 github.com/axiomhq/hyperloglog v0.2.6 github.com/bits-and-blooms/bitset v1.24.4 github.com/cespare/xxhash/v2 v2.3.0 @@ -24,13 +24,13 @@ require ( require ( github.com/alecthomas/chroma/v2 v2.23.1 github.com/charmbracelet/lipgloss v1.1.0 - github.com/davecgh/go-spew v1.1.1 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/jedib0t/go-pretty/v6 v6.7.8 github.com/kanmu/go-sqlfmt v0.0.2-0.20200215095417-d1e63e2ee5eb github.com/mitchellh/go-wordwrap v1.0.1 github.com/specterops/go-repl v1.0.1 - golang.org/x/term v0.39.0 + golang.org/x/term v0.41.0 ) require ( @@ -60,18 +60,19 @@ require ( github.com/alingse/nilnesserr v0.2.0 // indirect github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect github.com/ashanbrown/makezero/v2 v2.1.0 // indirect - github.com/aws/aws-sdk-go-v2 v1.39.3 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.18.17 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.10 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.10 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.10 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.10 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.29.7 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.2 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.38.7 // indirect - github.com/aws/smithy-go v1.23.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.13 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect + github.com/aws/smithy-go v1.24.2 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bkielbasa/cyclop v1.2.3 // indirect @@ -101,7 +102,8 @@ require ( github.com/fatih/color v1.19.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/firefart/nonamedreturns v1.0.6 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fzipp/gocyclo v0.6.0 // indirect github.com/ghostiam/protogetter v0.3.20 // indirect github.com/go-critic/go-critic v0.14.3 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect @@ -116,7 +118,6 @@ require ( github.com/gobwas/glob v0.2.3 // indirect github.com/godoc-lint/godoc-lint v0.11.2 // indirect github.com/gofrs/flock v0.13.0 // indirect - github.com/golang/protobuf v1.5.3 // indirect github.com/golangci/asciicheck v0.5.0 // indirect github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect github.com/golangci/go-printf-func-name v0.1.1 // indirect @@ -137,7 +138,6 @@ require ( github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect github.com/hashicorp/go-version v1.8.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgio v1.0.0 // indirect @@ -164,7 +164,6 @@ require ( github.com/leonklingele/grouper v1.1.2 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/macabu/inamedparam v0.2.0 // indirect - github.com/magiconair/properties v1.8.6 // indirect github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect github.com/manuelarte/funcorder v0.5.0 // indirect github.com/maratori/testableexamples v1.0.1 // indirect @@ -173,25 +172,23 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mgechev/revive v1.15.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moricho/tparallel v0.3.2 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/muesli/termenv v0.16.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.23.0 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.12.1 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.32.1 // indirect - github.com/prometheus/procfs v0.7.3 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.63.0 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/quasilyte/go-ruleguard v0.4.5 // indirect github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect github.com/quasilyte/gogrep v0.5.0 // indirect @@ -202,6 +199,7 @@ require ( github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/ryancurrah/gomodguard v1.4.1 // indirect github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect @@ -212,15 +210,14 @@ require ( github.com/sonatard/noctx v0.5.1 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cobra v1.10.2 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.12.0 // indirect + github.com/spf13/viper v1.21.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/subosito/gotenv v1.4.1 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/tetafro/godot v1.5.4 // indirect github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect github.com/timonwong/loggercheck v0.11.0 // indirect @@ -240,18 +237,20 @@ require ( go-simpler.org/sloglint v0.11.1 // indirect go.augendre.info/arangolint v0.4.0 // indirect go.augendre.info/fatcontext v0.9.0 // indirect - go.uber.org/multierr v1.10.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.49.0 // indirect golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect golang.org/x/mod v0.34.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.35.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + golang.org/x/text v0.36.0 // indirect + golang.org/x/tools v0.43.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.7.0 // indirect mvdan.cc/gofumpt v0.9.2 // indirect diff --git a/go.sum b/go.sum index ff24078..84f1616 100644 --- a/go.sum +++ b/go.sum @@ -2,38 +2,6 @@ 4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= 4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY= codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI= @@ -46,7 +14,6 @@ dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKf dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI= dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8= github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ= @@ -64,7 +31,6 @@ github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= @@ -84,11 +50,6 @@ github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsr github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ= github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q= github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M= @@ -105,40 +66,25 @@ github.com/ashanbrown/forbidigo/v2 v2.3.0 h1:OZZDOchCgsX5gvToVtEBoV2UWbFfI6RKQTi github.com/ashanbrown/forbidigo/v2 v2.3.0/go.mod h1:5p6VmsG5/1xx3E785W9fouMxIOkvY2rRV9nMdWadd6c= github.com/ashanbrown/makezero/v2 v2.1.0 h1:snuKYMbqosNokUKm+R6/+vOPs8yVAi46La7Ck6QYSaE= github.com/ashanbrown/makezero/v2 v2.1.0/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= -github.com/aws/aws-sdk-go-v2 v1.39.3 h1:h7xSsanJ4EQJXG5iuW4UqgP7qBopLpj84mpkNx3wPjM= -github.com/aws/aws-sdk-go-v2 v1.39.3/go.mod h1:yWSxrnioGUZ4WVv9TgMrNUeLV3PFESn/v+6T/Su8gnM= -github.com/aws/aws-sdk-go-v2/config v1.31.13 h1:wcqQB3B0PgRPUF5ZE/QL1JVOyB0mbPevHFoAMpemR9k= -github.com/aws/aws-sdk-go-v2/config v1.31.13/go.mod h1:ySB5D5ybwqGbT6c3GszZ+u+3KvrlYCUQNo62+hkKOFk= -github.com/aws/aws-sdk-go-v2/credentials v1.18.17 h1:skpEwzN/+H8cdrrtT8y+rvWJGiWWv0DeNAe+4VTf+Vs= -github.com/aws/aws-sdk-go-v2/credentials v1.18.17/go.mod h1:Ed+nXsaYa5uBINovJhcAWkALvXw2ZLk36opcuiSZfJM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.10 h1:UuGVOX48oP4vgQ36oiKmW9RuSeT8jlgQgBFQD+HUiHY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.10/go.mod h1:vM/Ini41PzvudT4YkQyE/+WiQJiQ6jzeDyU8pQKwCac= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.10 h1:xfgjONWMae6+y//dlhVukwt9N+I++FPuiwcQt7DI7Qg= -github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.10/go.mod h1:FO6aarJTHA2N3S8F2A4wKfnX9Jr6MPerJFaqoLgTctU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.10 h1:mj/bdWleWEh81DtpdHKkw41IrS+r3uw1J/VQtbwYYp8= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.10/go.mod h1:7+oEMxAZWP8gZCyjcm9VicI0M61Sx4DJtcGfKYv2yKQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.10 h1:wh+/mn57yhUrFtLIxyFPh2RgxgQz/u+Yrf7hiHGHqKY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.10/go.mod h1:7zirD+ryp5gitJJ2m1BBux56ai8RIRDykXZrJSp540w= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2 h1:xtuxji5CS0JknaXoACOunXOYOQzgfTvGAc9s2QdCJA4= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.2/go.mod h1:zxwi0DIR0rcRcgdbl7E2MSOvxDyyXGBlScvBkARFaLQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.10 h1:DRND0dkCKtJzCj4Xl4OpVbXZgfttY5q712H9Zj7qc/0= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.10/go.mod h1:tGGNmJKOTernmR2+VJ0fCzQRurcPZj9ut60Zu5Fi6us= -github.com/aws/aws-sdk-go-v2/service/sso v1.29.7 h1:fspVFg6qMx0svs40YgRmE7LZXh9VRZvTT35PfdQR6FM= -github.com/aws/aws-sdk-go-v2/service/sso v1.29.7/go.mod h1:BQTKL3uMECaLaUV3Zc2L4Qybv8C6BIXjuu1dOPyxTQs= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.2 h1:scVnW+NLXasGOhy7HhkdT9AGb6kjgW7fJ5xYkUaqHs0= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.2/go.mod h1:FRNCY3zTEWZXBKm2h5UBUPvCVDOecTad9KhynDyGBc0= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.7 h1:VEO5dqFkMsl8QZ2yHsFDJAIZLAkEbaYDB+xdKi0Feic= -github.com/aws/aws-sdk-go-v2/service/sts v1.38.7/go.mod h1:L1xxV3zAdB+qVrVW/pBIrIAnHFWHo6FBbFe4xOGsG/o= -github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M= -github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= +github.com/aws/aws-sdk-go-v2/config v1.32.13 h1:5KgbxMaS2coSWRrx9TX/QtWbqzgQkOdEa3sZPhBhCSg= +github.com/aws/aws-sdk-go-v2/credentials v1.19.13 h1:mA59E3fokBvyEGHKFdnpNNrvaR351cqiHgRg+JzOSRI= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.21 h1:HFn8sVT87KWnGs2Q2gO/brPZc2bR0RXD++cYKRmABzk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 h1:GcLE9ba5ehAQma6wlopUesYg/hbcOhFNWTjELkiWkh4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 h1:mP49nTpfKtpXLt5SLn8Uv8z6W+03jYVoOSAl/c02nog= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/axiomhq/hyperloglog v0.2.6 h1:sRhvvF3RIXWQgAXaTphLp4yJiX4S0IN3MWTaAgZoRJw= github.com/axiomhq/hyperloglog v0.2.6/go.mod h1:YjX/dQqCR/7QYX0g8mu8UZAjpIenz1FKM71UEsjFoTo= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= @@ -163,9 +109,6 @@ github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc= github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk= @@ -180,13 +123,8 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/apd/v3 v3.2.2 h1:R1VaDQkMR321HBM6+6b2eYZfxi0ybPJgUh0Ztr7twzU= github.com/cockroachdb/apd/v3 v3.2.2/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= @@ -203,8 +141,8 @@ github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEy github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM= @@ -213,10 +151,6 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/emicklei/proto v1.14.3 h1:zEhlzNkpP8kN6utonKMzlPfIvy82t5Kb9mufaJxSe1Q= github.com/emicklei/proto v1.14.3/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= @@ -225,10 +159,8 @@ github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E= github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ= @@ -237,14 +169,7 @@ github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1 github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -283,36 +208,6 @@ github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw= @@ -335,31 +230,11 @@ github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2b github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= @@ -367,8 +242,6 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs= github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw= github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= @@ -390,15 +263,10 @@ github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= @@ -464,15 +332,6 @@ github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjz github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= github.com/kamstrup/intmap v0.5.2 h1:qnwBm1mh4XAnW9W9Ue9tZtTff8pS6+s6iKF6JRIV2Dk= @@ -488,8 +347,6 @@ github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/tt github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -528,8 +385,6 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM= github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8= @@ -553,29 +408,19 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q= github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= github.com/neo4j/neo4j-go-driver/v5 v5.28.4 h1:7toxehVcYkZbyxV4W3Ib9VcnyRBQPucF+VwNNmtSXi4= @@ -601,38 +446,17 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/protocolbuffers/txtpbfmt v0.0.0-20260217160748-a481f6a22f94 h1:2PC6Ql3jipz1KvBlqUHjjk6v4aMwE86mfDu1XMH0LR8= github.com/protocolbuffers/txtpbfmt v0.0.0-20260217160748-a481f6a22f94/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA= @@ -661,6 +485,7 @@ github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I= github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg= github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= @@ -678,10 +503,8 @@ github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9Nz github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= @@ -694,18 +517,14 @@ github.com/specterops/go-repl v1.0.1 h1:+rHsCkC8TplTr3qZCitA64xSpBXLF7hqqH5nTa8q github.com/specterops/go-repl v1.0.1/go.mod h1:sv0wjdiFEM2QljRCyIRZN3rZ58fa2IzMwLFGTSu8lDY= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= -github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= @@ -727,8 +546,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= @@ -762,7 +580,6 @@ github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+ github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -781,11 +598,6 @@ go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR5 go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA= go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE= go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -795,21 +607,17 @@ go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -821,42 +629,16 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk= golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -868,39 +650,15 @@ golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -910,24 +668,12 @@ golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -935,54 +681,26 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1001,11 +719,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -1015,58 +730,18 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -1087,116 +762,21 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4= mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s= mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI= mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From c613d0c9a86b44b88e60ed359e6e442d956020d6 Mon Sep 17 00:00:00 2001 From: John Hopper Date: Wed, 13 May 2026 07:59:20 -0700 Subject: [PATCH 08/12] fix (golang): go mod tidy --- go.sum | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/go.sum b/go.sum index 84f1616..8c6f548 100644 --- a/go.sum +++ b/go.sum @@ -67,20 +67,35 @@ github.com/ashanbrown/forbidigo/v2 v2.3.0/go.mod h1:5p6VmsG5/1xx3E785W9fouMxIOkv github.com/ashanbrown/makezero/v2 v2.1.0 h1:snuKYMbqosNokUKm+R6/+vOPs8yVAi46La7Ck6QYSaE= github.com/ashanbrown/makezero/v2 v2.1.0/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= +github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= github.com/aws/aws-sdk-go-v2/config v1.32.13 h1:5KgbxMaS2coSWRrx9TX/QtWbqzgQkOdEa3sZPhBhCSg= +github.com/aws/aws-sdk-go-v2/config v1.32.13/go.mod h1:8zz7wedqtCbw5e9Mi2doEwDyEgHcEE9YOJp6a8jdSMY= github.com/aws/aws-sdk-go-v2/credentials v1.19.13 h1:mA59E3fokBvyEGHKFdnpNNrvaR351cqiHgRg+JzOSRI= +github.com/aws/aws-sdk-go-v2/credentials v1.19.13/go.mod h1:yoTXOQKea18nrM69wGF9jBdG4WocSZA1h38A+t/MAsk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI= github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.21 h1:HFn8sVT87KWnGs2Q2gO/brPZc2bR0RXD++cYKRmABzk= +github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.21/go.mod h1:BGZ/K6gLGJt8K36j6gcsD7WVxmWt0MGBYtr57iLweio= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA= github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI= github.com/aws/aws-sdk-go-v2/service/sso v1.30.14 h1:GcLE9ba5ehAQma6wlopUesYg/hbcOhFNWTjELkiWkh4= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.14/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18 h1:mP49nTpfKtpXLt5SLn8Uv8z6W+03jYVoOSAl/c02nog= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.18/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w= github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/axiomhq/hyperloglog v0.2.6 h1:sRhvvF3RIXWQgAXaTphLp4yJiX4S0IN3MWTaAgZoRJw= github.com/axiomhq/hyperloglog v0.2.6/go.mod h1:YjX/dQqCR/7QYX0g8mu8UZAjpIenz1FKM71UEsjFoTo= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -143,6 +158,7 @@ github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mTEIGbvhcYU3S8+uSNkuMjx/qZFfhtM= @@ -160,7 +176,9 @@ github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4 github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E= github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ= @@ -421,6 +439,7 @@ github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOl github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= github.com/neo4j/neo4j-go-driver/v5 v5.28.4 h1:7toxehVcYkZbyxV4W3Ib9VcnyRBQPucF+VwNNmtSXi4= @@ -453,10 +472,15 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/protocolbuffers/txtpbfmt v0.0.0-20260217160748-a481f6a22f94 h1:2PC6Ql3jipz1KvBlqUHjjk6v4aMwE86mfDu1XMH0LR8= github.com/protocolbuffers/txtpbfmt v0.0.0-20260217160748-a481f6a22f94/go.mod h1:JSbkp0BviKovYYt9XunS95M3mLPibE9bGg+Y95DsEEY= github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA= @@ -486,6 +510,7 @@ github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6 github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg= github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= @@ -518,6 +543,7 @@ github.com/specterops/go-repl v1.0.1/go.mod h1:sv0wjdiFEM2QljRCyIRZN3rZ58fa2IzMw github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -525,6 +551,7 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= @@ -547,6 +574,7 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= @@ -608,11 +636,13 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -630,6 +660,7 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= @@ -720,6 +751,7 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -731,6 +763,7 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -763,6 +796,7 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= From 1470cd8f64b7598ab5c8c6c8b78e5034421223db Mon Sep 17 00:00:00 2001 From: Stephen Hinck Date: Wed, 13 May 2026 11:10:14 -0700 Subject: [PATCH 09/12] cancel in-flight optimization on database switch --- graph/switch.go | 21 +++++++++++++++----- graph/switch_test.go | 47 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/graph/switch.go b/graph/switch.go index ade6600..53ce12a 100644 --- a/graph/switch.go +++ b/graph/switch.go @@ -120,14 +120,25 @@ func (s *DatabaseSwitch) retireInternalContext(ctx context.Context) { // nil. This keeps the wrapper transparent so callers can use the standard // Optimizer type-assertion against a *DatabaseSwitch without having to peek // through it manually. +// +// The call is registered as an internal context so that Switch can cancel +// any in-flight optimization and unblock authoritative database swaps. A +// driver whose Optimize implementation honors ctx cancellation will abort +// promptly and release the read lock guarding the active driver. func (s *DatabaseSwitch) Optimize(ctx context.Context) error { - s.currentDBLock.RLock() - defer s.currentDBLock.RUnlock() + if internalCtx, err := s.newInternalContext(ctx); err != nil { + return err + } else { + defer s.retireInternalContext(internalCtx) + + s.currentDBLock.RLock() + defer s.currentDBLock.RUnlock() - if optimizer, ok := s.currentDB.(Optimizer); ok { - return optimizer.Optimize(ctx) + if optimizer, ok := s.currentDB.(Optimizer); ok { + return optimizer.Optimize(internalCtx) + } + return nil } - return nil } func (s *DatabaseSwitch) ReadTransaction(ctx context.Context, txDelegate TransactionDelegate, options ...TransactionOption) error { diff --git a/graph/switch_test.go b/graph/switch_test.go index 7b91448..57861d2 100644 --- a/graph/switch_test.go +++ b/graph/switch_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "testing" + "time" "github.com/specterops/dawgs/graph" "github.com/stretchr/testify/assert" @@ -94,6 +95,52 @@ func TestDatabaseSwitch_Optimize_FollowsActiveDriverAfterSwitch(t *testing.T) { assert.Equal(t, 1, second.calls, "Optimize should be routed to the new active driver") } +// blockingOptimizingStubDatabase is a stubDatabase whose Optimize method +// blocks until its context is cancelled, allowing tests to exercise the +// cancellation path without relying on real driver behavior. +type blockingOptimizingStubDatabase struct { + stubDatabase + started chan struct{} +} + +func (s *blockingOptimizingStubDatabase) Optimize(ctx context.Context) error { + close(s.started) + <-ctx.Done() + return ctx.Err() +} + +// TestDatabaseSwitch_Optimize_SwitchCancelsInFlight verifies that an +// authoritative database swap aborts an in-flight Optimize call by cancelling +// its internal context, allowing the swap to acquire the write lock once the +// optimizer returns. +func TestDatabaseSwitch_Optimize_SwitchCancelsInFlight(t *testing.T) { + ctx := context.Background() + + driver := &blockingOptimizingStubDatabase{started: make(chan struct{})} + dbSwitch := graph.NewDatabaseSwitch(ctx, driver) + + optimizeErr := make(chan error, 1) + go func() { + optimizeErr <- dbSwitch.Optimize(ctx) + }() + + select { + case <-driver.started: + case <-time.After(time.Second): + t.Fatal("Optimize did not start within timeout") + } + + dbSwitch.Switch(stubDatabase{}) + + select { + case err := <-optimizeErr: + require.Error(t, err) + assert.ErrorIs(t, err, context.Canceled) + case <-time.After(time.Second): + t.Fatal("Optimize did not return after Switch cancelled it") + } +} + // Compile-time assertion that *graph.DatabaseSwitch satisfies graph.Optimizer. // This keeps the wrapper's optional-capability contract enforced by the type // system rather than relying on test discovery alone. From 190756ec78655f0717783a1cb1ebb9490735029a Mon Sep 17 00:00:00 2001 From: Stephen Hinck Date: Wed, 13 May 2026 15:04:08 -0700 Subject: [PATCH 10/12] Add support for GIN flush, missing vacuum analyze --- README.md | 42 ++-- drivers/pg/optimize.go | 441 +++++++++++++++++++++++++++++++++--- drivers/pg/optimize_test.go | 198 ++++++++++++++++ 3 files changed, 638 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 2c65931..117d63b 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ The PostgreSQL driver's schema bootstrap (`drivers/pg/query/sql/schema_up.sql`) |---------------|----------|----------------------------------------------------------------------------------------------------------| | `pg_trgm` | yes | GIN trigram indexes for `contains` / `starts with` / `ends with` lookups on graph entity properties. | | `intarray` | yes | Extended integer array operations used when maintaining node kind arrays. | -| `pgstattuple` | no | Measures btree leaf density and fragmentation for the driver-managed index optimization. | +| `pgstattuple` | no | Measures btree leaf density / fragmentation and GIN pending-list size for driver-managed optimization. | `pgstattuple` is treated as best-effort. Installing it typically requires a superuser role and on some managed Postgres deployments the contrib package is not exposed at all. Bootstrap wraps the install in an exception @@ -106,23 +106,33 @@ optimization in such environments, have an administrator install the extension o create extension if not exists pgstattuple; ``` -### Index Optimization +### Index and Table Optimization When `pgstattuple` is available the driver's `Optimize` method (exposed through the optional `graph.Optimizer` -interface) performs the following on each invocation against btree indexes on partitions of the `node` and `edge` -tables: - -1. Drops any `INVALID` indexes whose name matches the `_ccnew[N]` pattern, which Postgres leaves behind when a - prior `REINDEX CONCURRENTLY` was aborted. Cleanup failures are logged at `WARN` and never fatal; an orphan - wastes disk but does not block productive rebuilds. -2. Measures every candidate index with `pgstatindex` and flags those whose average leaf density falls below - `60%` or whose leaf-page fragmentation reaches `40%`. Thresholds are calibrated against production samples - and a freshly rebuilt baseline (~73.8% density). -3. Rebuilds each flagged index with `REINDEX INDEX CONCURRENTLY`, smallest first so that an early cancellation - still produces the maximum number of completed rebuilds. Per-index failures are logged at `WARN` and the - loop continues with the next candidate. Context cancellation aborts before the next candidate; an in-flight - `REINDEX CONCURRENTLY` runs to completion (interrupting it would leave a `_ccnew` artifact for the next pass - to reap). +interface) runs four phases on each invocation, scoped to partitions of the `node` and `edge` tables: + +1. **Orphan cleanup.** Drops any `INVALID` indexes whose name matches the `_ccnew[N]` pattern, which Postgres + leaves behind when a prior `REINDEX CONCURRENTLY` was aborted. Cleanup failures are logged at `WARN` and + never fatal; an orphan wastes disk but does not block productive rebuilds. +2. **GIN pending-list flush.** Measures every GIN index with `pgstatginindex` and flags those whose pending + list reaches `2048` pages (`16 MiB`, four times the default `gin_pending_list_limit`). Flagged indexes are + flushed with `gin_clean_pending_list`. The threshold is a starting default and may be tightened or relaxed + once fleet samples have been collected. +3. **Vacuum / analyze.** Reads `pg_stat_user_tables` for each partition. A partition is flagged for + `VACUUM (ANALYZE)` when its dead-tuple ratio reaches `20%` and dead-tuple count reaches `10000`, or when + `n_mod_since_analyze` reaches `50000` and the most recent (auto)analyze is older than `24h`. The dead-tuple + ratio mirrors Postgres's default `autovacuum_vacuum_scale_factor`; the analyze staleness window is aligned + with the typical pipeline-level optimization cooldown. `VACUUM FULL` is never emitted. +4. **Btree reindex.** Measures every candidate btree index with `pgstatindex` and flags those whose average + leaf density falls below `60%` or whose leaf-page fragmentation reaches `40%`. Thresholds are calibrated + against production samples and a freshly rebuilt baseline (~73.8% density). Rebuilds run with + `REINDEX INDEX CONCURRENTLY`, smallest first so that an early cancellation still produces the maximum + number of completed rebuilds. + +Phases run in the order above so that vacuum has a chance to reclaim index space before the btree assessment +quantifies bloat. Per-candidate failures within any phase are logged at `WARN` and the loop continues with the +next candidate. Context cancellation aborts before the next candidate; in-flight `REINDEX CONCURRENTLY` and +`VACUUM` statements run to a safe stopping point in Postgres. The pass is not bounded by wall-clock time or candidate count. Callers should serialize `Optimize` against their own scheduling loop. diff --git a/drivers/pg/optimize.go b/drivers/pg/optimize.go index a59725a..ca531e5 100644 --- a/drivers/pg/optimize.go +++ b/drivers/pg/optimize.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "sort" + "strings" "time" "github.com/jackc/pgx/v5" @@ -23,6 +24,34 @@ const ( // splits to warrant a rebuild even when leaf density alone has not crossed // its threshold. highIndexFragmentationThreshold = 40.0 + + // ginPendingPagesFlushThreshold is the pending-list page count at or above + // which a GIN index is flagged for an explicit gin_clean_pending_list call. + // At Postgres's 8 KiB page size this is 16 MiB, four times the default + // gin_pending_list_limit (4 MiB), giving autovacuum room to handle the + // common case while ensuring optimization addresses outliers that have + // outpaced background cleanup. Starting default; not yet calibrated + // against fleet samples. + ginPendingPagesFlushThreshold = 2048 + + // vacuumDeadTupleRatioThreshold mirrors Postgres's default + // autovacuum_vacuum_scale_factor (0.20). Tables whose dead-tuple ratio + // reaches this value are flagged for an explicit VACUUM (ANALYZE). + vacuumDeadTupleRatioThreshold = 0.20 + + // vacuumDeadTupleAbsoluteFloor prevents thrashing tiny partitions where + // a 20% dead-tuple ratio represents only a handful of rows. + vacuumDeadTupleAbsoluteFloor = 10000 + + // analyzeModificationThreshold is the n_mod_since_analyze count at or + // above which an ANALYZE is warranted, provided no analyze has run + // within analyzeStalenessWindow. + analyzeModificationThreshold = 50000 + + // analyzeStalenessWindow is the maximum age of the most recent + // (auto)analyze that suppresses an ANALYZE rerun. Aligned with the + // pipeline-level optimization cooldown. + analyzeStalenessWindow = 24 * time.Hour ) const ( @@ -69,6 +98,54 @@ const ( and x.indisvalid = false and i.relname ~ '_ccnew[0-9]*$' order by n.nspname, i.relname` + + sqlSelectGraphPartitionGinIndexes = ` + select i.oid as index_oid, + n.nspname as schema_name, + c.relname as table_name, + i.relname as index_name, + pg_relation_size(i.oid) as index_size_bytes + from pg_inherits inh + join pg_class p on p.oid = inh.inhparent + join pg_class c on c.oid = inh.inhrelid + join pg_namespace n on n.oid = c.relnamespace + join pg_index x on x.indrelid = c.oid + join pg_class i on i.oid = x.indexrelid + join pg_am a on a.oid = i.relam + where p.relname in ('node', 'edge') + and p.relnamespace = n.oid + and a.amname = 'gin' + order by c.relname, i.relname` + + sqlSelectGinPendingMetrics = `select pending_pages, pending_tuples from pgstatginindex($1::regclass)` + + // sqlCleanGinPendingList flushes the pending list of a single GIN index. + // gin_clean_pending_list returns the number of pages it processed; the + // optimizer ignores the return value and relies on per-call error handling. + sqlCleanGinPendingList = `select gin_clean_pending_list($1::regclass)` + + // sqlSelectGraphPartitionVacuumStats reads pg_stat_user_tables for every + // partition of the node and edge tables. The left join keeps partitions + // that have never been touched by autovacuum visible (the stats collector + // drops rows for relations that have not yet accumulated activity), so + // callers must treat zero/NULL as "no data" rather than "definitely zero". + sqlSelectGraphPartitionVacuumStats = ` + select n.nspname as schema_name, + c.relname as table_name, + coalesce(s.n_live_tup, 0) as n_live_tup, + coalesce(s.n_dead_tup, 0) as n_dead_tup, + coalesce(s.n_mod_since_analyze, 0) as n_mod_since_analyze, + greatest(s.last_analyze, s.last_autoanalyze) as last_analyzed_at + from pg_inherits inh + join pg_class p on p.oid = inh.inhparent + join pg_class c on c.oid = inh.inhrelid + join pg_namespace n on n.oid = c.relnamespace + left join pg_stat_user_tables s + on s.schemaname = n.nspname + and s.relname = c.relname + where p.relname in ('node', 'edge') + and p.relnamespace = n.oid + order by n.nspname, c.relname` ) // indexRow is a candidate index discovered by the listing query, prior to @@ -90,12 +167,20 @@ type indexCandidate struct { reason string } -// Optimize satisfies the graph.Optimizer interface. It performs a pre-flight -// sweep of orphaned REINDEX CONCURRENTLY artifacts, identifies btree indexes -// on partitions of the node and edge tables whose leaf density or -// fragmentation cross the rebuild thresholds, and rebuilds each flagged index -// with REINDEX INDEX CONCURRENTLY. Per-candidate failures are logged and do -// not abort the pass; the loop honors ctx cancellation between candidates. +// Optimize satisfies the graph.Optimizer interface. It runs the maintenance +// phases registered below in order against partitions of the node and edge +// tables. Phases are independent: per-candidate failures within a phase are +// logged at WARN and do not abort subsequent phases, and loops honor ctx +// cancellation between candidates. Optimize returns an error only when the +// pre-flight pgstattuple check itself fails; a missing extension is logged +// and treated as a no-op so that callers can run the daemon unconditionally +// against environments where pgstattuple cannot be installed. +// +// Phase ordering rationale: orphan cleanup is cheap disk reclamation that +// reduces noise for the rest of the pass; GIN pending-list flushes precede +// vacuum so autovacuum's own opportunistic flush is not duplicated; vacuum +// precedes btree reindex so reclaimed space is reflected in the bloat +// measurement that drives the rebuild decision. func (s *Driver) Optimize(ctx context.Context) error { if installed, err := s.pgstattupleInstalled(ctx); err != nil { return fmt.Errorf("checking pgstattuple extension: %w", err) @@ -105,13 +190,40 @@ func (s *Driver) Optimize(ctx context.Context) error { } s.cleanupOrphanedReindexArtifacts(ctx) + s.flushGinPendingLists(ctx) + s.vacuumGraphPartitions(ctx) + s.reindexBloatedBtreeIndexes(ctx) + return nil +} + +// needsReindex applies the rebuild thresholds to a measured index and returns +// a short human-readable reason when the index is flagged. Pure function; +// unit-tested in optimize_test.go. +func needsReindex(c indexCandidate) (string, bool) { + if c.leafDensity < bloatedIndexLeafDensityThreshold { + return fmt.Sprintf("leaf density %.1f%% below %.1f%% threshold", c.leafDensity, bloatedIndexLeafDensityThreshold), true + } + if c.fragmentation >= highIndexFragmentationThreshold { + return fmt.Sprintf("fragmentation %.1f%% at or above %.1f%% threshold", c.fragmentation, highIndexFragmentationThreshold), true + } + return "", false +} +// reindexBloatedBtreeIndexes discovers btree indexes on partitions of the +// node and edge tables, measures each index's leaf density and fragmentation +// with pgstatindex, and rebuilds those flagged by needsReindex with REINDEX +// INDEX CONCURRENTLY. Per-candidate failures are logged at WARN and never +// fatal; the loop honors ctx cancellation between candidates. Candidates are +// processed smallest first so that an early cancellation still produces the +// maximum number of completed rebuilds. +func (s *Driver) reindexBloatedBtreeIndexes(ctx context.Context) { indexes, err := s.listGraphPartitionBtreeIndexes(ctx) if err != nil { - return fmt.Errorf("listing graph partition btree indexes: %w", err) + slog.WarnContext(ctx, fmt.Sprintf("Btree reindex scan failed; continuing: %v", err)) + return } - slog.InfoContext(ctx, fmt.Sprintf("Index optimization assessment starting: %d btree index(es) under consideration", len(indexes))) + slog.InfoContext(ctx, fmt.Sprintf("Btree reindex assessment starting: %d index(es) under consideration", len(indexes))) var ( candidates []indexCandidate @@ -130,7 +242,7 @@ func (s *Driver) Optimize(ctx context.Context) error { candidates = append(candidates, candidate) totalBytes += candidate.sizeBytes slog.InfoContext(ctx, fmt.Sprintf( - "Index optimization candidate: %s.%s on %s (size=%d bytes, leaf_density=%.1f%%, fragmentation=%.1f%%, reason=%s)", + "Btree reindex candidate: %s.%s on %s (size=%d bytes, leaf_density=%.1f%%, fragmentation=%.1f%%, reason=%s)", candidate.schema, candidate.index, candidate.table, candidate.sizeBytes, candidate.leafDensity, candidate.fragmentation, candidate.reason, )) @@ -138,31 +250,15 @@ func (s *Driver) Optimize(ctx context.Context) error { } slog.InfoContext(ctx, fmt.Sprintf( - "Index optimization assessment complete: %d candidate(s) totaling %d bytes", + "Btree reindex assessment complete: %d candidate(s) totaling %d bytes", len(candidates), totalBytes, )) - // Process smallest candidates first so that a mid-pass ctx cancellation - // still results in the maximum number of completed rebuilds. sort.SliceStable(candidates, func(i, j int) bool { return candidates[i].sizeBytes < candidates[j].sizeBytes }) s.reindexCandidates(ctx, candidates) - return nil -} - -// needsReindex applies the rebuild thresholds to a measured index and returns -// a short human-readable reason when the index is flagged. Pure function; -// unit-tested in optimize_test.go. -func needsReindex(c indexCandidate) (string, bool) { - if c.leafDensity < bloatedIndexLeafDensityThreshold { - return fmt.Sprintf("leaf density %.1f%% below %.1f%% threshold", c.leafDensity, bloatedIndexLeafDensityThreshold), true - } - if c.fragmentation >= highIndexFragmentationThreshold { - return fmt.Sprintf("fragmentation %.1f%% at or above %.1f%% threshold", c.fragmentation, highIndexFragmentationThreshold), true - } - return "", false } func (s *Driver) pgstattupleInstalled(ctx context.Context) (bool, error) { @@ -216,11 +312,14 @@ func (s *Driver) cleanupOrphanedReindexArtifacts(ctx context.Context) { slog.WarnContext(ctx, fmt.Sprintf("Index optimization cleanup: failed to scan for orphaned reindex artifacts; continuing: %v", err)) return } + + slog.InfoContext(ctx, fmt.Sprintf("Orphan reindex cleanup assessment starting: %d artifact(s) under consideration", len(orphans))) if len(orphans) == 0 { + slog.InfoContext(ctx, "Orphan reindex cleanup assessment complete: 0 artifact(s) to drop") return } - slog.InfoContext(ctx, fmt.Sprintf("Index optimization cleanup: dropping %d orphaned reindex artifact(s)", len(orphans))) + slog.InfoContext(ctx, fmt.Sprintf("Orphan reindex cleanup assessment complete: %d artifact(s) to drop", len(orphans))) for _, o := range orphans { if _, err := s.pool.Exec(ctx, buildDropInvalidIndexSQL(o.schema, o.name)); err != nil { slog.WarnContext(ctx, fmt.Sprintf("Index optimization cleanup: failed to drop orphaned reindex artifact %s.%s; continuing: %v", o.schema, o.name, err)) @@ -294,3 +393,291 @@ func buildReindexSQL(schema, name string) string { func buildDropInvalidIndexSQL(schema, name string) string { return "drop index concurrently if exists " + pgx.Identifier{schema, name}.Sanitize() } + +// ginIndexRow is a candidate GIN index discovered by the listing query, prior +// to per-index pgstatginindex assessment. +type ginIndexRow struct { + oid uint32 + schema string + table string + index string + sizeBytes int64 +} + +// ginFlushCandidate is a GIN index whose pending-list measurement caused it +// to be flagged for an explicit gin_clean_pending_list call by needsGinFlush. +type ginFlushCandidate struct { + ginIndexRow + pendingPages int64 + pendingTuples int64 + reason string +} + +// needsGinFlush applies the pending-list threshold to a measured GIN index. +// Pure function; unit-tested in optimize_test.go. +func needsGinFlush(c ginFlushCandidate) (string, bool) { + if c.pendingPages >= ginPendingPagesFlushThreshold { + return fmt.Sprintf( + "pending pages %d at or above %d page threshold (%.1f MiB)", + c.pendingPages, ginPendingPagesFlushThreshold, + float64(c.pendingPages*8192)/(1024*1024), + ), true + } + return "", false +} + +// flushGinPendingLists discovers GIN indexes on partitions of the node and +// edge tables, measures each index's pending-list size, and calls +// gin_clean_pending_list on those whose pending pages have crossed the +// threshold. Per-candidate failures are logged at WARN and never fatal: a +// stuck flush wastes time on the next pass but does not block the rest of +// the optimization phases. +func (s *Driver) flushGinPendingLists(ctx context.Context) { + indexes, err := s.listGraphPartitionGinIndexes(ctx) + if err != nil { + slog.WarnContext(ctx, fmt.Sprintf("Index optimization GIN scan failed; continuing: %v", err)) + return + } + + slog.InfoContext(ctx, fmt.Sprintf("GIN flush assessment starting: %d index(es) under consideration", len(indexes))) + + var ( + candidates []ginFlushCandidate + totalPendingPages int64 + ) + for _, idx := range indexes { + pages, tuples, err := s.measureGinPending(ctx, idx.oid) + if err != nil { + slog.WarnContext(ctx, fmt.Sprintf("Skipping GIN pending-list assessment for index %s.%s: %v", idx.schema, idx.index, err)) + continue + } + + c := ginFlushCandidate{ginIndexRow: idx, pendingPages: pages, pendingTuples: tuples} + if reason, flagged := needsGinFlush(c); flagged { + c.reason = reason + candidates = append(candidates, c) + totalPendingPages += c.pendingPages + slog.InfoContext(ctx, fmt.Sprintf( + "GIN flush candidate: %s.%s on %s (size=%d bytes, pending_pages=%d, pending_tuples=%d, reason=%s)", + c.schema, c.index, c.table, c.sizeBytes, c.pendingPages, c.pendingTuples, c.reason, + )) + } + } + + slog.InfoContext(ctx, fmt.Sprintf( + "GIN flush assessment complete: %d candidate(s) totaling %d pending page(s)", + len(candidates), totalPendingPages, + )) + + if len(candidates) == 0 { + return + } + + for _, c := range candidates { + if err := ctx.Err(); err != nil { + slog.WarnContext(ctx, fmt.Sprintf("GIN flush cancelled before processing %s.%s: %v", c.schema, c.index, err)) + return + } + + started := time.Now() + if _, err := s.pool.Exec(ctx, sqlCleanGinPendingList, c.oid); err != nil { + slog.WarnContext(ctx, fmt.Sprintf( + "GIN flush failed for %s.%s after %s; continuing with next candidate: %v", + c.schema, c.index, time.Since(started), err, + )) + continue + } + + slog.InfoContext(ctx, fmt.Sprintf( + "GIN flush complete: %s.%s in %s (was %d pending pages, %d pending tuples)", + c.schema, c.index, time.Since(started), c.pendingPages, c.pendingTuples, + )) + } +} + +func (s *Driver) listGraphPartitionGinIndexes(ctx context.Context) ([]ginIndexRow, error) { + rows, err := s.pool.Query(ctx, sqlSelectGraphPartitionGinIndexes) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []ginIndexRow + for rows.Next() { + var r ginIndexRow + if err := rows.Scan(&r.oid, &r.schema, &r.table, &r.index, &r.sizeBytes); err != nil { + return nil, err + } + out = append(out, r) + } + return out, rows.Err() +} + +func (s *Driver) measureGinPending(ctx context.Context, indexOID uint32) (int64, int64, error) { + var pendingPages, pendingTuples int64 + if err := s.pool.QueryRow(ctx, sqlSelectGinPendingMetrics, indexOID).Scan(&pendingPages, &pendingTuples); err != nil { + return 0, 0, err + } + return pendingPages, pendingTuples, nil +} + +// vacuumStatsRow is a snapshot of pg_stat_user_tables for one partition of +// the node or edge tables. lastAnalyzedAt is nil when the table has never +// been analyzed (manually or by autovacuum). +type vacuumStatsRow struct { + schema string + table string + liveTuples int64 + deadTuples int64 + modSinceAnalyze int64 + lastAnalyzedAt *time.Time +} + +// vacuumCandidate is a partition flagged by vacuumAssessment. needsVacuum +// and needsAnalyze are independent and may both be true; the executor issues +// a single VACUUM (ANALYZE) when either fires for the analyze trigger. +type vacuumCandidate struct { + vacuumStatsRow + needsVacuum bool + needsAnalyze bool + reason string +} + +// vacuumAssessment applies the vacuum and analyze thresholds to a measured +// partition and returns a candidate plus a flagged bool. Pure function; +// unit-tested in optimize_test.go. now is injected to keep the staleness +// test deterministic. +func vacuumAssessment(r vacuumStatsRow, now time.Time) (vacuumCandidate, bool) { + candidate := vacuumCandidate{vacuumStatsRow: r} + var reasons []string + + if r.deadTuples >= vacuumDeadTupleAbsoluteFloor { + total := r.liveTuples + r.deadTuples + if total > 0 { + ratio := float64(r.deadTuples) / float64(total) + if ratio >= vacuumDeadTupleRatioThreshold { + candidate.needsVacuum = true + reasons = append(reasons, fmt.Sprintf( + "dead tuples %d (%.1f%% of %d) at or above %.0f%% threshold", + r.deadTuples, ratio*100, total, vacuumDeadTupleRatioThreshold*100, + )) + } + } + } + + if r.modSinceAnalyze >= analyzeModificationThreshold { + stale := r.lastAnalyzedAt == nil || now.Sub(*r.lastAnalyzedAt) >= analyzeStalenessWindow + if stale { + candidate.needsAnalyze = true + ageMsg := "never analyzed" + if r.lastAnalyzedAt != nil { + ageMsg = fmt.Sprintf("last analyzed %s ago", now.Sub(*r.lastAnalyzedAt).Round(time.Minute)) + } + reasons = append(reasons, fmt.Sprintf( + "modifications since analyze %d at or above %d threshold and %s", + r.modSinceAnalyze, analyzeModificationThreshold, ageMsg, + )) + } + } + + if !candidate.needsVacuum && !candidate.needsAnalyze { + return candidate, false + } + candidate.reason = strings.Join(reasons, "; ") + return candidate, true +} + +// vacuumGraphPartitions discovers partitions of the node and edge tables, +// reads pg_stat_user_tables for each, and runs VACUUM (ANALYZE) on those +// flagged by vacuumAssessment. Per-candidate failures are logged at WARN and +// never fatal. The loop honors ctx cancellation between partitions; an +// in-flight VACUUM aborts at the next safe point when ctx is cancelled. +func (s *Driver) vacuumGraphPartitions(ctx context.Context) { + stats, err := s.listGraphPartitionVacuumStats(ctx) + if err != nil { + slog.WarnContext(ctx, fmt.Sprintf("Vacuum assessment scan failed; continuing: %v", err)) + return + } + + slog.InfoContext(ctx, fmt.Sprintf("Vacuum assessment starting: %d partition(s) under consideration", len(stats))) + + now := time.Now() + var ( + candidates []vacuumCandidate + vacuumFlagged int + analyzeFlagged int + ) + for _, r := range stats { + if c, flagged := vacuumAssessment(r, now); flagged { + candidates = append(candidates, c) + if c.needsVacuum { + vacuumFlagged++ + } + if c.needsAnalyze { + analyzeFlagged++ + } + slog.InfoContext(ctx, fmt.Sprintf( + "Vacuum candidate: %s.%s (live=%d, dead=%d, mod_since_analyze=%d, reason=%s)", + c.schema, c.table, c.liveTuples, c.deadTuples, c.modSinceAnalyze, c.reason, + )) + } + } + + slog.InfoContext(ctx, fmt.Sprintf( + "Vacuum assessment complete: %d candidate(s) (%d flagged for VACUUM, %d for ANALYZE)", + len(candidates), vacuumFlagged, analyzeFlagged, + )) + + if len(candidates) == 0 { + return + } + + for _, c := range candidates { + if err := ctx.Err(); err != nil { + slog.WarnContext(ctx, fmt.Sprintf("Vacuum cancelled before processing %s.%s: %v", c.schema, c.table, err)) + return + } + + started := time.Now() + if _, err := s.pool.Exec(ctx, buildVacuumSQL(c.schema, c.table)); err != nil { + slog.WarnContext(ctx, fmt.Sprintf( + "Vacuum failed for %s.%s after %s; continuing with next candidate: %v", + c.schema, c.table, time.Since(started), err, + )) + continue + } + + slog.InfoContext(ctx, fmt.Sprintf( + "Vacuum complete: %s.%s in %s", + c.schema, c.table, time.Since(started), + )) + } +} + +func (s *Driver) listGraphPartitionVacuumStats(ctx context.Context) ([]vacuumStatsRow, error) { + rows, err := s.pool.Query(ctx, sqlSelectGraphPartitionVacuumStats) + if err != nil { + return nil, err + } + defer rows.Close() + + var out []vacuumStatsRow + for rows.Next() { + var r vacuumStatsRow + if err := rows.Scan(&r.schema, &r.table, &r.liveTuples, &r.deadTuples, &r.modSinceAnalyze, &r.lastAnalyzedAt); err != nil { + return nil, err + } + out = append(out, r) + } + return out, rows.Err() +} + +// buildVacuumSQL composes a VACUUM (ANALYZE) statement with a safely quoted +// schema-qualified identifier. VACUUM does not accept query parameters, and +// VACUUM (ANALYZE) is preferred over a separate ANALYZE because the planner +// statistics ride along at no additional cost when a vacuum is already +// scheduled. VACUUM FULL is intentionally never emitted: it takes +// AccessExclusiveLock and rewrites the table. +func buildVacuumSQL(schema, name string) string { + return "vacuum (analyze) " + pgx.Identifier{schema, name}.Sanitize() +} diff --git a/drivers/pg/optimize_test.go b/drivers/pg/optimize_test.go index 53d73da..2dd37e3 100644 --- a/drivers/pg/optimize_test.go +++ b/drivers/pg/optimize_test.go @@ -3,6 +3,7 @@ package pg import ( "strings" "testing" + "time" "github.com/specterops/dawgs/graph" "github.com/stretchr/testify/assert" @@ -111,6 +112,12 @@ func TestThresholdsAreOrdered(t *testing.T) { assert.Less(t, bloatedIndexLeafDensityThreshold, 100.0, "leaf density threshold must be a percentage") assert.Greater(t, highIndexFragmentationThreshold, 0.0, "fragmentation threshold must be positive") assert.Less(t, highIndexFragmentationThreshold, 100.0, "fragmentation threshold must be a percentage") + assert.Greater(t, ginPendingPagesFlushThreshold, int(0), "GIN pending-pages threshold must be positive") + assert.Greater(t, vacuumDeadTupleRatioThreshold, 0.0, "vacuum dead-tuple ratio threshold must be positive") + assert.Less(t, vacuumDeadTupleRatioThreshold, 1.0, "vacuum dead-tuple ratio threshold must be a fraction") + assert.Greater(t, vacuumDeadTupleAbsoluteFloor, int(0), "vacuum dead-tuple floor must be positive") + assert.Greater(t, analyzeModificationThreshold, int(0), "analyze modification threshold must be positive") + assert.Greater(t, analyzeStalenessWindow, time.Duration(0), "analyze staleness window must be positive") } // TestBuildReindexSQL verifies the REINDEX statement is produced with both @@ -175,3 +182,194 @@ func TestOrphanedReindexArtifactQuery_FiltersByValidityAndNameAndAm(t *testing.T "orphan-cleanup SQL is missing required filter %q; relaxing this filter risks dropping unrelated indexes", fragment) } } + +// TestNeedsGinFlush exercises the threshold logic that decides whether a +// measured GIN index is flagged for an explicit gin_clean_pending_list call. +// The function is pure; integration coverage of the surrounding pgstatginindex +// query and the gin_clean_pending_list execution itself is exercised under +// make test_integration against a real Postgres backend. +func TestNeedsGinFlush(t *testing.T) { + cases := []struct { + name string + pendingPages int64 + wantFlagged bool + wantReasonHas string + }{ + { + name: "empty pending list is not flagged", + pendingPages: 0, + wantFlagged: false, + }, + { + name: "pending list one page below threshold is not flagged", + pendingPages: ginPendingPagesFlushThreshold - 1, + wantFlagged: false, + }, + { + name: "pending list exactly at threshold is flagged", + pendingPages: ginPendingPagesFlushThreshold, + wantFlagged: true, + wantReasonHas: "16.0 MiB", + }, + { + name: "pending list well above threshold is flagged", + pendingPages: 22076, + wantFlagged: true, + wantReasonHas: "pending pages 22076", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + c := ginFlushCandidate{pendingPages: tc.pendingPages} + reason, flagged := needsGinFlush(c) + assert.Equal(t, tc.wantFlagged, flagged, "unexpected flagged result for %s", tc.name) + if tc.wantFlagged { + assert.True(t, strings.Contains(reason, tc.wantReasonHas), + "reason %q does not mention expected substring %q", reason, tc.wantReasonHas) + } else { + assert.Empty(t, reason, "expected empty reason when not flagged") + } + }) + } +} + +// TestVacuumAssessment exercises the threshold logic that decides whether a +// partition is flagged for VACUUM (ANALYZE). Both triggers are independent +// and may co-fire; the test covers each in isolation plus the boundary cases. +func TestVacuumAssessment(t *testing.T) { + now := time.Date(2026, 5, 13, 12, 0, 0, 0, time.UTC) + freshAnalyze := now.Add(-time.Hour) + staleAnalyze := now.Add(-(analyzeStalenessWindow + time.Minute)) + + cases := []struct { + name string + row vacuumStatsRow + wantFlagged bool + wantNeedsVacuum bool + wantNeedsAnalyze bool + wantReasonHas string + }{ + { + name: "healthy partition with low dead tuples and recent analyze is not flagged", + row: vacuumStatsRow{liveTuples: 1_000_000, deadTuples: 5_000, modSinceAnalyze: 1_000, lastAnalyzedAt: &freshAnalyze}, + wantFlagged: false, + }, + { + name: "dead-tuple ratio above threshold but below absolute floor is not flagged", + row: vacuumStatsRow{liveTuples: 100, deadTuples: 100, modSinceAnalyze: 0, lastAnalyzedAt: &freshAnalyze}, + wantFlagged: false, + }, + { + name: "dead-tuple ratio at threshold and above floor flags vacuum", + row: vacuumStatsRow{liveTuples: 40_000, deadTuples: 10_000, modSinceAnalyze: 0, lastAnalyzedAt: &freshAnalyze}, + wantFlagged: true, + wantNeedsVacuum: true, + wantNeedsAnalyze: false, + wantReasonHas: "dead tuples 10000", + }, + { + name: "modifications above threshold with stale analyze flags analyze", + row: vacuumStatsRow{liveTuples: 1_000_000, deadTuples: 0, modSinceAnalyze: analyzeModificationThreshold, lastAnalyzedAt: &staleAnalyze}, + wantFlagged: true, + wantNeedsVacuum: false, + wantNeedsAnalyze: true, + wantReasonHas: "modifications since analyze", + }, + { + name: "modifications above threshold with fresh analyze is not flagged", + row: vacuumStatsRow{liveTuples: 1_000_000, deadTuples: 0, modSinceAnalyze: analyzeModificationThreshold, lastAnalyzedAt: &freshAnalyze}, + wantFlagged: false, + wantNeedsAnalyze: false, + }, + { + name: "never-analyzed partition above modification threshold flags analyze", + row: vacuumStatsRow{liveTuples: 1_000_000, deadTuples: 0, modSinceAnalyze: analyzeModificationThreshold, lastAnalyzedAt: nil}, + wantFlagged: true, + wantNeedsAnalyze: true, + wantReasonHas: "never analyzed", + }, + { + name: "both triggers fire concurrently", + row: vacuumStatsRow{liveTuples: 100_000, deadTuples: 50_000, modSinceAnalyze: analyzeModificationThreshold, lastAnalyzedAt: &staleAnalyze}, + wantFlagged: true, + wantNeedsVacuum: true, + wantNeedsAnalyze: true, + wantReasonHas: "dead tuples", + }, + { + name: "empty partition is not flagged", + row: vacuumStatsRow{liveTuples: 0, deadTuples: 0, modSinceAnalyze: 0, lastAnalyzedAt: nil}, + wantFlagged: false, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + candidate, flagged := vacuumAssessment(tc.row, now) + assert.Equal(t, tc.wantFlagged, flagged, "unexpected flagged result") + assert.Equal(t, tc.wantNeedsVacuum, candidate.needsVacuum, "unexpected needsVacuum") + assert.Equal(t, tc.wantNeedsAnalyze, candidate.needsAnalyze, "unexpected needsAnalyze") + if tc.wantFlagged { + assert.True(t, strings.Contains(candidate.reason, tc.wantReasonHas), + "reason %q does not mention expected substring %q", candidate.reason, tc.wantReasonHas) + } else { + assert.Empty(t, candidate.reason, "expected empty reason when not flagged") + } + }) + } +} + +// TestBuildVacuumSQL verifies the VACUUM (ANALYZE) statement is produced with +// both schema and table name quoted via pgx.Identifier.Sanitize. VACUUM FULL +// must never appear in the generated SQL. +func TestBuildVacuumSQL(t *testing.T) { + cases := []struct { + name string + schema string + table string + want string + }{ + { + name: "ordinary identifiers are double-quoted", + schema: "graph", + table: "edge_1", + want: `vacuum (analyze) "graph"."edge_1"`, + }, + { + name: "embedded double quote is escaped by doubling", + schema: `evil"schema`, + table: "edge_1", + want: `vacuum (analyze) "evil""schema"."edge_1"`, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := buildVacuumSQL(tc.schema, tc.table) + assert.Equal(t, tc.want, got) + assert.False(t, strings.Contains(strings.ToLower(got), "full"), + "generated VACUUM statement must not contain FULL: %q", got) + }) + } +} + +// TestGinIndexQuery_FiltersByAmAndParents guards the SQL string that scans +// for GIN flush candidates against accidental loosening of its filters, +// since this query controls the blast radius of gin_clean_pending_list. +func TestGinIndexQuery_FiltersByAmAndParents(t *testing.T) { + for _, fragment := range []string{ + "a.amname = 'gin'", + "p.relname in ('node', 'edge')", + } { + assert.Contains(t, sqlSelectGraphPartitionGinIndexes, fragment, + "GIN listing SQL is missing required filter %q; relaxing this filter risks flushing unrelated indexes", fragment) + } +} + +// TestVacuumStatsQuery_FiltersByParents guards the SQL string that scans for +// vacuum candidates against accidental loosening of its filters, since this +// query controls the blast radius of VACUUM (ANALYZE). +func TestVacuumStatsQuery_FiltersByParents(t *testing.T) { + assert.Contains(t, sqlSelectGraphPartitionVacuumStats, "p.relname in ('node', 'edge')", + "vacuum stats SQL is missing required parent-table filter; relaxing this filter risks vacuuming unrelated tables") +} From bc9f4570a00f4ce7319ededeb100ae136172bbcd Mon Sep 17 00:00:00 2001 From: Stephen Hinck Date: Wed, 13 May 2026 15:23:51 -0700 Subject: [PATCH 11/12] make all --- go.mod | 4 ---- 1 file changed, 4 deletions(-) diff --git a/go.mod b/go.mod index d1e4112..057ea9a 100644 --- a/go.mod +++ b/go.mod @@ -103,7 +103,6 @@ require ( github.com/fatih/structtag v1.2.0 // indirect github.com/firefart/nonamedreturns v1.0.6 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fzipp/gocyclo v0.6.0 // indirect github.com/ghostiam/protogetter v0.3.20 // indirect github.com/go-critic/go-critic v0.14.3 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect @@ -246,10 +245,7 @@ require ( golang.org/x/mod v0.34.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect golang.org/x/text v0.36.0 // indirect - golang.org/x/tools v0.43.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.7.0 // indirect From 7ae639c1f2f803827dd93758829d0db01976ee8c Mon Sep 17 00:00:00 2001 From: Stephen Hinck Date: Wed, 13 May 2026 16:36:52 -0700 Subject: [PATCH 12/12] Drive down CRAP --- drivers/pg/optimize.go | 78 ++++--- drivers/pg/optimize_fake_test.go | 148 +++++++++++++ drivers/pg/optimize_phases_test.go | 334 +++++++++++++++++++++++++++++ drivers/pg/optimize_wiring_test.go | 150 +++++++++++++ 4 files changed, 675 insertions(+), 35 deletions(-) create mode 100644 drivers/pg/optimize_fake_test.go create mode 100644 drivers/pg/optimize_phases_test.go create mode 100644 drivers/pg/optimize_wiring_test.go diff --git a/drivers/pg/optimize.go b/drivers/pg/optimize.go index ca531e5..35d14bd 100644 --- a/drivers/pg/optimize.go +++ b/drivers/pg/optimize.go @@ -182,17 +182,25 @@ type indexCandidate struct { // precedes btree reindex so reclaimed space is reflected in the bloat // measurement that drives the rebuild decision. func (s *Driver) Optimize(ctx context.Context) error { - if installed, err := s.pgstattupleInstalled(ctx); err != nil { + return optimize(ctx, s.pool) +} + +// optimize is the package-level implementation that the four maintenance +// phases dispatch through. Splitting Optimize from its receiver lets unit +// tests drive the phase wiring against a fake driver without standing up a +// real *pgxpool.Pool; the *Driver method is a forwarder. +func optimize(ctx context.Context, db driver) error { + if installed, err := pgstattupleInstalled(ctx, db); err != nil { return fmt.Errorf("checking pgstattuple extension: %w", err) } else if !installed { slog.WarnContext(ctx, "Index optimization skipped: pgstattuple extension is not installed; verify the DAWGS schema bootstrap completed successfully") return nil } - s.cleanupOrphanedReindexArtifacts(ctx) - s.flushGinPendingLists(ctx) - s.vacuumGraphPartitions(ctx) - s.reindexBloatedBtreeIndexes(ctx) + cleanupOrphanedReindexArtifacts(ctx, db) + flushGinPendingLists(ctx, db) + vacuumGraphPartitions(ctx, db) + reindexBloatedBtreeIndexes(ctx, db) return nil } @@ -216,8 +224,8 @@ func needsReindex(c indexCandidate) (string, bool) { // fatal; the loop honors ctx cancellation between candidates. Candidates are // processed smallest first so that an early cancellation still produces the // maximum number of completed rebuilds. -func (s *Driver) reindexBloatedBtreeIndexes(ctx context.Context) { - indexes, err := s.listGraphPartitionBtreeIndexes(ctx) +func reindexBloatedBtreeIndexes(ctx context.Context, db driver) { + indexes, err := listGraphPartitionBtreeIndexes(ctx, db) if err != nil { slog.WarnContext(ctx, fmt.Sprintf("Btree reindex scan failed; continuing: %v", err)) return @@ -230,7 +238,7 @@ func (s *Driver) reindexBloatedBtreeIndexes(ctx context.Context) { totalBytes int64 ) for _, idx := range indexes { - density, fragmentation, err := s.measureIndexBloat(ctx, idx.oid) + density, fragmentation, err := measureIndexBloat(ctx, db, idx.oid) if err != nil { slog.WarnContext(ctx, fmt.Sprintf("Skipping bloat assessment for index %s.%s: %v", idx.schema, idx.index, err)) continue @@ -258,19 +266,19 @@ func (s *Driver) reindexBloatedBtreeIndexes(ctx context.Context) { return candidates[i].sizeBytes < candidates[j].sizeBytes }) - s.reindexCandidates(ctx, candidates) + reindexCandidates(ctx, db, candidates) } -func (s *Driver) pgstattupleInstalled(ctx context.Context) (bool, error) { +func pgstattupleInstalled(ctx context.Context, db driver) (bool, error) { var installed bool - if err := s.pool.QueryRow(ctx, sqlPgstattupleInstalled).Scan(&installed); err != nil { + if err := db.QueryRow(ctx, sqlPgstattupleInstalled).Scan(&installed); err != nil { return false, err } return installed, nil } -func (s *Driver) listGraphPartitionBtreeIndexes(ctx context.Context) ([]indexRow, error) { - rows, err := s.pool.Query(ctx, sqlSelectGraphPartitionBtreeIndexes) +func listGraphPartitionBtreeIndexes(ctx context.Context, db driver) ([]indexRow, error) { + rows, err := db.Query(ctx, sqlSelectGraphPartitionBtreeIndexes) if err != nil { return nil, err } @@ -287,9 +295,9 @@ func (s *Driver) listGraphPartitionBtreeIndexes(ctx context.Context) ([]indexRow return out, rows.Err() } -func (s *Driver) measureIndexBloat(ctx context.Context, indexOID uint32) (float64, float64, error) { +func measureIndexBloat(ctx context.Context, db driver, indexOID uint32) (float64, float64, error) { var density, fragmentation float64 - if err := s.pool.QueryRow(ctx, sqlSelectIndexBloatMetrics, indexOID).Scan(&density, &fragmentation); err != nil { + if err := db.QueryRow(ctx, sqlSelectIndexBloatMetrics, indexOID).Scan(&density, &fragmentation); err != nil { return 0, 0, err } return density, fragmentation, nil @@ -306,8 +314,8 @@ type orphanedReindexArtifact struct { // left behind by previously aborted REINDEX CONCURRENTLY runs. Failures are // logged at WARN and never fatal: an orphan wastes disk but does not block // productive rebuilds, so a stuck cleanup must not gate the rest of the pass. -func (s *Driver) cleanupOrphanedReindexArtifacts(ctx context.Context) { - orphans, err := s.listOrphanedReindexArtifacts(ctx) +func cleanupOrphanedReindexArtifacts(ctx context.Context, db driver) { + orphans, err := listOrphanedReindexArtifacts(ctx, db) if err != nil { slog.WarnContext(ctx, fmt.Sprintf("Index optimization cleanup: failed to scan for orphaned reindex artifacts; continuing: %v", err)) return @@ -321,7 +329,7 @@ func (s *Driver) cleanupOrphanedReindexArtifacts(ctx context.Context) { slog.InfoContext(ctx, fmt.Sprintf("Orphan reindex cleanup assessment complete: %d artifact(s) to drop", len(orphans))) for _, o := range orphans { - if _, err := s.pool.Exec(ctx, buildDropInvalidIndexSQL(o.schema, o.name)); err != nil { + if _, err := db.Exec(ctx, buildDropInvalidIndexSQL(o.schema, o.name)); err != nil { slog.WarnContext(ctx, fmt.Sprintf("Index optimization cleanup: failed to drop orphaned reindex artifact %s.%s; continuing: %v", o.schema, o.name, err)) continue } @@ -329,8 +337,8 @@ func (s *Driver) cleanupOrphanedReindexArtifacts(ctx context.Context) { } } -func (s *Driver) listOrphanedReindexArtifacts(ctx context.Context) ([]orphanedReindexArtifact, error) { - rows, err := s.pool.Query(ctx, sqlSelectOrphanedReindexArtifacts) +func listOrphanedReindexArtifacts(ctx context.Context, db driver) ([]orphanedReindexArtifact, error) { + rows, err := db.Query(ctx, sqlSelectOrphanedReindexArtifacts) if err != nil { return nil, err } @@ -352,7 +360,7 @@ func (s *Driver) listOrphanedReindexArtifacts(ctx context.Context) ([]orphanedRe // Per-candidate failures are logged at WARN and the loop continues; ctx // cancellation aborts further candidates but in-flight REINDEX statements // must run to completion in Postgres to avoid leaving _ccnew artifacts. -func (s *Driver) reindexCandidates(ctx context.Context, candidates []indexCandidate) { +func reindexCandidates(ctx context.Context, db driver, candidates []indexCandidate) { for _, c := range candidates { if err := ctx.Err(); err != nil { slog.WarnContext(ctx, fmt.Sprintf("Index optimization rebuild cancelled before processing %s.%s: %v", c.schema, c.index, err)) @@ -365,7 +373,7 @@ func (s *Driver) reindexCandidates(ctx context.Context, candidates []indexCandid )) started := time.Now() - if _, err := s.pool.Exec(ctx, buildReindexSQL(c.schema, c.index)); err != nil { + if _, err := db.Exec(ctx, buildReindexSQL(c.schema, c.index)); err != nil { slog.WarnContext(ctx, fmt.Sprintf( "Index optimization rebuild failed for %s.%s after %s; continuing with next candidate: %v", c.schema, c.index, time.Since(started), err, @@ -432,8 +440,8 @@ func needsGinFlush(c ginFlushCandidate) (string, bool) { // threshold. Per-candidate failures are logged at WARN and never fatal: a // stuck flush wastes time on the next pass but does not block the rest of // the optimization phases. -func (s *Driver) flushGinPendingLists(ctx context.Context) { - indexes, err := s.listGraphPartitionGinIndexes(ctx) +func flushGinPendingLists(ctx context.Context, db driver) { + indexes, err := listGraphPartitionGinIndexes(ctx, db) if err != nil { slog.WarnContext(ctx, fmt.Sprintf("Index optimization GIN scan failed; continuing: %v", err)) return @@ -446,7 +454,7 @@ func (s *Driver) flushGinPendingLists(ctx context.Context) { totalPendingPages int64 ) for _, idx := range indexes { - pages, tuples, err := s.measureGinPending(ctx, idx.oid) + pages, tuples, err := measureGinPending(ctx, db, idx.oid) if err != nil { slog.WarnContext(ctx, fmt.Sprintf("Skipping GIN pending-list assessment for index %s.%s: %v", idx.schema, idx.index, err)) continue @@ -480,7 +488,7 @@ func (s *Driver) flushGinPendingLists(ctx context.Context) { } started := time.Now() - if _, err := s.pool.Exec(ctx, sqlCleanGinPendingList, c.oid); err != nil { + if _, err := db.Exec(ctx, sqlCleanGinPendingList, c.oid); err != nil { slog.WarnContext(ctx, fmt.Sprintf( "GIN flush failed for %s.%s after %s; continuing with next candidate: %v", c.schema, c.index, time.Since(started), err, @@ -495,8 +503,8 @@ func (s *Driver) flushGinPendingLists(ctx context.Context) { } } -func (s *Driver) listGraphPartitionGinIndexes(ctx context.Context) ([]ginIndexRow, error) { - rows, err := s.pool.Query(ctx, sqlSelectGraphPartitionGinIndexes) +func listGraphPartitionGinIndexes(ctx context.Context, db driver) ([]ginIndexRow, error) { + rows, err := db.Query(ctx, sqlSelectGraphPartitionGinIndexes) if err != nil { return nil, err } @@ -513,9 +521,9 @@ func (s *Driver) listGraphPartitionGinIndexes(ctx context.Context) ([]ginIndexRo return out, rows.Err() } -func (s *Driver) measureGinPending(ctx context.Context, indexOID uint32) (int64, int64, error) { +func measureGinPending(ctx context.Context, db driver, indexOID uint32) (int64, int64, error) { var pendingPages, pendingTuples int64 - if err := s.pool.QueryRow(ctx, sqlSelectGinPendingMetrics, indexOID).Scan(&pendingPages, &pendingTuples); err != nil { + if err := db.QueryRow(ctx, sqlSelectGinPendingMetrics, indexOID).Scan(&pendingPages, &pendingTuples); err != nil { return 0, 0, err } return pendingPages, pendingTuples, nil @@ -592,8 +600,8 @@ func vacuumAssessment(r vacuumStatsRow, now time.Time) (vacuumCandidate, bool) { // flagged by vacuumAssessment. Per-candidate failures are logged at WARN and // never fatal. The loop honors ctx cancellation between partitions; an // in-flight VACUUM aborts at the next safe point when ctx is cancelled. -func (s *Driver) vacuumGraphPartitions(ctx context.Context) { - stats, err := s.listGraphPartitionVacuumStats(ctx) +func vacuumGraphPartitions(ctx context.Context, db driver) { + stats, err := listGraphPartitionVacuumStats(ctx, db) if err != nil { slog.WarnContext(ctx, fmt.Sprintf("Vacuum assessment scan failed; continuing: %v", err)) return @@ -639,7 +647,7 @@ func (s *Driver) vacuumGraphPartitions(ctx context.Context) { } started := time.Now() - if _, err := s.pool.Exec(ctx, buildVacuumSQL(c.schema, c.table)); err != nil { + if _, err := db.Exec(ctx, buildVacuumSQL(c.schema, c.table)); err != nil { slog.WarnContext(ctx, fmt.Sprintf( "Vacuum failed for %s.%s after %s; continuing with next candidate: %v", c.schema, c.table, time.Since(started), err, @@ -654,8 +662,8 @@ func (s *Driver) vacuumGraphPartitions(ctx context.Context) { } } -func (s *Driver) listGraphPartitionVacuumStats(ctx context.Context) ([]vacuumStatsRow, error) { - rows, err := s.pool.Query(ctx, sqlSelectGraphPartitionVacuumStats) +func listGraphPartitionVacuumStats(ctx context.Context, db driver) ([]vacuumStatsRow, error) { + rows, err := db.Query(ctx, sqlSelectGraphPartitionVacuumStats) if err != nil { return nil, err } diff --git a/drivers/pg/optimize_fake_test.go b/drivers/pg/optimize_fake_test.go new file mode 100644 index 0000000..cbdc15b --- /dev/null +++ b/drivers/pg/optimize_fake_test.go @@ -0,0 +1,148 @@ +package pg + +import ( + "context" + "reflect" + "strings" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +// fakeDriver is a hand-rolled stand-in for the package-private `driver` +// interface used by optimize.go. It lets unit tests exercise the phase +// wiring (list -> assess -> exec) without standing up a *pgxpool.Pool. +// +// Tests register canned Query/QueryRow responses keyed by a substring of +// the SQL statement; the first registered substring that matches a given +// SQL is consumed. Exec responses are similarly keyed by substring; every +// Exec call is appended to execCalls for assertion. +type fakeDriver struct { + queryRules []queryRule + queryRowRules []queryRowRule + execRules []execRule + execCalls []execCall +} + +type queryRule struct { + match string + response func() (pgx.Rows, error) +} + +type queryRowRule struct { + match string + response func() pgx.Row +} + +type execRule struct { + match string + response func() (pgconn.CommandTag, error) +} + +type execCall struct { + sql string + args []any +} + +func (s *fakeDriver) Exec(_ context.Context, sql string, args ...any) (pgconn.CommandTag, error) { + s.execCalls = append(s.execCalls, execCall{sql: sql, args: args}) + for i := range s.execRules { + if strings.Contains(sql, s.execRules[i].match) { + return s.execRules[i].response() + } + } + return pgconn.CommandTag{}, nil +} + +func (s *fakeDriver) Query(_ context.Context, sql string, _ ...any) (pgx.Rows, error) { + for i, r := range s.queryRules { + if strings.Contains(sql, r.match) { + s.queryRules = append(s.queryRules[:i], s.queryRules[i+1:]...) + return r.response() + } + } + return &fakeRows{}, nil +} + +func (s *fakeDriver) QueryRow(_ context.Context, sql string, _ ...any) pgx.Row { + for i, r := range s.queryRowRules { + if strings.Contains(sql, r.match) { + s.queryRowRules = append(s.queryRowRules[:i], s.queryRowRules[i+1:]...) + return r.response() + } + } + return &fakeRow{err: pgx.ErrNoRows} +} + +// fakeRows implements pgx.Rows over a slice of value tuples. Each tuple is +// a []any whose element types must match the pointer destinations passed to +// Scan (assignment is performed via reflect). closeErr is returned by Err() +// after the cursor is exhausted, simulating a partial-read failure. +type fakeRows struct { + values [][]any + pos int + scanErr error + closeErr error + closed bool +} + +func (s *fakeRows) Close() { s.closed = true } +func (s *fakeRows) Err() error { return s.closeErr } +func (s *fakeRows) CommandTag() pgconn.CommandTag { return pgconn.CommandTag{} } +func (s *fakeRows) FieldDescriptions() []pgconn.FieldDescription { return nil } +func (s *fakeRows) Values() ([]any, error) { return nil, nil } +func (s *fakeRows) RawValues() [][]byte { return nil } +func (s *fakeRows) Conn() *pgx.Conn { return nil } + +func (s *fakeRows) Next() bool { + if s.pos >= len(s.values) { + return false + } + s.pos++ + return true +} + +func (s *fakeRows) Scan(dest ...any) error { + if s.scanErr != nil { + return s.scanErr + } + row := s.values[s.pos-1] + return assignRow(row, dest) +} + +// fakeRow implements pgx.Row for a single QueryRow response. +type fakeRow struct { + values []any + err error +} + +func (s *fakeRow) Scan(dest ...any) error { + if s.err != nil { + return s.err + } + return assignRow(s.values, dest) +} + +// assignRow copies row[i] into the value pointed to by dest[i] using reflect, +// bridging the typed pointer destinations passed to Scan with the loosely +// typed []any values registered by the test. A length mismatch or assignment +// error returns a descriptive error to make scan misuse obvious in tests. +func assignRow(row []any, dest []any) error { + if len(row) != len(dest) { + return &scanMismatchError{want: len(row), got: len(dest)} + } + for i, v := range row { + dv := reflect.ValueOf(dest[i]) + if dv.Kind() != reflect.Ptr { + return &scanMismatchError{want: len(row), got: len(dest)} + } + dv.Elem().Set(reflect.ValueOf(v)) + } + return nil +} + +type scanMismatchError struct{ want, got int } + +func (s *scanMismatchError) Error() string { + return "fake scan mismatch" +} diff --git a/drivers/pg/optimize_phases_test.go b/drivers/pg/optimize_phases_test.go new file mode 100644 index 0000000..95a954b --- /dev/null +++ b/drivers/pg/optimize_phases_test.go @@ -0,0 +1,334 @@ +package pg + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestFlushGinPendingLists exercises the GIN flush phase wiring: scan error, +// per-index measure error, threshold filtering, and exec error continuation. +func TestFlushGinPendingLists(t *testing.T) { + t.Run("list error short-circuits", func(t *testing.T) { + db := &fakeDriver{queryRules: []queryRule{{ + match: "a.amname = 'gin'", + response: func() (pgx.Rows, error) { return nil, errSentinel }, + }}} + flushGinPendingLists(context.Background(), db) + assert.Empty(t, db.execCalls) + }) + + t.Run("measure error skips that index but flush continues", func(t *testing.T) { + db := &fakeDriver{ + queryRules: []queryRule{{ + match: "a.amname = 'gin'", + response: func() (pgx.Rows, error) { + return newFakeRows( + indexRowValues(1, "graph", "node_1", "ng_idx_1", 1024), + indexRowValues(2, "graph", "node_2", "ng_idx_2", 2048), + ), nil + }, + }}, + queryRowRules: []queryRowRule{ + {match: "pgstatginindex", response: func() pgx.Row { return &fakeRow{err: errSentinel} }}, + {match: "pgstatginindex", response: func() pgx.Row { + return &fakeRow{values: []any{int64(ginPendingPagesFlushThreshold), int64(99)}} + }}, + }, + } + flushGinPendingLists(context.Background(), db) + require.Len(t, db.execCalls, 1, "only the second index, which scanned successfully and crossed the threshold, should be flushed") + assert.True(t, strings.Contains(db.execCalls[0].sql, "gin_clean_pending_list")) + }) + + t.Run("only flagged candidates exec; below-threshold ignored", func(t *testing.T) { + db := &fakeDriver{ + queryRules: []queryRule{{ + match: "a.amname = 'gin'", + response: func() (pgx.Rows, error) { + return newFakeRows( + indexRowValues(1, "graph", "node_1", "ng_idx_1", 1024), + indexRowValues(2, "graph", "node_2", "ng_idx_2", 2048), + ), nil + }, + }}, + queryRowRules: []queryRowRule{ + {match: "pgstatginindex", response: func() pgx.Row { + return &fakeRow{values: []any{int64(ginPendingPagesFlushThreshold - 1), int64(0)}} + }}, + {match: "pgstatginindex", response: func() pgx.Row { + return &fakeRow{values: []any{int64(ginPendingPagesFlushThreshold + 100), int64(50)}} + }}, + }, + } + flushGinPendingLists(context.Background(), db) + require.Len(t, db.execCalls, 1) + }) + + t.Run("loop continues past per-candidate exec error", func(t *testing.T) { + var execAttempts int + db := &fakeDriver{ + queryRules: []queryRule{{ + match: "a.amname = 'gin'", + response: func() (pgx.Rows, error) { + return newFakeRows( + indexRowValues(1, "graph", "node_1", "ng_idx_1", 1024), + indexRowValues(2, "graph", "node_2", "ng_idx_2", 2048), + ), nil + }, + }}, + queryRowRules: []queryRowRule{ + {match: "pgstatginindex", response: func() pgx.Row { + return &fakeRow{values: []any{int64(ginPendingPagesFlushThreshold), int64(1)}} + }}, + {match: "pgstatginindex", response: func() pgx.Row { + return &fakeRow{values: []any{int64(ginPendingPagesFlushThreshold), int64(1)}} + }}, + }, + execRules: []execRule{ + {match: "gin_clean_pending_list", response: func() (pgconn.CommandTag, error) { execAttempts++; return pgconn.CommandTag{}, errSentinel }}, + {match: "gin_clean_pending_list", response: func() (pgconn.CommandTag, error) { execAttempts++; return pgconn.CommandTag{}, nil }}, + }, + } + flushGinPendingLists(context.Background(), db) + assert.Equal(t, 2, execAttempts) + }) + + t.Run("ctx cancellation between candidates aborts remaining flushes", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + db := &fakeDriver{ + queryRules: []queryRule{{ + match: "a.amname = 'gin'", + response: func() (pgx.Rows, error) { + return newFakeRows( + indexRowValues(1, "graph", "node_1", "ng_idx_1", 1024), + indexRowValues(2, "graph", "node_2", "ng_idx_2", 2048), + ), nil + }, + }}, + queryRowRules: []queryRowRule{ + {match: "pgstatginindex", response: func() pgx.Row { + return &fakeRow{values: []any{int64(ginPendingPagesFlushThreshold), int64(0)}} + }}, + {match: "pgstatginindex", response: func() pgx.Row { + return &fakeRow{values: []any{int64(ginPendingPagesFlushThreshold), int64(0)}} + }}, + }, + } + flushGinPendingLists(ctx, db) + assert.Empty(t, db.execCalls, "cancelled ctx must prevent any flush exec") + }) +} + +// vacuumStatsValues mirrors the column order of sqlSelectGraphPartitionVacuumStats +// so the wiring tests can register stats rows compactly. +func vacuumStatsValues(schema, table string, live, dead, mod int64, lastAnalyzed *time.Time) []any { + return []any{schema, table, live, dead, mod, lastAnalyzed} +} + +// TestVacuumGraphPartitions exercises the vacuum phase wiring: scan error, +// threshold filtering, exec error continuation, and ctx cancellation. +func TestVacuumGraphPartitions(t *testing.T) { + stale := time.Now().Add(-(analyzeStalenessWindow + time.Hour)) + + t.Run("list error short-circuits", func(t *testing.T) { + db := &fakeDriver{queryRules: []queryRule{{ + match: "pg_stat_user_tables", + response: func() (pgx.Rows, error) { return nil, errSentinel }, + }}} + vacuumGraphPartitions(context.Background(), db) + assert.Empty(t, db.execCalls) + }) + + t.Run("only flagged partitions exec; healthy partitions ignored", func(t *testing.T) { + db := &fakeDriver{queryRules: []queryRule{{ + match: "pg_stat_user_tables", + response: func() (pgx.Rows, error) { + return newFakeRows( + vacuumStatsValues("graph", "node_1", 1_000_000, 5_000, 1_000, &stale), + vacuumStatsValues("graph", "edge_1", 40_000, vacuumDeadTupleAbsoluteFloor, 0, &stale), + ), nil + }, + }}} + vacuumGraphPartitions(context.Background(), db) + require.Len(t, db.execCalls, 1) + assert.True(t, strings.Contains(db.execCalls[0].sql, "vacuum (analyze)")) + assert.True(t, strings.Contains(db.execCalls[0].sql, `"edge_1"`)) + }) + + t.Run("loop continues past per-partition exec error", func(t *testing.T) { + var execAttempts int + db := &fakeDriver{ + queryRules: []queryRule{{ + match: "pg_stat_user_tables", + response: func() (pgx.Rows, error) { + return newFakeRows( + vacuumStatsValues("graph", "node_1", 40_000, vacuumDeadTupleAbsoluteFloor, 0, &stale), + vacuumStatsValues("graph", "edge_1", 40_000, vacuumDeadTupleAbsoluteFloor, 0, &stale), + ), nil + }, + }}, + execRules: []execRule{ + {match: "vacuum (analyze)", response: func() (pgconn.CommandTag, error) { execAttempts++; return pgconn.CommandTag{}, errSentinel }}, + {match: "vacuum (analyze)", response: func() (pgconn.CommandTag, error) { execAttempts++; return pgconn.CommandTag{}, nil }}, + }, + } + vacuumGraphPartitions(context.Background(), db) + assert.Equal(t, 2, execAttempts) + }) + + t.Run("ctx cancellation between partitions aborts remaining vacuums", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + db := &fakeDriver{queryRules: []queryRule{{ + match: "pg_stat_user_tables", + response: func() (pgx.Rows, error) { + return newFakeRows( + vacuumStatsValues("graph", "node_1", 40_000, vacuumDeadTupleAbsoluteFloor, 0, &stale), + vacuumStatsValues("graph", "edge_1", 40_000, vacuumDeadTupleAbsoluteFloor, 0, &stale), + ), nil + }, + }}} + vacuumGraphPartitions(ctx, db) + assert.Empty(t, db.execCalls) + }) +} + +// TestReindexBloatedBtreeIndexes exercises the btree reindex phase wiring: +// scan error, per-index measure error, threshold filtering, smallest-first +// ordering, exec error continuation, and ctx cancellation. +func TestReindexBloatedBtreeIndexes(t *testing.T) { + bloated := func() pgx.Row { return &fakeRow{values: []any{30.0, 0.0}} } + healthy := func() pgx.Row { return &fakeRow{values: []any{85.0, 5.0}} } + + t.Run("list error short-circuits", func(t *testing.T) { + db := &fakeDriver{queryRules: []queryRule{{ + match: "a.amname = 'btree'", + response: func() (pgx.Rows, error) { return nil, errSentinel }, + }}} + reindexBloatedBtreeIndexes(context.Background(), db) + assert.Empty(t, db.execCalls) + }) + + t.Run("measure error skips that index but reindex continues", func(t *testing.T) { + db := &fakeDriver{ + queryRules: []queryRule{{ + match: "a.amname = 'btree'", + response: func() (pgx.Rows, error) { + return newFakeRows( + indexRowValues(1, "graph", "node_1", "node_1_pkey", 1024), + indexRowValues(2, "graph", "node_2", "node_2_pkey", 2048), + ), nil + }, + }}, + queryRowRules: []queryRowRule{ + {match: "pgstatindex", response: func() pgx.Row { return &fakeRow{err: errSentinel} }}, + {match: "pgstatindex", response: bloated}, + }, + } + reindexBloatedBtreeIndexes(context.Background(), db) + require.Len(t, db.execCalls, 1) + assert.True(t, strings.Contains(db.execCalls[0].sql, "reindex index concurrently")) + }) + + t.Run("only flagged candidates exec; healthy indexes ignored", func(t *testing.T) { + db := &fakeDriver{ + queryRules: []queryRule{{ + match: "a.amname = 'btree'", + response: func() (pgx.Rows, error) { + return newFakeRows( + indexRowValues(1, "graph", "node_1", "node_1_pkey", 1024), + indexRowValues(2, "graph", "node_2", "node_2_pkey", 2048), + ), nil + }, + }}, + queryRowRules: []queryRowRule{ + {match: "pgstatindex", response: healthy}, + {match: "pgstatindex", response: bloated}, + }, + } + reindexBloatedBtreeIndexes(context.Background(), db) + require.Len(t, db.execCalls, 1) + assert.True(t, strings.Contains(db.execCalls[0].sql, `"node_2_pkey"`)) + }) + + // candidates are processed smallest-first so cancellation mid-pass leaves + // the maximum number of rebuilds completed; the larger size_bytes index + // must trail the smaller one in the recorded exec order. + t.Run("flagged candidates execute smallest-first", func(t *testing.T) { + db := &fakeDriver{ + queryRules: []queryRule{{ + match: "a.amname = 'btree'", + response: func() (pgx.Rows, error) { + return newFakeRows( + indexRowValues(1, "graph", "node_1", "big_idx", 9999), + indexRowValues(2, "graph", "node_2", "small_idx", 100), + ), nil + }, + }}, + queryRowRules: []queryRowRule{ + {match: "pgstatindex", response: bloated}, + {match: "pgstatindex", response: bloated}, + }, + } + reindexBloatedBtreeIndexes(context.Background(), db) + require.Len(t, db.execCalls, 2) + assert.True(t, strings.Contains(db.execCalls[0].sql, `"small_idx"`), + "smaller index must be reindexed first; got %q", db.execCalls[0].sql) + assert.True(t, strings.Contains(db.execCalls[1].sql, `"big_idx"`)) + }) + + t.Run("loop continues past per-candidate exec error", func(t *testing.T) { + var execAttempts int + db := &fakeDriver{ + queryRules: []queryRule{{ + match: "a.amname = 'btree'", + response: func() (pgx.Rows, error) { + return newFakeRows( + indexRowValues(1, "graph", "node_1", "node_1_pkey", 1024), + indexRowValues(2, "graph", "node_2", "node_2_pkey", 2048), + ), nil + }, + }}, + queryRowRules: []queryRowRule{ + {match: "pgstatindex", response: bloated}, + {match: "pgstatindex", response: bloated}, + }, + execRules: []execRule{ + {match: "reindex index concurrently", response: func() (pgconn.CommandTag, error) { execAttempts++; return pgconn.CommandTag{}, errSentinel }}, + {match: "reindex index concurrently", response: func() (pgconn.CommandTag, error) { execAttempts++; return pgconn.CommandTag{}, nil }}, + }, + } + reindexBloatedBtreeIndexes(context.Background(), db) + assert.Equal(t, 2, execAttempts) + }) + + t.Run("ctx cancellation between candidates aborts remaining reindexes", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + db := &fakeDriver{ + queryRules: []queryRule{{ + match: "a.amname = 'btree'", + response: func() (pgx.Rows, error) { + return newFakeRows( + indexRowValues(1, "graph", "node_1", "node_1_pkey", 1024), + indexRowValues(2, "graph", "node_2", "node_2_pkey", 2048), + ), nil + }, + }}, + queryRowRules: []queryRowRule{ + {match: "pgstatindex", response: bloated}, + {match: "pgstatindex", response: bloated}, + }, + } + reindexBloatedBtreeIndexes(ctx, db) + assert.Empty(t, db.execCalls, "cancelled ctx must prevent any reindex exec") + }) +} diff --git a/drivers/pg/optimize_wiring_test.go b/drivers/pg/optimize_wiring_test.go new file mode 100644 index 0000000..6b44170 --- /dev/null +++ b/drivers/pg/optimize_wiring_test.go @@ -0,0 +1,150 @@ +package pg + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// errSentinel is a fixed error used by wiring tests to assert that a real +// error value (not a wrapped variant) propagates out of the listing / +// measuring / executing call sites. +var errSentinel = errors.New("sentinel") + +func newFakeRows(values ...[]any) *fakeRows { return &fakeRows{values: values} } + +// TestOptimize_PgstattupleNotInstalled_NoOp covers the early-exit path: when +// the pre-flight extension probe returns false, optimize must not invoke any +// of the four maintenance phases. +func TestOptimize_PgstattupleNotInstalled_NoOp(t *testing.T) { + db := &fakeDriver{ + queryRowRules: []queryRowRule{{ + match: "pg_extension", + response: func() pgx.Row { return &fakeRow{values: []any{false}} }, + }}, + } + require.NoError(t, optimize(context.Background(), db)) + assert.Empty(t, db.execCalls, "no exec should run when pgstattuple is missing") +} + +// TestOptimize_PgstattupleCheckError_ReturnsError covers the only path that +// makes optimize return a non-nil error: the pre-flight probe itself failing. +func TestOptimize_PgstattupleCheckError_ReturnsError(t *testing.T) { + db := &fakeDriver{ + queryRowRules: []queryRowRule{{ + match: "pg_extension", + response: func() pgx.Row { return &fakeRow{err: errSentinel} }, + }}, + } + err := optimize(context.Background(), db) + require.Error(t, err) + assert.ErrorIs(t, err, errSentinel) +} + +// TestOptimize_AllPhasesRunWhenInstalled covers the happy path of the +// dispatcher: when pgstattuple is installed each of the four list queries +// must be issued. Phase bodies are exercised by the per-phase tests below. +func TestOptimize_AllPhasesRunWhenInstalled(t *testing.T) { + var queriedFragments []string + listResponder := func(fragment string) func() (pgx.Rows, error) { + return func() (pgx.Rows, error) { + queriedFragments = append(queriedFragments, fragment) + return newFakeRows(), nil + } + } + db := &fakeDriver{ + queryRowRules: []queryRowRule{{ + match: "pg_extension", + response: func() pgx.Row { return &fakeRow{values: []any{true}} }, + }}, + queryRules: []queryRule{ + {match: "_ccnew", response: listResponder("orphans")}, + {match: "a.amname = 'gin'", response: listResponder("gin")}, + {match: "pg_stat_user_tables", response: listResponder("vacuum")}, + {match: "a.amname = 'btree'", response: listResponder("btree")}, + }, + } + require.NoError(t, optimize(context.Background(), db)) + assert.Equal(t, []string{"orphans", "gin", "vacuum", "btree"}, queriedFragments, + "all four phases must dispatch their list query in the documented order") +} + +// TestCleanupOrphanedReindexArtifacts covers the orphan-cleanup phase wiring: +// list error short-circuits, empty list logs and exits, listed orphans each +// receive a DROP, and per-orphan exec failures do not abort the loop. +func TestCleanupOrphanedReindexArtifacts(t *testing.T) { + t.Run("list error returns without exec", func(t *testing.T) { + db := &fakeDriver{queryRules: []queryRule{{ + match: "_ccnew", + response: func() (pgx.Rows, error) { return nil, errSentinel }, + }}} + cleanupOrphanedReindexArtifacts(context.Background(), db) + assert.Empty(t, db.execCalls) + }) + + t.Run("empty list issues no exec", func(t *testing.T) { + db := &fakeDriver{queryRules: []queryRule{{ + match: "_ccnew", + response: func() (pgx.Rows, error) { return newFakeRows(), nil }, + }}} + cleanupOrphanedReindexArtifacts(context.Background(), db) + assert.Empty(t, db.execCalls) + }) + + t.Run("each orphan dropped with sanitized identifier", func(t *testing.T) { + db := &fakeDriver{queryRules: []queryRule{{ + match: "_ccnew", + response: func() (pgx.Rows, error) { + return newFakeRows( + []any{"graph", "edge_1_pkey_ccnew"}, + []any{"graph", "node_2_pkey_ccnew1"}, + ), nil + }, + }}} + cleanupOrphanedReindexArtifacts(context.Background(), db) + require.Len(t, db.execCalls, 2) + for _, call := range db.execCalls { + assert.True(t, strings.HasPrefix(call.sql, "drop index concurrently if exists "), + "orphan cleanup must use DROP INDEX CONCURRENTLY IF EXISTS, got %q", call.sql) + } + }) + + t.Run("loop continues past per-orphan exec error", func(t *testing.T) { + var execAttempts int + db := &fakeDriver{ + queryRules: []queryRule{{ + match: "_ccnew", + response: func() (pgx.Rows, error) { + return newFakeRows( + []any{"graph", "first_ccnew"}, + []any{"graph", "second_ccnew"}, + ), nil + }, + }}, + execRules: []execRule{{ + match: "first_ccnew", + response: func() (pgconn.CommandTag, error) { return pgconn.CommandTag{}, errSentinel }, + }}, + } + cleanupOrphanedReindexArtifacts(context.Background(), db) + for _, call := range db.execCalls { + if strings.Contains(call.sql, "_ccnew") { + execAttempts++ + } + } + assert.Equal(t, 2, execAttempts, "second orphan must be attempted after first fails") + }) +} + +// indexRowValues mirrors the column order of sqlSelectGraphPartitionBtreeIndexes +// and sqlSelectGraphPartitionGinIndexes so the wiring tests below can register +// listing rows compactly. +func indexRowValues(oid uint32, schema, table, index string, sizeBytes int64) []any { + return []any{oid, schema, table, index, sizeBytes} +}