From d84495af2ea41f42e6d51615d9903eebdd8db831 Mon Sep 17 00:00:00 2001 From: AI Engineer Date: Sun, 3 May 2026 23:02:31 +0800 Subject: [PATCH] feat: implement smart delete with shadow table support --- DB.go | 34 +++++++++++++++++++++---------- Tx.go | 17 ++++++++++++++-- delete_test.go | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 delete_test.go diff --git a/DB.go b/DB.go index 3dded27..c18c6e3 100644 --- a/DB.go +++ b/DB.go @@ -610,18 +610,30 @@ func (db *DB) Update(table string, data any, conditions string, args ...any) *Ex } func (db *DB) Delete(table string, conditions string, args ...any) *ExecResult { - if conditions != "" { - conditions = " where " + conditions - } - query := fmt.Sprintf("delete from %s%s", db.Quote(table), conditions) - r := baseExec(db.conn, nil, query, args...) - r.logger = db.logger - if r.Error != nil { - db.logger.LogQueryError(r.Error.Error(), query, args, r.usedTime) - } else { - if db.Config.LogSlow > 0 && r.usedTime >= float32(db.Config.LogSlow.TimeDuration()/time.Millisecond) { - db.logger.LogQuery(query, args, r.usedTime) + ts := db.getTable(table) + if !ts.HasShadowTable { + if conditions != "" { + conditions = " where " + conditions } + query := fmt.Sprintf("delete from %s%s", db.Quote(table), conditions) + r := baseExec(db.conn, nil, query, args...) + r.logger = db.logger + if r.Error != nil { + db.logger.LogQueryError(r.Error.Error(), query, args, r.usedTime) + } else { + if db.Config.LogSlow > 0 && r.usedTime >= float32(db.Config.LogSlow.TimeDuration()/time.Millisecond) { + db.logger.LogQuery(query, args, r.usedTime) + } + } + return r + } + + // Shadow delete + tx := db.Begin() + defer tx.CheckFinished() + r := tx.Delete(table, conditions, args...) + if r.Error == nil { + tx.Commit() } return r } diff --git a/Tx.go b/Tx.go index decfe00..035b14c 100644 --- a/Tx.go +++ b/Tx.go @@ -165,10 +165,23 @@ func (tx *Tx) Update(table string, data any, conditions string, args ...any) *Ex } func (tx *Tx) Delete(table string, conditions string, args ...any) *ExecResult { + ts := tx.db.getTable(table) + where := "" if conditions != "" { - conditions = " where " + conditions + where = " where " + conditions } - query := fmt.Sprintf("delete from %s%s", tx.Quote(table), conditions) + + if ts.HasShadowTable { + // Move to shadow table + moveQuery := fmt.Sprintf("insert into %s select * from %s%s", tx.Quote(table+"_deleted"), tx.Quote(table), where) + r := baseExec(nil, tx.conn, moveQuery, args...) + if r.Error != nil { + tx.logger.LogQueryError(r.Error.Error(), moveQuery, args, r.usedTime) + return r + } + } + + query := fmt.Sprintf("delete from %s%s", tx.Quote(table), where) tx.lastSql = &query tx.lastArgs = args r := baseExec(nil, tx.conn, query, args...) diff --git a/delete_test.go b/delete_test.go new file mode 100644 index 0000000..13a23e4 --- /dev/null +++ b/delete_test.go @@ -0,0 +1,54 @@ +package db_test + +import ( + "testing" + "apigo.cc/go/db" + _ "modernc.org/sqlite" +) + +func TestSmartDelete(t *testing.T) { + dbInst := db.GetDB("sqlite://:memory:", nil) + + // Create table and shadow table + dbInst.Exec("CREATE TABLE orders (id INTEGER PRIMARY KEY, item TEXT)") + dbInst.Exec("CREATE TABLE orders_deleted (id INTEGER PRIMARY KEY, item TEXT)") + + t.Run("ShadowDelete", func(t *testing.T) { + dbInst.Exec("INSERT INTO orders (id, item) VALUES (1, 'Phone')") + + res := dbInst.Delete("orders", "id = 1") + if res.Error != nil { + t.Fatalf("Delete failed: %v", res.Error) + } + if res.Changes() != 1 { + t.Errorf("Expected 1 change, got %d", res.Changes()) + } + + // Verify it's gone from main table + qr := dbInst.Query("SELECT COUNT(*) FROM orders WHERE id = 1") + count, _ := db.ToValue[int](qr) + if count != 0 { + t.Errorf("Expected 0 records in main table, got %d", count) + } + + // Verify it's in shadow table + qr2 := dbInst.Query("SELECT COUNT(*) FROM orders_deleted WHERE id = 1") + countDeleted, _ := db.ToValue[int](qr2) + if countDeleted != 1 { + t.Errorf("Expected 1 record in shadow table, got %d", countDeleted) + } + }) + + t.Run("PhysicalDelete", func(t *testing.T) { + dbInst.Exec("CREATE TABLE logs (id INTEGER PRIMARY KEY, msg TEXT)") + dbInst.Exec("INSERT INTO logs (id, msg) VALUES (1, 'Login')") + + dbInst.Delete("logs", "id = 1") + + qr := dbInst.Query("SELECT COUNT(*) FROM logs WHERE id = 1") + count, _ := db.ToValue[int](qr) + if count != 0 { + t.Errorf("Expected 0 records in logs, got %d", count) + } + }) +}