feat: vacuum records manually (#420)

This commit is contained in:
boojack 2022-11-06 12:21:58 +08:00 committed by GitHub
parent 4f10c12092
commit dc5d705f8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 295 additions and 50 deletions

View File

@ -90,5 +90,5 @@ type MemoFind struct {
} }
type MemoDelete struct { type MemoDelete struct {
ID int `json:"id"` ID int
} }

View File

@ -19,3 +19,8 @@ type MemoOrganizerUpsert struct {
UserID int UserID int
Pinned bool `json:"pinned"` Pinned bool `json:"pinned"`
} }
type MemoOrganizerDelete struct {
MemoID *int
UserID *int
}

View File

@ -19,6 +19,6 @@ type MemoResourceFind struct {
} }
type MemoResourceDelete struct { type MemoResourceDelete struct {
MemoID int MemoID *int
ResourceID *int ResourceID *int
} }

View File

@ -40,18 +40,16 @@ type ResourceFind struct {
MemoID *int MemoID *int
} }
type ResourceDelete struct {
ID int
// Standard fields
CreatorID int
}
type ResourcePatch struct { type ResourcePatch struct {
ID int ID int
// Standard fields // Standard fields
UpdatedTs *int64 UpdatedTs *int64
// Domain specific fields
Filename *string `json:"filename"` Filename *string `json:"filename"`
} }
type ResourceDelete struct {
ID int
}

View File

@ -46,5 +46,8 @@ type ShortcutFind struct {
} }
type ShortcutDelete struct { type ShortcutDelete struct {
ID int ID *int
// Standard fields
CreatorID *int
} }

View File

@ -156,3 +156,7 @@ type UserSettingFind struct {
Key *UserSettingKey `json:"key"` Key *UserSettingKey `json:"key"`
} }
type UserSettingDelete struct {
UserID int
}

View File

@ -500,7 +500,7 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
} }
memoResourceDelete := &api.MemoResourceDelete{ memoResourceDelete := &api.MemoResourceDelete{
MemoID: memoID, MemoID: &memoID,
ResourceID: &resourceID, ResourceID: &resourceID,
} }
if err := s.Store.DeleteMemoResource(ctx, memoResourceDelete); err != nil { if err := s.Store.DeleteMemoResource(ctx, memoResourceDelete); err != nil {

View File

@ -170,9 +170,19 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.Param("resourceId"))).SetInternal(err)
} }
resource, err := s.Store.FindResource(ctx, &api.ResourceFind{
ID: &resourceID,
CreatorID: &userID,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find resource").SetInternal(err)
}
if resource == nil {
return echo.NewHTTPError(http.StatusNotFound, "Not find resource").SetInternal(err)
}
resourceDelete := &api.ResourceDelete{ resourceDelete := &api.ResourceDelete{
ID: resourceID, ID: resourceID,
CreatorID: userID,
} }
if err := s.Store.DeleteResource(ctx, resourceDelete); err != nil { if err := s.Store.DeleteResource(ctx, resourceDelete); err != nil {
if common.ErrorCode(err) == common.NotFound { if common.ErrorCode(err) == common.NotFound {

View File

@ -128,7 +128,7 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
} }
shortcutDelete := &api.ShortcutDelete{ shortcutDelete := &api.ShortcutDelete{
ID: shortcutID, ID: &shortcutID,
} }
if err := s.Store.DeleteShortcut(ctx, shortcutDelete); err != nil { if err := s.Store.DeleteShortcut(ctx, shortcutDelete); err != nil {
if common.ErrorCode(err) == common.NotFound { if common.ErrorCode(err) == common.NotFound {

View File

@ -221,13 +221,8 @@ func (s *Store) DeleteMemo(ctx context.Context, delete *api.MemoDelete) error {
if err := deleteMemo(ctx, tx, delete); err != nil { if err := deleteMemo(ctx, tx, delete); err != nil {
return FormatError(err) return FormatError(err)
} }
if err := vacuum(ctx, tx); err != nil {
resourceDelete := &api.MemoResourceDelete{ return err
MemoID: delete.ID,
}
if err := deleteMemoResource(ctx, tx, resourceDelete); err != nil {
return FormatError(err)
} }
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
@ -323,7 +318,7 @@ func findMemoRawList(ctx context.Context, tx *sql.Tx, find *api.MemoFind) ([]*me
where, args = append(where, "row_status = ?"), append(args, *v) where, args = append(where, "row_status = ?"), append(args, *v)
} }
if v := find.Pinned; v != nil { if v := find.Pinned; v != nil {
where = append(where, "id in (SELECT memo_id FROM memo_organizer WHERE pinned = 1 AND user_id = memo.creator_id)") where = append(where, "id IN (SELECT memo_id FROM memo_organizer WHERE pinned = 1 AND user_id = memo.creator_id)")
} }
if v := find.ContentSearch; v != nil { if v := find.ContentSearch; v != nil {
where, args = append(where, "content LIKE ?"), append(args, "%"+*v+"%") where, args = append(where, "content LIKE ?"), append(args, "%"+*v+"%")
@ -382,16 +377,36 @@ func findMemoRawList(ctx context.Context, tx *sql.Tx, find *api.MemoFind) ([]*me
} }
func deleteMemo(ctx context.Context, tx *sql.Tx, delete *api.MemoDelete) error { func deleteMemo(ctx context.Context, tx *sql.Tx, delete *api.MemoDelete) error {
result, err := tx.ExecContext(ctx, ` where, args := []string{"id = ?"}, []interface{}{delete.ID}
DELETE FROM memo WHERE id = ?
`, delete.ID) stmt := `DELETE FROM memo WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
if err != nil { if err != nil {
return FormatError(err) return FormatError(err)
} }
rows, _ := result.RowsAffected() rows, _ := result.RowsAffected()
if rows == 0 { if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo ID not found: %d", delete.ID)} return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo not found")}
}
return nil
}
func vacuumMemo(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
memo
WHERE
creator_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
} }
return nil return nil

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"strings"
"github.com/usememos/memos/api" "github.com/usememos/memos/api"
"github.com/usememos/memos/common" "github.com/usememos/memos/common"
@ -65,6 +66,24 @@ func (s *Store) UpsertMemoOrganizer(ctx context.Context, upsert *api.MemoOrganiz
return nil return nil
} }
func (s *Store) DeleteMemoOrganizer(ctx context.Context, delete *api.MemoOrganizerDelete) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := deleteMemoOrganizer(ctx, tx, delete); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
return nil
}
func findMemoOrganizer(ctx context.Context, tx *sql.Tx, find *api.MemoOrganizerFind) (*memoOrganizerRaw, error) { func findMemoOrganizer(ctx context.Context, tx *sql.Tx, find *api.MemoOrganizerFind) (*memoOrganizerRaw, error) {
query := ` query := `
SELECT SELECT
@ -127,3 +146,52 @@ func upsertMemoOrganizer(ctx context.Context, tx *sql.Tx, upsert *api.MemoOrgani
return nil return nil
} }
func deleteMemoOrganizer(ctx context.Context, tx *sql.Tx, delete *api.MemoOrganizerDelete) error {
where, args := []string{}, []interface{}{}
if v := delete.MemoID; v != nil {
where, args = append(where, "memo_id = ?"), append(args, *v)
}
if v := delete.UserID; v != nil {
where, args = append(where, "user_id = ?"), append(args, *v)
}
stmt := `DELETE FROM memo_organizer WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
if err != nil {
return FormatError(err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("memo organizer not found")}
}
return nil
}
func vacuumMemoOrganizer(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
memo_organizer
WHERE
memo_id NOT IN (
SELECT
id
FROM
memo
)
OR user_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
}
return nil
}

View File

@ -188,14 +188,17 @@ func upsertMemoResource(ctx context.Context, tx *sql.Tx, upsert *api.MemoResourc
} }
func deleteMemoResource(ctx context.Context, tx *sql.Tx, delete *api.MemoResourceDelete) error { func deleteMemoResource(ctx context.Context, tx *sql.Tx, delete *api.MemoResourceDelete) error {
where, args := []string{"memo_id = ?"}, []interface{}{delete.MemoID} where, args := []string{}, []interface{}{}
if v := delete.MemoID; v != nil {
where, args = append(where, "memo_id = ?"), append(args, *v)
}
if v := delete.ResourceID; v != nil { if v := delete.ResourceID; v != nil {
where, args = append(where, "resource_id = ?"), append(args, *v) where, args = append(where, "resource_id = ?"), append(args, *v)
} }
result, err := tx.ExecContext(ctx, ` stmt := `DELETE FROM memo_resource WHERE ` + strings.Join(where, " AND ")
DELETE FROM memo_resource WHERE `+strings.Join(where, " AND "), args...) result, err := tx.ExecContext(ctx, stmt, args...)
if err != nil { if err != nil {
return FormatError(err) return FormatError(err)
} }
@ -207,3 +210,28 @@ func deleteMemoResource(ctx context.Context, tx *sql.Tx, delete *api.MemoResourc
return nil return nil
} }
func vacuumMemoResource(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
memo_resource
WHERE
memo_id NOT IN (
SELECT
id
FROM
memo
)
OR resource_id NOT IN (
SELECT
id
FROM
resource
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
}
return nil
}

View File

@ -169,8 +169,10 @@ func (s *Store) DeleteResource(ctx context.Context, delete *api.ResourceDelete)
} }
defer tx.Rollback() defer tx.Rollback()
err = deleteResource(ctx, tx, delete) if err := deleteResource(ctx, tx, delete); err != nil {
if err != nil { return err
}
if err := vacuum(ctx, tx); err != nil {
return err return err
} }
@ -178,11 +180,6 @@ func (s *Store) DeleteResource(ctx context.Context, delete *api.ResourceDelete)
return FormatError(err) return FormatError(err)
} }
// Vacuum sqlite database file size after deleting resource.
if _, err := s.db.Exec("VACUUM"); err != nil {
return err
}
s.cache.DeleteCache(api.ResourceCache, delete.ID) s.cache.DeleteCache(api.ResourceCache, delete.ID)
return nil return nil
@ -340,16 +337,36 @@ func findResourceList(ctx context.Context, tx *sql.Tx, find *api.ResourceFind) (
} }
func deleteResource(ctx context.Context, tx *sql.Tx, delete *api.ResourceDelete) error { func deleteResource(ctx context.Context, tx *sql.Tx, delete *api.ResourceDelete) error {
result, err := tx.ExecContext(ctx, ` where, args := []string{"id = ?"}, []interface{}{delete.ID}
DELETE FROM resource WHERE id = ? AND creator_id = ?
`, delete.ID, delete.CreatorID) stmt := `DELETE FROM resource WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
if err != nil { if err != nil {
return FormatError(err) return FormatError(err)
} }
rows, _ := result.RowsAffected() rows, _ := result.RowsAffected()
if rows == 0 { if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("resource ID not found: %d", delete.ID)} return &common.Error{Code: common.NotFound, Err: fmt.Errorf("resource not found")}
}
return nil
}
func vacuumResource(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
resource
WHERE
creator_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
} }
return nil return nil

View File

@ -164,7 +164,7 @@ func (s *Store) DeleteShortcut(ctx context.Context, delete *api.ShortcutDelete)
return FormatError(err) return FormatError(err)
} }
s.cache.DeleteCache(api.ShortcutCache, delete.ID) s.cache.DeleteCache(api.ShortcutCache, *delete.ID)
return nil return nil
} }
@ -292,16 +292,43 @@ func findShortcutList(ctx context.Context, tx *sql.Tx, find *api.ShortcutFind) (
} }
func deleteShortcut(ctx context.Context, tx *sql.Tx, delete *api.ShortcutDelete) error { func deleteShortcut(ctx context.Context, tx *sql.Tx, delete *api.ShortcutDelete) error {
result, err := tx.ExecContext(ctx, ` where, args := []string{}, []interface{}{}
DELETE FROM shortcut WHERE id = ?
`, delete.ID) if v := delete.ID; v != nil {
where, args = append(where, "id = ?"), append(args, *v)
}
if v := delete.CreatorID; v != nil {
where, args = append(where, "creator_id = ?"), append(args, *v)
}
stmt := `DELETE FROM shortcut WHERE ` + strings.Join(where, " AND ")
result, err := tx.ExecContext(ctx, stmt, args...)
if err != nil { if err != nil {
return FormatError(err) return FormatError(err)
} }
rows, _ := result.RowsAffected() rows, _ := result.RowsAffected()
if rows == 0 { if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("shortcut ID not found: %d", delete.ID)} return &common.Error{Code: common.NotFound, Err: fmt.Errorf("shortcut not found")}
}
return nil
}
func vacuumShortcut(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
shortcut
WHERE
creator_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
} }
return nil return nil

View File

@ -1,6 +1,7 @@
package store package store
import ( import (
"context"
"database/sql" "database/sql"
"github.com/usememos/memos/api" "github.com/usememos/memos/api"
@ -24,3 +25,51 @@ func New(db *sql.DB, profile *profile.Profile) *Store {
cache: cacheService, cache: cacheService,
} }
} }
func (s *Store) Vacuum(ctx context.Context) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return FormatError(err)
}
defer tx.Rollback()
if err := vacuum(ctx, tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return FormatError(err)
}
// Vacuum sqlite database file size after deleting resource.
if _, err := s.db.Exec("VACUUM"); err != nil {
return err
}
return nil
}
// Exec vacuum records in a transcation.
func vacuum(ctx context.Context, tx *sql.Tx) error {
if err := vacuumMemo(ctx, tx); err != nil {
return err
}
if err := vacuumResource(ctx, tx); err != nil {
return err
}
if err := vacuumShortcut(ctx, tx); err != nil {
return err
}
if err := vacuumUserSetting(ctx, tx); err != nil {
return err
}
if err := vacuumMemoOrganizer(ctx, tx); err != nil {
return err
}
if err := vacuumMemoResource(ctx, tx); err != nil {
// Prevent revive warning.
return err
}
return nil
}

View File

@ -175,13 +175,15 @@ func (s *Store) DeleteUser(ctx context.Context, delete *api.UserDelete) error {
} }
defer tx.Rollback() defer tx.Rollback()
err = deleteUser(ctx, tx, delete) if err := deleteUser(ctx, tx, delete); err != nil {
if err != nil { return err
return FormatError(err) }
if err := vacuum(ctx, tx); err != nil {
return err
} }
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
return FormatError(err) return err
} }
s.cache.DeleteCache(api.UserCache, delete.ID) s.cache.DeleteCache(api.UserCache, delete.ID)
@ -364,7 +366,7 @@ func deleteUser(ctx context.Context, tx *sql.Tx, delete *api.UserDelete) error {
rows, _ := result.RowsAffected() rows, _ := result.RowsAffected()
if rows == 0 { if rows == 0 {
return &common.Error{Code: common.NotFound, Err: fmt.Errorf("user ID not found: %d", delete.ID)} return &common.Error{Code: common.NotFound, Err: fmt.Errorf("user not found")}
} }
return nil return nil

View File

@ -149,3 +149,22 @@ func findUserSettingList(ctx context.Context, tx *sql.Tx, find *api.UserSettingF
return userSettingRawList, nil return userSettingRawList, nil
} }
func vacuumUserSetting(ctx context.Context, tx *sql.Tx) error {
stmt := `
DELETE FROM
user_setting
WHERE
user_id NOT IN (
SELECT
id
FROM
user
)`
_, err := tx.ExecContext(ctx, stmt)
if err != nil {
return FormatError(err)
}
return nil
}