| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706 |
- package storage
- import (
- "bufio"
- "fmt"
- "net"
- "sort"
- "strings"
- "sync"
- "testing"
- "time"
- )
- type testScan struct {
- keys []string
- offset int
- limit uint32
- keysOnly bool
- }
- type testKVServer struct {
- mu sync.Mutex
- data map[string][]byte
- lsns map[string]uint64
- writes map[string]int
- nextLSN uint64
- maxScanPage uint32
- scanOpens int
- scanNexts int
- scanCloses int
- keyOnlyOpens int
- gets int
- multiGets int
- closers []net.Conn
- }
- func newTestKVServer(t *testing.T) *testKVServer {
- t.Helper()
- return &testKVServer{
- data: make(map[string][]byte),
- lsns: make(map[string]uint64),
- writes: make(map[string]int),
- }
- }
- func newTestKVPool(kv *testKVServer, size int, timeout time.Duration) *KVPool {
- pool := &KVPool{
- pool: make(chan *KVClient, size),
- size: size,
- timeout: timeout,
- }
- for i := 0; i < size; i++ {
- pool.pool <- kv.client()
- }
- return pool
- }
- func (s *testKVServer) close() {
- s.mu.Lock()
- closers := append([]net.Conn(nil), s.closers...)
- s.mu.Unlock()
- for _, conn := range closers {
- _ = conn.Close()
- }
- }
- func (s *testKVServer) client() *KVClient {
- clientConn, serverConn := net.Pipe()
- s.mu.Lock()
- s.closers = append(s.closers, clientConn, serverConn)
- s.mu.Unlock()
- go s.handle(serverConn)
- return &KVClient{
- conn: clientConn,
- reader: bufio.NewReader(clientConn),
- writer: bufio.NewWriter(clientConn),
- nextID: 1,
- lastUsed: time.Now(),
- }
- }
- func (s *testKVServer) writeCount(prefix string) int {
- s.mu.Lock()
- defer s.mu.Unlock()
- var count int
- for key, writes := range s.writes {
- if strings.Contains(key, prefix) {
- count += writes
- }
- }
- return count
- }
- func (s *testKVServer) hasKey(key string) bool {
- s.mu.Lock()
- defer s.mu.Unlock()
- _, ok := s.data[key]
- return ok
- }
- func (s *testKVServer) countKeys(prefix string) int {
- s.mu.Lock()
- defer s.mu.Unlock()
- var count int
- for key := range s.data {
- if strings.HasPrefix(key, prefix) {
- count++
- }
- }
- return count
- }
- func (s *testKVServer) scanStats() (opens, nexts, closes int) {
- s.mu.Lock()
- defer s.mu.Unlock()
- return s.scanOpens, s.scanNexts, s.scanCloses
- }
- func (s *testKVServer) keyOnlyOpenCount() int {
- s.mu.Lock()
- defer s.mu.Unlock()
- return s.keyOnlyOpens
- }
- func (s *testKVServer) readStats() (gets, multiGets int) {
- s.mu.Lock()
- defer s.mu.Unlock()
- return s.gets, s.multiGets
- }
- func (s *testKVServer) handle(conn net.Conn) {
- defer conn.Close()
- r := bufio.NewReader(conn)
- scans := make(map[uint64]*testScan)
- var nextScan uint64 = 1
- for {
- opcode, _, requestID, payload, err := readFrame(r)
- if err != nil {
- return
- }
- body := s.execute(opcode, payload, scans, &nextScan)
- if _, err := conn.Write(encodeResponse(opcode, requestID, body)); err != nil {
- return
- }
- }
- }
- func (s *testKVServer) execute(opcode uint16, payload []byte, scans map[uint64]*testScan, nextScan *uint64) []byte {
- switch opcode {
- case opPing:
- body := make([]byte, 2+len(payload))
- putU16(body[0:2], statusOK)
- copy(body[2:], payload)
- return body
- case opGet:
- key, ok := parseOneKey(payload)
- if !ok {
- return errorBody("InvalidPayload")
- }
- s.mu.Lock()
- s.gets++
- value, found := s.data[string(key)]
- lsn := s.lsns[string(key)]
- s.mu.Unlock()
- if !found {
- body := make([]byte, 2)
- putU16(body[0:2], statusNotFound)
- return body
- }
- body := make([]byte, 14+len(value))
- putU16(body[0:2], statusOK)
- putU64(body[2:10], lsn)
- putU32(body[10:14], uint32(len(value)))
- copy(body[14:], value)
- return body
- case opPut:
- key, value, ok := parsePut(payload)
- if !ok {
- return errorBody("InvalidPayload")
- }
- s.mu.Lock()
- s.nextLSN++
- lsn := s.nextLSN
- s.data[string(key)] = append([]byte(nil), value...)
- s.lsns[string(key)] = lsn
- s.writes[string(key)]++
- s.mu.Unlock()
- body := make([]byte, 10)
- putU16(body[0:2], statusOK)
- putU64(body[2:10], lsn)
- return body
- case opDelete:
- key, ok := parseOneKey(payload)
- if !ok {
- return errorBody("InvalidPayload")
- }
- s.mu.Lock()
- _, found := s.data[string(key)]
- delete(s.data, string(key))
- delete(s.lsns, string(key))
- s.mu.Unlock()
- body := make([]byte, 3)
- putU16(body[0:2], statusOK)
- if found {
- body[2] = 1
- }
- return body
- case opExists:
- key, ok := parseOneKey(payload)
- if !ok {
- return errorBody("InvalidPayload")
- }
- s.mu.Lock()
- _, found := s.data[string(key)]
- s.mu.Unlock()
- body := make([]byte, 3)
- putU16(body[0:2], statusOK)
- if found {
- body[2] = 1
- }
- return body
- case opMultiGet:
- keys, ok := parseMultiGetKeys(payload)
- if !ok {
- return errorBody("InvalidPayload")
- }
- body := make([]byte, 6)
- putU16(body[0:2], statusOK)
- putU32(body[2:6], uint32(len(keys)))
- s.mu.Lock()
- s.multiGets++
- for _, key := range keys {
- value, found := s.data[string(key)]
- if !found {
- body = append(body, make([]byte, 16)...)
- continue
- }
- lsn := s.lsns[string(key)]
- entry := make([]byte, 16+len(value))
- entry[0] = 1
- putU32(entry[4:8], uint32(len(value)))
- putU64(entry[8:16], lsn)
- copy(entry[16:], value)
- body = append(body, entry...)
- }
- s.mu.Unlock()
- return body
- case opBatchWrite:
- ops, ok := parseBatchOps(payload)
- if !ok {
- return errorBody("InvalidPayload")
- }
- s.mu.Lock()
- s.nextLSN++
- lsn := s.nextLSN
- for _, op := range ops {
- if op.Op == batchPut {
- s.data[string(op.Key)] = append([]byte(nil), op.Value...)
- s.lsns[string(op.Key)] = lsn
- s.writes[string(op.Key)]++
- } else {
- delete(s.data, string(op.Key))
- delete(s.lsns, string(op.Key))
- }
- }
- s.mu.Unlock()
- body := make([]byte, 10)
- putU16(body[0:2], statusOK)
- putU64(body[2:10], lsn)
- return body
- case opScanOpen:
- includeValues, limit, prefix, ok := parseScanOpen(payload)
- if !ok {
- return errorBody("InvalidPayload")
- }
- s.mu.Lock()
- s.scanOpens++
- if !includeValues {
- s.keyOnlyOpens++
- }
- s.mu.Unlock()
- s.mu.Lock()
- keys := make([]string, 0)
- for key := range s.data {
- if strings.HasPrefix(key, string(prefix)) {
- keys = append(keys, key)
- }
- }
- s.mu.Unlock()
- sort.Strings(keys)
- if s.maxScanPage > 0 && limit > s.maxScanPage {
- limit = s.maxScanPage
- }
- id := *nextScan
- *nextScan = id + 1
- scans[id] = &testScan{keys: keys, limit: limit, keysOnly: !includeValues}
- body := make([]byte, 10)
- putU16(body[0:2], statusOK)
- putU64(body[2:10], id)
- return body
- case opScanNext:
- id, ok := parseScanID(payload)
- if !ok {
- return errorBody("InvalidPayload")
- }
- scan := scans[id]
- if scan == nil {
- return errorBody("ScanNotFound")
- }
- s.mu.Lock()
- s.scanNexts++
- s.mu.Unlock()
- remaining := len(scan.keys) - scan.offset
- count := int(scan.limit)
- if count > remaining {
- count = remaining
- }
- end := scan.offset + count
- body := make([]byte, 10)
- putU16(body[0:2], statusOK)
- if end >= len(scan.keys) {
- body[2] = 1
- }
- putU32(body[6:10], uint32(count))
- s.mu.Lock()
- for _, key := range scan.keys[scan.offset:end] {
- value := s.data[key]
- if scan.keysOnly {
- value = nil
- }
- lsn := s.lsns[key]
- entry := make([]byte, 16+len(key)+len(value))
- putU32(entry[0:4], uint32(len(key)))
- putU32(entry[4:8], uint32(len(value)))
- putU64(entry[8:16], lsn)
- copy(entry[16:], key)
- copy(entry[16+len(key):], value)
- body = append(body, entry...)
- }
- s.mu.Unlock()
- scan.offset = end
- return body
- case opScanClose:
- id, ok := parseScanID(payload)
- if !ok {
- return errorBody("InvalidPayload")
- }
- s.mu.Lock()
- s.scanCloses++
- s.mu.Unlock()
- delete(scans, id)
- body := make([]byte, 3)
- putU16(body[0:2], statusOK)
- body[2] = 1
- return body
- default:
- return errorBody("UnknownOpcode")
- }
- }
- func parsePut(payload []byte) ([]byte, []byte, bool) {
- if len(payload) < 8 {
- return nil, nil, false
- }
- keyLen := getU32(payload[0:4])
- valueLen := getU32(payload[4:8])
- if keyLen > maxKeySize || valueLen > maxValueSize {
- return nil, nil, false
- }
- if uint64(8)+uint64(keyLen)+uint64(valueLen) != uint64(len(payload)) {
- return nil, nil, false
- }
- return payload[8 : 8+keyLen], payload[8+keyLen:], true
- }
- func parseMultiGetKeys(payload []byte) ([][]byte, bool) {
- if len(payload) < 4 {
- return nil, false
- }
- count := getU32(payload[0:4])
- if count > maxOperations {
- return nil, false
- }
- keys := make([][]byte, 0, count)
- pos := 4
- for i := uint32(0); i < count; i++ {
- if len(payload)-pos < 4 {
- return nil, false
- }
- length := getU32(payload[pos : pos+4])
- pos += 4
- if length > maxKeySize || len(payload)-pos < int(length) {
- return nil, false
- }
- keys = append(keys, payload[pos:pos+int(length)])
- pos += int(length)
- }
- return keys, pos == len(payload)
- }
- func parseBatchOps(payload []byte) ([]BatchOp, bool) {
- if len(payload) < 8 {
- return nil, false
- }
- count := getU32(payload[0:4])
- metadataLen := getU32(payload[4:8])
- if count == 0 || count > maxOperations || uint64(metadataLen) > uint64(len(payload)-8) {
- return nil, false
- }
- pos := 8 + int(metadataLen)
- ops := make([]BatchOp, 0, count)
- for i := uint32(0); i < count; i++ {
- if len(payload)-pos < 12 {
- return nil, false
- }
- opcode := payload[pos]
- keyLen := getU32(payload[pos+4 : pos+8])
- valueLen := getU32(payload[pos+8 : pos+12])
- pos += 12
- if opcode != batchPut && opcode != batchDelete {
- return nil, false
- }
- if keyLen > maxKeySize || valueLen > maxValueSize {
- return nil, false
- }
- if opcode == batchDelete && valueLen != 0 {
- return nil, false
- }
- if len(payload)-pos < int(keyLen)+int(valueLen) {
- return nil, false
- }
- key := payload[pos : pos+int(keyLen)]
- pos += int(keyLen)
- value := payload[pos : pos+int(valueLen)]
- pos += int(valueLen)
- ops = append(ops, BatchOp{Op: opcode, Key: key, Value: value})
- }
- return ops, pos == len(payload)
- }
- func parseScanOpen(payload []byte) (bool, uint32, []byte, bool) {
- if len(payload) < 12 {
- return false, 0, nil, false
- }
- includeValues := payload[0] != 0
- limit := getU32(payload[4:8])
- prefixLen := getU32(payload[8:12])
- if limit == 0 || limit > 4096 || prefixLen > maxKeySize {
- return false, 0, nil, false
- }
- if uint64(12)+uint64(prefixLen) != uint64(len(payload)) {
- return false, 0, nil, false
- }
- return includeValues, limit, payload[12:], true
- }
- func parseScanID(payload []byte) (uint64, bool) {
- if len(payload) < 8 {
- return 0, false
- }
- return getU64(payload[0:8]), true
- }
- func TestInsertDoesNotRewriteSchemaForRowIDUpdates(t *testing.T) {
- kv := newTestKVServer(t)
- defer kv.close()
- pool := newTestKVPool(kv, 2, 5*time.Second)
- defer pool.Close()
- schemas := NewSchemaManager(pool, "testdb")
- tables := NewTableManager(pool, schemas, "testdb")
- err := schemas.CreateTable(&Schema{
- Name: "users",
- Columns: []Column{
- {Name: "id", Type: "INTEGER", Nullable: false, PrimaryKey: true},
- {Name: "name", Type: "TEXT", Nullable: true},
- },
- })
- if err != nil {
- t.Fatalf("create table: %v", err)
- }
- initialSchemaWrites := kv.writeCount(":_schema:")
- if initialSchemaWrites != 1 {
- t.Fatalf("expected create table to write schema once, got %d", initialSchemaWrites)
- }
- for i := int64(1); i <= 3; i++ {
- err := tables.Insert("users", Row{"id": i, "name": fmt.Sprintf("user-%d", i)})
- if err != nil {
- t.Fatalf("insert %d: %v", i, err)
- }
- }
- if got := kv.writeCount(":_schema:"); got != initialSchemaWrites {
- t.Fatalf("expected inserts not to rewrite schema, got %d schema writes", got)
- }
- if got := kv.writeCount(":_sys:rowid:"); got != 0 {
- t.Fatalf("expected no rowid counter writes for inserts, got %d", got)
- }
- }
- func TestRowIDIsDerivedFromRowsAfterRestart(t *testing.T) {
- kv := newTestKVServer(t)
- defer kv.close()
- pool := newTestKVPool(kv, 2, 5*time.Second)
- defer pool.Close()
- schemas := NewSchemaManager(pool, "testdb")
- tables := NewTableManager(pool, schemas, "testdb")
- err := schemas.CreateTable(&Schema{
- Name: "events",
- Columns: []Column{
- {Name: "name", Type: "TEXT", Nullable: true},
- },
- })
- if err != nil {
- t.Fatalf("create table: %v", err)
- }
- for i := 1; i <= 2; i++ {
- err := tables.Insert("events", Row{"name": fmt.Sprintf("event-%d", i)})
- if err != nil {
- t.Fatalf("insert %d: %v", i, err)
- }
- }
- // Simulate a process restart: new managers have empty in-memory ROWID state
- // but the same durable KV rows.
- restartedSchemas := NewSchemaManager(pool, "testdb")
- restartedTables := NewTableManager(pool, restartedSchemas, "testdb")
- if err := restartedTables.Insert("events", Row{"name": "event-3"}); err != nil {
- t.Fatalf("insert after restart: %v", err)
- }
- if !kv.hasKey("testdb:_data:events:3") {
- t.Fatalf("expected restart insert to continue at rowid 3")
- }
- if got := kv.writeCount(":_sys:rowid:"); got != 0 {
- t.Fatalf("expected no rowid counter writes, got %d", got)
- }
- }
- func TestInsertDoesNotWriteDurableIndexEntries(t *testing.T) {
- kv := newTestKVServer(t)
- defer kv.close()
- pool := newTestKVPool(kv, 2, 5*time.Second)
- defer pool.Close()
- schemas := NewSchemaManager(pool, "testdb")
- tables := NewTableManager(pool, schemas, "testdb")
- err := schemas.CreateTable(&Schema{
- Name: "users",
- Columns: []Column{
- {Name: "id", Type: "INTEGER", Nullable: false, PrimaryKey: true},
- {Name: "status", Type: "TEXT", Nullable: false},
- },
- })
- if err != nil {
- t.Fatalf("create table: %v", err)
- }
- err = schemas.CreateIndex(&Index{
- Name: "idx_users_status",
- Table: "users",
- Columns: []IndexColumn{
- {Name: "status"},
- },
- })
- if err != nil {
- t.Fatalf("create index: %v", err)
- }
- for i := int64(1); i <= 3; i++ {
- status := "active"
- if i == 2 {
- status = "inactive"
- }
- err := tables.Insert("users", Row{"id": i, "status": status})
- if err != nil {
- t.Fatalf("insert %d: %v", i, err)
- }
- }
- if got := kv.writeCount(":idx:"); got != 0 {
- t.Fatalf("expected no durable index entry writes, got %d", got)
- }
- rows, err := tables.SelectByIndex("users", "idx_users_status", "active")
- if err != nil {
- t.Fatalf("select by index: %v", err)
- }
- if len(rows) != 2 {
- t.Fatalf("expected 2 active rows from derived index, got %d", len(rows))
- }
- }
- func TestIndexIsDerivedFromRowsAfterRestart(t *testing.T) {
- kv := newTestKVServer(t)
- defer kv.close()
- pool := newTestKVPool(kv, 2, 5*time.Second)
- defer pool.Close()
- schemas := NewSchemaManager(pool, "testdb")
- tables := NewTableManager(pool, schemas, "testdb")
- err := schemas.CreateTable(&Schema{
- Name: "users",
- Columns: []Column{
- {Name: "id", Type: "INTEGER", Nullable: false, PrimaryKey: true},
- {Name: "status", Type: "TEXT", Nullable: false},
- },
- })
- if err != nil {
- t.Fatalf("create table: %v", err)
- }
- err = schemas.CreateIndex(&Index{
- Name: "idx_users_status",
- Table: "users",
- Columns: []IndexColumn{
- {Name: "status"},
- },
- })
- if err != nil {
- t.Fatalf("create index: %v", err)
- }
- for i := int64(1); i <= 3; i++ {
- status := "active"
- if i == 3 {
- status = "inactive"
- }
- if err := tables.Insert("users", Row{"id": i, "status": status}); err != nil {
- t.Fatalf("insert %d: %v", i, err)
- }
- }
- restartedSchemas := NewSchemaManager(pool, "testdb")
- restartedTables := NewTableManager(pool, restartedSchemas, "testdb")
- rows, err := restartedTables.SelectByIndex("users", "idx_users_status", "active")
- if err != nil {
- t.Fatalf("select by index after restart: %v", err)
- }
- if len(rows) != 2 {
- t.Fatalf("expected 2 active rows from restart-derived index, got %d", len(rows))
- }
- if got := kv.writeCount(":idx:"); got != 0 {
- t.Fatalf("expected no durable index entry writes, got %d", got)
- }
- }
- func TestListTableIndexesCachesMetadata(t *testing.T) {
- kv := newTestKVServer(t)
- defer kv.close()
- pool := newTestKVPool(kv, 2, 5*time.Second)
- defer pool.Close()
- schemas := NewSchemaManager(pool, "testdb")
- if err := schemas.CreateTable(&Schema{
- Name: "items",
- Columns: []Column{
- {Name: "id", Type: "INTEGER", PrimaryKey: true},
- {Name: "kind", Type: "TEXT"},
- },
- }); err != nil {
- t.Fatal(err)
- }
- if err := schemas.CreateIndex(&Index{
- Name: "idx_items_kind", Table: "items", Columns: []IndexColumn{{Name: "kind"}},
- }); err != nil {
- t.Fatal(err)
- }
- restarted := NewSchemaManager(pool, "testdb")
- getsBefore, _ := kv.readStats()
- if indexes, err := restarted.ListTableIndexes("items"); err != nil || len(indexes) != 1 {
- t.Fatalf("first list: indexes=%v err=%v", indexes, err)
- }
- getsAfterFirst, _ := kv.readStats()
- if getsAfterFirst <= getsBefore {
- t.Fatal("first index metadata lookup did not read durable metadata")
- }
- for i := 0; i < 10; i++ {
- if indexes, err := restarted.ListTableIndexes("items"); err != nil || len(indexes) != 1 {
- t.Fatalf("cached list %d: indexes=%v err=%v", i, indexes, err)
- }
- }
- getsAfterCached, _ := kv.readStats()
- if getsAfterCached != getsAfterFirst {
- t.Fatalf("cached index metadata issued %d extra reads", getsAfterCached-getsAfterFirst)
- }
- }
|