feat(14-01): beachcomber DropIndex and EnsureIndex for reindex tooling
- optional IndexDropper reports whether a dropped index existed; DropIndex falls back to Flush - optional IndexEnsurer creates an empty index from its schema; EnsureIndex is a no-op otherwise - typesense implements both (404 is already absent; ensure reuses collection creation)
This commit is contained in:
@@ -47,6 +47,10 @@ type Engine struct {
|
||||
}
|
||||
|
||||
var _ beachcomber.Engine = (*Engine)(nil)
|
||||
var (
|
||||
_ beachcomber.IndexDropper = (*Engine)(nil)
|
||||
_ beachcomber.IndexEnsurer = (*Engine)(nil)
|
||||
)
|
||||
|
||||
// New returns an engine for cfg. Every request is bounded by
|
||||
// cfg.ConnectionTimeout.
|
||||
@@ -214,6 +218,33 @@ func (e *Engine) Flush(ctx context.Context, index string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DropIndex drops the collection with DELETE /collections/{index} and
|
||||
// reports whether it existed: 2xx is true, 404 is false, any other status is
|
||||
// a StatusError. It implements beachcomber.IndexDropper.
|
||||
func (e *Engine) DropIndex(ctx context.Context, index string) (bool, error) {
|
||||
path := "/collections/" + url.PathEscape(index)
|
||||
code, _, err := e.do(ctx, http.MethodDelete, path, nil, nil, "")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
switch {
|
||||
case ok2xx(code):
|
||||
return true, nil
|
||||
case code == http.StatusNotFound:
|
||||
return false, nil
|
||||
default:
|
||||
return false, statusError(http.MethodDelete, path, code)
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureIndex creates the collection from schema when Typesense does not
|
||||
// have it (GET /collections/{index}, then POST /collections on 404, 409 as
|
||||
// success), the same step Upsert runs. It implements
|
||||
// beachcomber.IndexEnsurer.
|
||||
func (e *Engine) EnsureIndex(ctx context.Context, index string, schema map[string]any) error {
|
||||
return e.ensureCollection(ctx, index, schema)
|
||||
}
|
||||
|
||||
// searchAnswer is the part of a search answer SearchIDs and SearchPage read.
|
||||
type searchAnswer struct {
|
||||
Found int `json:"found"`
|
||||
|
||||
@@ -367,3 +367,66 @@ func TestSyncEngineRegistration(t *testing.T) {
|
||||
t.Fatal("a blank API key counts as configured")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineDropIndex(t *testing.T) {
|
||||
f, e := newFake(t)
|
||||
f.answer("DELETE /ts/collections/albums", http.StatusOK, `{"name":"albums"}`)
|
||||
existed, err := e.DropIndex(t.Context(), "albums")
|
||||
if err != nil || !existed {
|
||||
t.Fatalf("200: existed=%v err=%v", existed, err)
|
||||
}
|
||||
f.answer("DELETE /ts/collections/albums", http.StatusNotFound, `{"message":"Not Found"}`)
|
||||
existed, err = e.DropIndex(t.Context(), "albums")
|
||||
if err != nil || existed {
|
||||
t.Fatalf("404: existed=%v err=%v, want already absent", existed, err)
|
||||
}
|
||||
f.answer("DELETE /ts/collections/albums", http.StatusInternalServerError, `{"message":"boom"}`)
|
||||
if _, err := e.DropIndex(t.Context(), "albums"); statusOf(err) != http.StatusInternalServerError {
|
||||
t.Fatalf("500: err=%v", err)
|
||||
}
|
||||
calls := f.take()
|
||||
if len(calls) != 3 {
|
||||
t.Fatalf("calls = %+v", calls)
|
||||
}
|
||||
for _, c := range calls {
|
||||
if c.method != http.MethodDelete || c.path != "/ts/collections/albums" || c.key != testKey {
|
||||
t.Fatalf("call = %+v", c)
|
||||
}
|
||||
}
|
||||
existed, err = beachcomber.DropIndex(t.Context(), e, "a b")
|
||||
if err != nil || existed {
|
||||
t.Fatalf("through beachcomber.DropIndex: existed=%v err=%v", existed, err)
|
||||
}
|
||||
if got := f.take(); len(got) != 1 || got[0].path != "/ts/collections/a%20b" {
|
||||
t.Fatalf("escaped path calls = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineEnsureIndex(t *testing.T) {
|
||||
f, e := newFake(t)
|
||||
f.answer("POST /ts/collections", http.StatusCreated, `{"name":"albums"}`)
|
||||
schema := map[string]any{"fields": []map[string]any{{"name": "title", "type": "string"}}}
|
||||
if err := beachcomber.EnsureIndex(t.Context(), e, "albums", schema); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
calls := f.take()
|
||||
if len(calls) != 2 || calls[0].method != http.MethodGet || calls[1].method != http.MethodPost || calls[1].path != "/ts/collections" {
|
||||
t.Fatalf("first ensure calls = %+v", calls)
|
||||
}
|
||||
var created map[string]any
|
||||
if err := json.Unmarshal([]byte(calls[1].body), &created); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if created["name"] != "albums" || created["fields"] == nil {
|
||||
t.Fatalf("create body = %s", calls[1].body)
|
||||
}
|
||||
|
||||
f.answer("GET /ts/collections/albums", http.StatusOK, `{"name":"albums"}`)
|
||||
if err := e.EnsureIndex(t.Context(), "albums", schema); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
calls = f.take()
|
||||
if len(calls) != 1 || calls[0].method != http.MethodGet {
|
||||
t.Fatalf("second ensure must send no create: %+v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user