diff --git a/handler.go b/handler.go index b686cbbb9..1730867df 100644 --- a/handler.go +++ b/handler.go @@ -106,6 +106,13 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } + case "/db": + switch r.Method { + case "DELETE": + h.handleDeleteDB(w, r) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } case "/db/attr/diff": switch r.Method { case "POST": @@ -264,6 +271,33 @@ type sliceMaxResponse struct { SliceMax uint64 `json:"SliceMax"` } +// handleDeleteDB handles DELETE /db request. +func (h *Handler) handleDeleteDB(w http.ResponseWriter, r *http.Request) { + // Decode request. + var req deleteDBRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // Delete database from the index. + if err := h.Index.DeleteDB(req.DB); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Encode response. + if err := json.NewEncoder(w).Encode(deleteDBResponse{}); err != nil { + h.logger().Printf("response encoding error: %s", err) + } +} + +type deleteDBRequest struct { + DB string `json:"db"` +} + +type deleteDBResponse struct{} + // handlePostDBAttrDiff handles POST /db/attr/diff requests. func (h *Handler) handlePostDBAttrDiff(w http.ResponseWriter, r *http.Request) { // Decode request. diff --git a/handler_test.go b/handler_test.go index 672f51e02..a7c69d539 100644 --- a/handler_test.go +++ b/handler_test.go @@ -416,6 +416,42 @@ func TestHandler_Query_ErrParse(t *testing.T) { } } +// Ensure the handler can delete a database. +func TestHandler_DB_Delete(t *testing.T) { + idx := MustOpenIndex() + defer idx.Close() + + s := NewServer() + s.Handler.Index = idx.Index + defer s.Close() + + // Create database. + if _, err := idx.CreateDBIfNotExists("d"); err != nil { + t.Fatal(err) + } + + // Send request to delete database. + resp, err := http.DefaultClient.Do(MustNewHTTPRequest("DELETE", s.URL+"/db", strings.NewReader(`{"db":"d"}`))) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + // Verify body response. + if resp.StatusCode != http.StatusOK { + t.Fatalf("unexpected status: %d", resp.StatusCode) + } else if buf, err := ioutil.ReadAll(resp.Body); err != nil { + t.Fatal(err) + } else if string(buf) != "{}\n" { + t.Fatalf("unexpected response body: %s", buf) + } + + // Verify database is gone. + if idx.DB("d") != nil { + t.Fatal("expected nil database") + } +} + // Ensure the handler can return data in differing blocks for a database. func TestHandler_DB_AttrStore_Diff(t *testing.T) { idx := MustOpenIndex() diff --git a/index.go b/index.go index d05a507bd..e104763ba 100644 --- a/index.go +++ b/index.go @@ -183,6 +183,33 @@ func (i *Index) createDBIfNotExists(name string) (*DB, error) { return db, nil } +// DeleteDB removes a database from the index. +func (i *Index) DeleteDB(name string) error { + i.mu.Lock() + defer i.mu.Unlock() + + // Ignore if database doesn't exist. + db := i.db(name) + if db == nil { + return nil + } + + // Close database. + if err := db.Close(); err != nil { + return err + } + + // Delete database directory. + if err := os.RemoveAll(i.DBPath(name)); err != nil { + return err + } + + // Remove reference. + delete(i.dbs, name) + + return nil +} + // Frame returns the frame for a database and name. func (i *Index) Frame(db, name string) *Frame { d := i.DB(db) diff --git a/index_test.go b/index_test.go index 19859af93..05d680fbd 100644 --- a/index_test.go +++ b/index_test.go @@ -11,6 +11,39 @@ import ( "github.com/umbel/pilosa/pql" ) +// Ensure index can delete a database and its underlying files. +func TestIndex_DeleteDB(t *testing.T) { + idx := MustOpenIndex() + defer idx.Close() + + // Write bits to separate databases. + f0 := idx.MustCreateFragmentIfNotExists("d0", "f", 0) + if _, err := f0.SetBit(100, 200, nil, 0); err != nil { + t.Fatal(err) + } + f1 := idx.MustCreateFragmentIfNotExists("d1", "f", 0) + if _, err := f1.SetBit(100, 200, nil, 0); err != nil { + t.Fatal(err) + } + + // Ensure d0 exists. + if _, err := os.Stat(idx.DBPath("d0")); err != nil { + t.Fatal(err) + } + + // Delete d0. + if err := idx.DeleteDB("d0"); err != nil { + t.Fatal(err) + } + + // Ensure d0 files are removed & d1 still exists. + if _, err := os.Stat(idx.DBPath("d0")); !os.IsNotExist(err) { + t.Fatal("expected d0 file deletion") + } else if _, err := os.Stat(idx.DBPath("d1")); err != nil { + t.Fatal("expected d1 files to still exist", err) + } +} + // Ensure index can sync with a remote index. func TestIndexSyncer_SyncIndex(t *testing.T) { cluster := NewCluster(2)