mirror of
https://github.com/hanzoai/orm.git
synced 2026-10-30 15:01:22 +00:00
_entities.id is a bare primary key, and the put upsert replaced data whatever kind held the id, keeping that row's kind: a key written at a user's id became that user's row. Put, PutMulti and the transaction's Put update only a row of their own kind and answer ErrKindMismatch when another kind holds the id, leaving it byte for byte; a batch with one such key writes nothing. The ZAP SQL backend carries the same guard, held on a real PostgreSQL through the ZAP client, and the document backend updates by (_id, kind) and reports an insert conflict as ErrKindMismatch.
123 lines
4.0 KiB
Go
123 lines
4.0 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
// A put makes its row live, so an id that was deleted can be used again.
|
|
//
|
|
// Delete is a tombstone, and the put upsert used to replace only `data`: the write
|
|
// returned no error and the row stayed deleted = 1, which every read filters out.
|
|
// Writing the same id a second, third and fourth time changed nothing — the entity
|
|
// was stored and unreadable, and the caller had no way to tell. Held across all
|
|
// three writers because it was one statement copied three times.
|
|
func TestPutRevivesADeletedId(t *testing.T) {
|
|
db := newTestDB(t)
|
|
ctx := context.Background()
|
|
key := db.NewKey("user", "u1", 0, nil)
|
|
|
|
if _, err := db.Put(ctx, key, &testEntity{Name: "first"}); err != nil {
|
|
t.Fatalf("put: %v", err)
|
|
}
|
|
if err := db.Delete(ctx, key); err != nil {
|
|
t.Fatalf("delete: %v", err)
|
|
}
|
|
if _, err := db.Put(ctx, key, &testEntity{Name: "second"}); err != nil {
|
|
t.Fatalf("re-put: %v", err)
|
|
}
|
|
|
|
var got testEntity
|
|
if err := db.Get(ctx, key, &got); err != nil {
|
|
t.Fatalf("an entity written after its id was deleted is unreadable: %v", err)
|
|
}
|
|
if got.Name != "second" {
|
|
t.Fatalf("read %q, want the value the second put wrote", got.Name)
|
|
}
|
|
|
|
// A query answers for it too, so the row is live and not merely gettable.
|
|
var all []testEntity
|
|
keys, err := db.Query("user").GetAll(ctx, &all)
|
|
if err != nil {
|
|
t.Fatalf("query: %v", err)
|
|
}
|
|
if len(keys) != 1 || len(all) != 1 || all[0].Name != "second" {
|
|
t.Fatalf("query returned %d rows %+v, want the one live entity", len(keys), all)
|
|
}
|
|
}
|
|
|
|
func TestPutMultiRevivesADeletedId(t *testing.T) {
|
|
db := newTestDB(t)
|
|
ctx := context.Background()
|
|
keys := []Key{db.NewKey("user", "u1", 0, nil), db.NewKey("user", "u2", 0, nil)}
|
|
|
|
if _, err := db.PutMulti(ctx, keys, []*testEntity{{Name: "a"}, {Name: "b"}}); err != nil {
|
|
t.Fatalf("put multi: %v", err)
|
|
}
|
|
if err := db.DeleteMulti(ctx, keys); err != nil {
|
|
t.Fatalf("delete multi: %v", err)
|
|
}
|
|
if _, err := db.PutMulti(ctx, keys, []*testEntity{{Name: "a2"}, {Name: "b2"}}); err != nil {
|
|
t.Fatalf("re-put multi: %v", err)
|
|
}
|
|
|
|
var got testEntity
|
|
if err := db.Get(ctx, keys[0], &got); err != nil || got.Name != "a2" {
|
|
t.Fatalf("first re-put entity unreadable: %v %+v", err, got)
|
|
}
|
|
if err := db.Get(ctx, keys[1], &got); err != nil || got.Name != "b2" {
|
|
t.Fatalf("second re-put entity unreadable: %v %+v", err, got)
|
|
}
|
|
}
|
|
|
|
func TestTransactionPutRevivesADeletedId(t *testing.T) {
|
|
db := newTestDB(t)
|
|
ctx := context.Background()
|
|
key := db.NewKey("user", "u1", 0, nil)
|
|
|
|
if _, err := db.Put(ctx, key, &testEntity{Name: "first"}); err != nil {
|
|
t.Fatalf("put: %v", err)
|
|
}
|
|
if err := db.Delete(ctx, key); err != nil {
|
|
t.Fatalf("delete: %v", err)
|
|
}
|
|
if err := db.RunInTransaction(ctx, func(tx Transaction) error {
|
|
_, err := tx.Put(key, &testEntity{Name: "second"})
|
|
return err
|
|
}, nil); err != nil {
|
|
t.Fatalf("transaction: %v", err)
|
|
}
|
|
|
|
var got testEntity
|
|
if err := db.Get(ctx, key, &got); err != nil || got.Name != "second" {
|
|
t.Fatalf("entity written in a transaction after its id was deleted is unreadable: %v %+v", err, got)
|
|
}
|
|
}
|
|
|
|
// The revival is the writer's OWN row and no one else's: an id held by a deleted
|
|
// row of another kind stays deleted, and the put onto it is refused, so no
|
|
// tombstone is published under a kind it was never written as.
|
|
func TestPutLeavesAnotherKindsTombstone(t *testing.T) {
|
|
db := newTestDB(t)
|
|
ctx := context.Background()
|
|
|
|
user := db.NewKey("user", "shared", 0, nil)
|
|
if _, err := db.Put(ctx, user, &testEntity{Name: "user row"}); err != nil {
|
|
t.Fatalf("put: %v", err)
|
|
}
|
|
if err := db.Delete(ctx, user); err != nil {
|
|
t.Fatalf("delete: %v", err)
|
|
}
|
|
|
|
order := db.NewKey("order", "shared", 0, nil)
|
|
if _, err := db.Put(ctx, order, &testEntity{Name: "order row"}); !errors.Is(err, ErrKindMismatch) {
|
|
t.Fatalf("put other kind: %v, want ErrKindMismatch", err)
|
|
}
|
|
|
|
var got testEntity
|
|
if err := db.Get(ctx, user, &got); !errors.Is(err, ErrNoSuchEntity) {
|
|
t.Fatalf("the deleted user row came back as %+v (err %v)", got, err)
|
|
}
|
|
}
|