Skip to content

Commit 3b1668b

Browse files
StephenHinckzinic
authored andcommitted
Pull optimizer through to DatabaseSwitch
1 parent da4bfef commit 3b1668b

3 files changed

Lines changed: 121 additions & 0 deletions

File tree

graph/optimizer.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ import "context"
1717
//
1818
// Drivers that do not have a meaningful notion of optimization should simply
1919
// not implement this interface; consumers must type-assert before calling.
20+
//
21+
// Note that *DatabaseSwitch implements Optimizer as a transparent delegate:
22+
// its Optimize method forwards to the currently active driver when that
23+
// driver implements Optimizer, and is a no-op otherwise. As a consequence,
24+
// a successful type assertion to Optimizer against a *DatabaseSwitch does
25+
// not by itself imply that the underlying driver supports optimization.
2026
type Optimizer interface {
2127
Optimize(ctx context.Context) error
2228
}

graph/switch.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,21 @@ func (s *DatabaseSwitch) retireInternalContext(ctx context.Context) {
115115
}
116116
}
117117

118+
// Optimize satisfies the optional Optimizer capability by delegating to the
119+
// currently active driver when it implements Optimizer; otherwise it returns
120+
// nil. This keeps the wrapper transparent so callers can use the standard
121+
// Optimizer type-assertion against a *DatabaseSwitch without having to peek
122+
// through it manually.
123+
func (s *DatabaseSwitch) Optimize(ctx context.Context) error {
124+
s.currentDBLock.RLock()
125+
defer s.currentDBLock.RUnlock()
126+
127+
if optimizer, ok := s.currentDB.(Optimizer); ok {
128+
return optimizer.Optimize(ctx)
129+
}
130+
return nil
131+
}
132+
118133
func (s *DatabaseSwitch) ReadTransaction(ctx context.Context, txDelegate TransactionDelegate, options ...TransactionOption) error {
119134
if internalCtx, err := s.newInternalContext(ctx); err != nil {
120135
return err

graph/switch_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package graph_test
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
"github.com/specterops/dawgs/graph"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// stubDatabase is a no-op Database implementation suitable for exercising
14+
// behavior on *graph.DatabaseSwitch that does not depend on real driver
15+
// semantics. Methods return zero values; tests that need richer behavior
16+
// should embed this and override only what they need.
17+
type stubDatabase struct{}
18+
19+
func (stubDatabase) SetWriteFlushSize(int) {}
20+
func (stubDatabase) SetBatchWriteSize(int) {}
21+
func (stubDatabase) ReadTransaction(context.Context, graph.TransactionDelegate, ...graph.TransactionOption) error {
22+
return nil
23+
}
24+
func (stubDatabase) WriteTransaction(context.Context, graph.TransactionDelegate, ...graph.TransactionOption) error {
25+
return nil
26+
}
27+
func (stubDatabase) BatchOperation(context.Context, graph.BatchDelegate, ...graph.BatchOption) error {
28+
return nil
29+
}
30+
func (stubDatabase) AssertSchema(context.Context, graph.Schema) error { return nil }
31+
func (stubDatabase) SetDefaultGraph(context.Context, graph.Graph) error { return nil }
32+
func (stubDatabase) Run(context.Context, string, map[string]any) error { return nil }
33+
func (stubDatabase) Close(context.Context) error { return nil }
34+
func (stubDatabase) FetchKinds(context.Context) (graph.Kinds, error) { return nil, nil }
35+
func (stubDatabase) RefreshKinds(context.Context) error { return nil }
36+
37+
// optimizingStubDatabase is a stubDatabase that additionally satisfies
38+
// graph.Optimizer. Each call to Optimize increments calls and returns err.
39+
type optimizingStubDatabase struct {
40+
stubDatabase
41+
calls int
42+
err error
43+
}
44+
45+
func (s *optimizingStubDatabase) Optimize(context.Context) error {
46+
s.calls++
47+
return s.err
48+
}
49+
50+
// TestDatabaseSwitch_Optimize_DelegatesWhenUnderlyingImplementsOptimizer
51+
// verifies that *graph.DatabaseSwitch forwards Optimize to the active driver
52+
// when that driver implements graph.Optimizer, and propagates its return.
53+
func TestDatabaseSwitch_Optimize_DelegatesWhenUnderlyingImplementsOptimizer(t *testing.T) {
54+
ctx := context.Background()
55+
56+
driver := &optimizingStubDatabase{}
57+
dbSwitch := graph.NewDatabaseSwitch(ctx, driver)
58+
59+
require.NoError(t, dbSwitch.Optimize(ctx))
60+
assert.Equal(t, 1, driver.calls, "Optimize should be invoked exactly once on the underlying driver")
61+
62+
driver.err = errors.New("optimizer reported failure")
63+
err := dbSwitch.Optimize(ctx)
64+
assert.ErrorIs(t, err, driver.err, "DatabaseSwitch must propagate the underlying optimizer error")
65+
assert.Equal(t, 2, driver.calls)
66+
}
67+
68+
// TestDatabaseSwitch_Optimize_NoOpWhenUnderlyingDoesNotImplementOptimizer
69+
// verifies that the wrapper returns nil without panicking when the active
70+
// driver lacks an Optimize method.
71+
func TestDatabaseSwitch_Optimize_NoOpWhenUnderlyingDoesNotImplementOptimizer(t *testing.T) {
72+
ctx := context.Background()
73+
dbSwitch := graph.NewDatabaseSwitch(ctx, stubDatabase{})
74+
75+
require.NoError(t, dbSwitch.Optimize(ctx))
76+
}
77+
78+
// TestDatabaseSwitch_Optimize_FollowsActiveDriverAfterSwitch verifies that
79+
// after Switch reassigns the active driver, Optimize routes to the new one.
80+
func TestDatabaseSwitch_Optimize_FollowsActiveDriverAfterSwitch(t *testing.T) {
81+
ctx := context.Background()
82+
83+
first := &optimizingStubDatabase{}
84+
second := &optimizingStubDatabase{}
85+
86+
dbSwitch := graph.NewDatabaseSwitch(ctx, first)
87+
require.NoError(t, dbSwitch.Optimize(ctx))
88+
assert.Equal(t, 1, first.calls)
89+
assert.Equal(t, 0, second.calls)
90+
91+
dbSwitch.Switch(second)
92+
require.NoError(t, dbSwitch.Optimize(ctx))
93+
assert.Equal(t, 1, first.calls, "first driver should not be invoked after Switch")
94+
assert.Equal(t, 1, second.calls, "Optimize should be routed to the new active driver")
95+
}
96+
97+
// Compile-time assertion that *graph.DatabaseSwitch satisfies graph.Optimizer.
98+
// This keeps the wrapper's optional-capability contract enforced by the type
99+
// system rather than relying on test discovery alone.
100+
var _ graph.Optimizer = (*graph.DatabaseSwitch)(nil)

0 commit comments

Comments
 (0)