| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000 |
- package db
- import (
- "bufio"
- "encoding/binary"
- "fmt"
- "io"
- "os"
- "regexp"
- "sort"
- "strconv"
- "strings"
- "sync"
- "sync/atomic"
- )
- // hash returns a 32-bit FNV-1a hash of the string
- func hash(s string) uint32 {
- var h uint32 = 2166136261
- for i := 0; i < len(s); i++ {
- h = (h * 16777619) ^ uint32(s[i])
- }
- return h
- }
- // --- Radix Tree Implementation (Minimal) ---
- type radixNode struct {
- path string
- indices string // first char of children paths for fast lookup
- children []*radixNode
- leaf *IndexEntry // If non-nil, this is a valid key
- key string // Full key stored at leaf for convenience
- }
- type RadixTree struct {
- root *radixNode
- size int
- mu sync.RWMutex
- }
- func NewRadixTree() *RadixTree {
- return &RadixTree{
- root: &radixNode{},
- }
- }
- // longestCommonPrefix finds the length of the shared prefix of two strings
- func longestCommonPrefix(k1, k2 string) int {
- max := len(k1)
- if len(k2) < max {
- max = len(k2)
- }
- var i int
- for i = 0; i < max; i++ {
- if k1[i] != k2[i] {
- break
- }
- }
- return i
- }
- // Insert adds a key and its index entry to the tree
- func (t *RadixTree) Insert(key string, entry IndexEntry) {
- t.mu.Lock()
- defer t.mu.Unlock()
- n := t.root
- search := key
- for {
- // Handle empty search string (shouldn't happen in recursion usually)
- if len(search) == 0 {
- if n.leaf == nil {
- t.size++
- }
- n.leaf = &entry
- n.key = key
- return
- }
- // Find child
- parent := n
- found := false
- for i, char := range []byte(n.indices) {
- if char == search[0] {
- // Found a child starting with the same char
- n = n.children[i]
-
- // Check common prefix
- common := longestCommonPrefix(search, n.path)
-
- if common == len(n.path) {
- // Full match of child path, continue down
- search = search[common:]
- found = true
- break // Continue outer loop
- } else {
- // Split the node
- // Current child n becomes a child of the new split node
-
- // 1. Create split node with part of path
- splitNode := &radixNode{
- path: n.path[:common],
- indices: string(n.path[common]), // The char that differs
- children: []*radixNode{n},
- }
-
- // 2. Update existing child n
- n.path = n.path[common:] // Remaining part
-
- // 3. Insert new leaf for the new key (if needed)
- // The new key diverges at 'common' index
- if len(search) == common {
- // The new key IS the split node
- splitNode.leaf = &entry
- splitNode.key = key
- t.size++
- } else {
- // The new key is a child of the split node
- newBranch := &radixNode{
- path: search[common:],
- leaf: &entry,
- key: key,
- }
- t.size++
- splitNode.indices += string(search[common])
- splitNode.children = append(splitNode.children, newBranch)
- // Keep indices sorted? Not strictly necessary for correctness but good for determinism
- }
-
- // 4. Update parent to point to splitNode instead of n
- parent.children[i] = splitNode
- return
- }
- }
- }
-
- if !found {
- // No matching child, add new child
- newChild := &radixNode{
- path: search,
- leaf: &entry,
- key: key,
- }
- n.indices += string(search[0])
- n.children = append(n.children, newChild)
- t.size++
- return
- }
- }
- }
- // Get retrieves an entry
- func (t *RadixTree) Get(key string) (IndexEntry, bool) {
- t.mu.RLock()
- defer t.mu.RUnlock()
- n := t.root
- search := key
- for {
- if len(search) == 0 {
- if n.leaf != nil {
- return *n.leaf, true
- }
- return IndexEntry{}, false
- }
- found := false
- for i, char := range []byte(n.indices) {
- if char == search[0] {
- n = n.children[i]
- if strings.HasPrefix(search, n.path) {
- search = search[len(n.path):]
- found = true
- break
- }
- return IndexEntry{}, false
- }
- }
- if !found {
- return IndexEntry{}, false
- }
- }
- }
- // WalkPrefix iterates over all keys starting with prefix.
- // Returns true from callback to continue, false to stop.
- func (t *RadixTree) WalkPrefix(prefix string, callback func(key string, entry IndexEntry) bool) {
- t.mu.RLock()
- defer t.mu.RUnlock()
- n := t.root
- search := prefix
- // 1. Locate the node covering the prefix
- for len(search) > 0 {
- found := false
- for i, char := range []byte(n.indices) {
- if char == search[0] {
- n = n.children[i]
- // 3 cases:
- // 1. n.path == search (Exact match or consume all search) -> found target node
- // 2. n.path starts with search -> found target node (it's inside n)
- // 3. search starts with n.path -> continue down
-
- common := longestCommonPrefix(search, n.path)
- if common == len(search) {
- // Prefix fully consumed. 'n' is the root of the subtree.
- search = ""
- found = true
- break
- } else if common == len(n.path) {
- // 'n' path fully consumed, continue deeper
- search = search[common:]
- found = true
- break
- } else {
- // Mismatch, prefix not found
- return
- }
- }
- }
- if !found {
- return
- }
- }
- // 2. Recursive walk from n
- t.recursiveWalk(n, callback)
- }
- func (t *RadixTree) recursiveWalk(n *radixNode, callback func(key string, entry IndexEntry) bool) bool {
- if n.leaf != nil {
- if !callback(n.key, *n.leaf) {
- return false
- }
- }
- // To iterate in order, we should ideally keep indices sorted.
- // For now, we just iterate. If order matters, we sort children locally.
- // Let's do a quick sort of indices to ensure lexicographical order.
- if len(n.children) > 1 {
- // Optimization: clone to avoid mutating tree during read lock?
- // Actually indices string is immutable. We need to know the permutation.
- // A simple way is to build a list of children sorted by first char.
- type childRef struct {
- char byte
- node *radixNode
- }
- refs := make([]childRef, len(n.children))
- for i, char := range []byte(n.indices) {
- refs[i] = childRef{char, n.children[i]}
- }
- sort.Slice(refs, func(i, j int) bool { return refs[i].char < refs[j].char })
-
- for _, ref := range refs {
- if !t.recursiveWalk(ref.node, callback) {
- return false
- }
- }
- } else {
- for _, child := range n.children {
- if !t.recursiveWalk(child, callback) {
- return false
- }
- }
- }
- return true
- }
- // --- End Radix Tree ---
- // --- Full Text Index (Simple In-Memory) ---
- // Replacing Table 3 with a cleaner Token -> []Key logic handled via Radix too?
- // No, Inverted Index maps Token -> List of Keys.
- // We can use a map[string][]string (Token -> Keys) for simplicity and speed.
- type FullTextIndex struct {
- // Token -> List of Keys (strings)
- // Using string keys instead of IDs simplifies things as we moved away from KeyMap ID logic.
- index map[string][]string
- mu sync.RWMutex
- }
- func NewFullTextIndex() *FullTextIndex {
- return &FullTextIndex{
- index: make(map[string][]string),
- }
- }
- func (fti *FullTextIndex) Add(key string, value string) {
- fti.mu.Lock()
- defer fti.mu.Unlock()
-
- tokens := tokenize(value)
- for _, token := range tokens {
- // Deduplication check per key is expensive O(N).
- // For high perf, we might accept duplicates or use a set per token (map[string]map[string]bool).
- // Let's use simple append for now, optimized for write.
- fti.index[token] = append(fti.index[token], key)
- }
- }
- func tokenize(val string) []string {
- f := func(c rune) bool {
- return !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))
- }
- return strings.FieldsFunc(val, f)
- }
- // --- Storage & Cache ---
- // FreeList manages reusable disk space in memory using Best-Fit strategy.
- type FreeList struct {
- // Capacity -> Stack of Offsets
- buckets map[uint32][]int64
- // Sorted list of capacities available in buckets for fast Best-Fit lookup
- sortedCaps []uint32
- mu sync.Mutex
- }
- func NewFreeList() *FreeList {
- return &FreeList{
- buckets: make(map[uint32][]int64),
- }
- }
- // Add adds an offset to the free list for a given capacity.
- func (fl *FreeList) Add(cap uint32, offset int64) {
- fl.mu.Lock()
- defer fl.mu.Unlock()
-
- if _, exists := fl.buckets[cap]; !exists {
- fl.buckets[cap] = []int64{offset}
- // Maintain sortedCaps
- fl.sortedCaps = append(fl.sortedCaps, cap)
- sort.Slice(fl.sortedCaps, func(i, j int) bool { return fl.sortedCaps[i] < fl.sortedCaps[j] })
- } else {
- fl.buckets[cap] = append(fl.buckets[cap], offset)
- }
- }
- // Pop tries to get an available offset using Best-Fit strategy.
- func (fl *FreeList) Pop(targetCap uint32) (int64, uint32, bool) {
- fl.mu.Lock()
- defer fl.mu.Unlock()
-
- idx := sort.Search(len(fl.sortedCaps), func(i int) bool {
- return fl.sortedCaps[i] >= targetCap
- })
-
- if idx >= len(fl.sortedCaps) {
- return 0, 0, false
- }
-
- foundCap := fl.sortedCaps[idx]
- offsets := fl.buckets[foundCap]
-
- if len(offsets) == 0 {
- return 0, 0, false
- }
-
- lastIdx := len(offsets) - 1
- offset := offsets[lastIdx]
-
- if lastIdx == 0 {
- delete(fl.buckets, foundCap)
- fl.sortedCaps = append(fl.sortedCaps[:idx], fl.sortedCaps[idx+1:]...)
- } else {
- fl.buckets[foundCap] = offsets[:lastIdx]
- }
-
- return offset, foundCap, true
- }
- // StripedLock provides hashed locks for keys
- type StripedLock struct {
- locks [1024]sync.RWMutex
- }
- func (sl *StripedLock) GetLock(key string) *sync.RWMutex {
- h := hash(key)
- return &sl.locks[h%1024]
- }
- // Storage (Table 2) manages disk storage for values.
- type Storage struct {
- file *os.File
- filename string
- offset int64 // Current end of file (Atomic)
- freeList *FreeList
-
- // Simple LRU Cache for Values
- // Using sync.Map for simplicity in this iteration, though it's not LRU.
- // For true LRU we'd need a list+map protected by mutex.
- // Let's use a Mutex protected Map as a "Hot Cache".
- cache map[int64]string
- cacheMu sync.RWMutex
- }
- const (
- FlagDeleted = 0x00
- FlagValid = 0x01
- HeaderSize = 9 // 1(Flag) + 4(Cap) + 4(Len)
- AlignSize = 16 // Align capacity to 16 bytes
- MaxCacheSize = 10000 // Simple cap
- )
- func NewStorage(filename string) (*Storage, error) {
- f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644)
- if err != nil {
- return nil, err
- }
- st := &Storage{
- file: f,
- filename: filename,
- freeList: NewFreeList(),
- cache: make(map[int64]string),
- }
- if err := st.scan(); err != nil {
- f.Close()
- return nil, err
- }
- return st, nil
- }
- func (s *Storage) scan() error {
- offset := int64(0)
-
- if _, err := s.file.Seek(0, 0); err != nil {
- return err
- }
-
- reader := bufio.NewReader(s.file)
-
- for {
- header := make([]byte, HeaderSize)
- n, err := io.ReadFull(reader, header)
- if err == io.EOF {
- break
- }
- if err == io.ErrUnexpectedEOF && n == 0 {
- break
- }
- if err != nil {
- return err
- }
-
- flag := header[0]
- cap := binary.LittleEndian.Uint32(header[1:])
-
- if flag == FlagDeleted {
- s.freeList.Add(cap, offset)
- }
-
- discardLen := int(cap)
- if _, err := reader.Discard(discardLen); err != nil {
- buf := make([]byte, discardLen)
- if _, err := io.ReadFull(reader, buf); err != nil {
- return err
- }
- }
-
- offset += int64(HeaderSize + discardLen)
- }
-
- s.offset = offset
- return nil
- }
- func alignCapacity(length int) uint32 {
- if length == 0 {
- return AlignSize
- }
- cap := (length + AlignSize - 1) / AlignSize * AlignSize
- return uint32(cap)
- }
- // TryUpdateInPlace tries to update value in-place if capacity allows.
- func (s *Storage) TryUpdateInPlace(val string, offset int64) bool {
- if offset < 0 {
- return false
- }
-
- valLen := len(val)
- capBuf := make([]byte, 4)
- if _, err := s.file.ReadAt(capBuf, offset+1); err != nil {
- return false
- }
- oldCap := binary.LittleEndian.Uint32(capBuf)
- if uint32(valLen) > oldCap {
- return false
- }
-
- // Fits! Write Length then Data
- lenBuf := make([]byte, 4)
- binary.LittleEndian.PutUint32(lenBuf, uint32(valLen))
-
- if _, err := s.file.WriteAt(lenBuf, offset+5); err != nil {
- return false
- }
- if _, err := s.file.WriteAt([]byte(val), offset+HeaderSize); err != nil {
- return false
- }
-
- // Update Cache
- s.cacheMu.Lock()
- s.cache[offset] = val
- s.cacheMu.Unlock()
-
- return true
- }
- // AppendOrReuse writes a new value, reusing FreeList or Appending.
- func (s *Storage) AppendOrReuse(val string) (int64, error) {
- valLen := len(val)
- newCap := alignCapacity(valLen)
-
- var writeOffset int64
- var actualCap uint32
-
- // Try Reuse
- reusedOffset, capFromFree, found := s.freeList.Pop(newCap)
- if found {
- writeOffset = reusedOffset
- actualCap = capFromFree
- } else {
- // Append
- totalSize := HeaderSize + int(newCap)
- newEnd := atomic.AddInt64(&s.offset, int64(totalSize))
- writeOffset = newEnd - int64(totalSize)
- actualCap = newCap
- }
- buf := make([]byte, HeaderSize+int(actualCap))
- buf[0] = FlagValid
- binary.LittleEndian.PutUint32(buf[1:], actualCap)
- binary.LittleEndian.PutUint32(buf[5:], uint32(valLen))
- copy(buf[HeaderSize:], []byte(val))
-
- if _, err := s.file.WriteAt(buf, writeOffset); err != nil {
- return 0, err
- }
-
- // Add to Cache
- s.cacheMu.Lock()
- if len(s.cache) < MaxCacheSize {
- s.cache[writeOffset] = val
- }
- s.cacheMu.Unlock()
- return writeOffset, nil
- }
- // MarkDeleted marks a slot as deleted.
- func (s *Storage) MarkDeleted(offset int64) error {
- capBuf := make([]byte, 4)
- if _, err := s.file.ReadAt(capBuf, offset+1); err != nil {
- return err
- }
- oldCap := binary.LittleEndian.Uint32(capBuf)
- if _, err := s.file.WriteAt([]byte{FlagDeleted}, offset); err != nil {
- return err
- }
- s.freeList.Add(oldCap, offset)
-
- // Remove from Cache
- s.cacheMu.Lock()
- delete(s.cache, offset)
- s.cacheMu.Unlock()
-
- return nil
- }
- // ReadValue reads value at offset with Caching.
- func (s *Storage) ReadValue(offset int64) (string, error) {
- // 1. Check Cache
- s.cacheMu.RLock()
- if val, ok := s.cache[offset]; ok {
- s.cacheMu.RUnlock()
- return val, nil
- }
- s.cacheMu.RUnlock()
- // 2. Read Disk
- header := make([]byte, HeaderSize)
- if _, err := s.file.ReadAt(header, offset); err != nil {
- return "", err
- }
- flag := header[0]
- if flag == FlagDeleted {
- return "", fmt.Errorf("record deleted")
- }
- length := binary.LittleEndian.Uint32(header[5:])
- data := make([]byte, length)
- if _, err := s.file.ReadAt(data, offset+HeaderSize); err != nil {
- return "", err
- }
- val := string(data)
-
- // 3. Fill Cache
- s.cacheMu.Lock()
- if len(s.cache) < MaxCacheSize {
- s.cache[offset] = val
- } else {
- // Random eviction (map iteration order is random)
- // This is a poor man's eviction, but fast.
- for k := range s.cache {
- delete(s.cache, k)
- break
- }
- s.cache[offset] = val
- }
- s.cacheMu.Unlock()
- return val, nil
- }
- func (s *Storage) Close() error {
- return s.file.Close()
- }
- // IndexEntry (Table 1)
- type IndexEntry struct {
- ValueOffset int64
- CommitIndex uint64
- }
- // Engine is the core storage engine.
- type Engine struct {
- // Replaces KeyMap + Sharded Index with a single Radix Tree
- // Radix Tree is thread-safe and stores IndexEntry directly.
- Index *RadixTree
- // Table 2: Disk Storage
- Storage *Storage
- // Table 3: Full Text Index (New Implementation)
- FTIndex *FullTextIndex
-
- KeyLocks StripedLock
- LastCommitIndex uint64
- commitMu sync.Mutex // Protects LastCommitIndex
- dataDir string
- }
- func NewEngine(dataDir string) (*Engine, error) {
- if err := os.MkdirAll(dataDir, 0755); err != nil {
- return nil, err
- }
- store, err := NewStorage(dataDir + "/values.data")
- if err != nil {
- return nil, err
- }
- e := &Engine{
- Index: NewRadixTree(),
- Storage: store,
- FTIndex: NewFullTextIndex(),
- dataDir: dataDir,
- }
- return e, nil
- }
- func (e *Engine) Close() error {
- return e.Storage.Close()
- }
- func (e *Engine) Set(key, value string, commitIndex uint64) error {
- // 0. Lock Key
- kLock := e.KeyLocks.GetLock(key)
- kLock.Lock()
- defer kLock.Unlock()
- // 1. Check existing
- // We no longer need separate KeyID lookup. Radix Tree stores it all.
- var oldOffset int64 = -1
- if entry, ok := e.Index.Get(key); ok {
- oldOffset = entry.ValueOffset
- }
- // 2. Try In-Place
- if e.Storage.TryUpdateInPlace(value, oldOffset) {
- // Update Index (CommitIndex might change)
- entry := IndexEntry{ValueOffset: oldOffset, CommitIndex: commitIndex}
- e.Index.Insert(key, entry)
-
- e.commitMu.Lock()
- if commitIndex > e.LastCommitIndex {
- e.LastCommitIndex = commitIndex
- }
- e.commitMu.Unlock()
- return nil
- }
- // 3. Write New
- newOffset, err := e.Storage.AppendOrReuse(value)
- if err != nil {
- return err
- }
- // 4. Update Index
- // Write New Entry
- e.Index.Insert(key, IndexEntry{
- ValueOffset: newOffset,
- CommitIndex: commitIndex,
- })
-
- // 5. Delete Old
- if oldOffset >= 0 {
- e.Storage.MarkDeleted(oldOffset)
- }
-
- // Update global commit index
- e.commitMu.Lock()
- if commitIndex > e.LastCommitIndex {
- e.LastCommitIndex = commitIndex
- }
- e.commitMu.Unlock()
- // 6. Update Full Text Index
- // Ideally we remove old tokens too, but that's expensive without reverse index.
- // For append-only log structured db, we just add new ones.
- e.FTIndex.Add(key, value)
-
- return nil
- }
- func (e *Engine) Get(key string) (string, bool) {
- kLock := e.KeyLocks.GetLock(key)
- kLock.RLock()
- defer kLock.RUnlock()
- entry, ok := e.Index.Get(key)
- if !ok {
- return "", false
- }
- val, err := e.Storage.ReadValue(entry.ValueOffset)
- if err != nil {
- return "", false
- }
- return val, true
- }
- type QueryResult struct {
- Key string `json:"key"`
- Value string `json:"value"`
- CommitIndex uint64 `json:"commit_index"`
- }
- func WildcardMatch(str, pattern string) bool {
- s := []rune(str)
- p := []rune(pattern)
- sLen, pLen := len(s), len(p)
- i, j := 0, 0
- starIdx, matchIdx := -1, -1
- for i < sLen {
- if j < pLen && (p[j] == '?' || p[j] == s[i]) {
- i++
- j++
- } else if j < pLen && p[j] == '*' {
- starIdx = j
- matchIdx = i
- j++
- } else if starIdx != -1 {
- j = starIdx + 1
- matchIdx++
- i = matchIdx
- } else {
- return false
- }
- }
- for j < pLen && p[j] == '*' {
- j++
- }
- return j == pLen
- }
- func (e *Engine) Query(sql string) ([]QueryResult, error) {
- sql = strings.TrimSpace(sql)
-
- reLimit := regexp.MustCompile(`(?i)\s+LIMIT\s+(\d+)`)
- reOffset := regexp.MustCompile(`(?i)\s+OFFSET\s+(\d+)`)
-
- limit := -1
- offset := 0
-
- if match := reLimit.FindStringSubmatch(sql); len(match) > 0 {
- limit, _ = strconv.Atoi(match[1])
- sql = reLimit.ReplaceAllString(sql, "")
- }
- if match := reOffset.FindStringSubmatch(sql); len(match) > 0 {
- offset, _ = strconv.Atoi(match[1])
- sql = reOffset.ReplaceAllString(sql, "")
- }
- re := regexp.MustCompile(`(key|value|CommitIndex)\s*(=|like|>=|<=|>|<)\s*("[^"]*"|\d+)`)
- matches := re.FindAllStringSubmatch(sql, -1)
- if len(matches) == 0 {
- return nil, fmt.Errorf("invalid query")
- }
- extractString := func(s string) string {
- return strings.Trim(strings.TrimSpace(s), "\"")
- }
- // Optimization: Point Lookup Fast Path
- // If query is exactly `key = "..."`, utilize Hash/Radix Lookup O(1)
- for _, match := range matches {
- if match[1] == "key" && match[2] == "=" {
- targetKey := extractString(match[3])
- // 1. Get Entry
- entry, ok := e.Index.Get(targetKey)
- if !ok {
- return []QueryResult{}, nil
- }
-
- // 2. Load Value (if needed by other filters, but here we load anyway for result)
- // Optimize: check if we filter on Value before loading?
- // For simplicity, just load.
- val, err := e.Storage.ReadValue(entry.ValueOffset)
- if err != nil {
- return []QueryResult{}, nil
- }
-
- // 3. Verify other conditions (e.g. CommitIndex)
- matchAll := true
- for _, m := range matches {
- field, op, valRaw := m[1], m[2], m[3]
- switch field {
- case "CommitIndex":
- num, _ := strconv.ParseUint(valRaw, 10, 64)
- switch op {
- case ">": if !(entry.CommitIndex > num) { matchAll = false }
- case "<": if !(entry.CommitIndex < num) { matchAll = false }
- case ">=": if !(entry.CommitIndex >= num) { matchAll = false }
- case "<=": if !(entry.CommitIndex <= num) { matchAll = false }
- case "=": if !(entry.CommitIndex == num) { matchAll = false }
- }
- case "value":
- t := extractString(valRaw)
- switch op {
- case "=": if val != t { matchAll = false }
- case "like": if !WildcardMatch(val, t) { matchAll = false }
- }
- }
- if !matchAll { break }
- }
-
- if matchAll {
- return []QueryResult{{Key: targetKey, Value: val, CommitIndex: entry.CommitIndex}}, nil
- }
- return []QueryResult{}, nil
- }
- }
- var results []QueryResult
- var mu sync.Mutex
- // Strategy:
- // 1. Identify primary filter (Key Prefix is best)
- // 2. Iterate candidates
- // 3. Filter remaining conditions
-
- var prefix string = ""
- var usePrefix bool = false
-
- // Check for key prefix
- for _, match := range matches {
- if match[1] == "key" && match[2] == "like" {
- pattern := extractString(match[3])
- if strings.HasSuffix(pattern, "*") {
- clean := pattern[:len(pattern)-1]
- if !strings.ContainsAny(clean, "*?") {
- prefix = clean
- usePrefix = true
- break
- }
- }
- }
- }
-
- // Iterator
- iterator := func(key string, entry IndexEntry) bool {
- // Filter Logic
- var valStr string
- var valLoaded bool
-
- matchAll := true
- for _, match := range matches {
- field, op, valRaw := match[1], match[2], match[3]
- switch field {
- case "CommitIndex":
- num, _ := strconv.ParseUint(valRaw, 10, 64)
- switch op {
- case ">": if !(entry.CommitIndex > num) { matchAll = false }
- case "<": if !(entry.CommitIndex < num) { matchAll = false }
- case ">=": if !(entry.CommitIndex >= num) { matchAll = false }
- case "<=": if !(entry.CommitIndex <= num) { matchAll = false }
- case "=": if !(entry.CommitIndex == num) { matchAll = false }
- }
- case "key":
- target := extractString(valRaw)
- switch op {
- case "=": if key != target { matchAll = false }
- case "like": if !WildcardMatch(key, target) { matchAll = false }
- }
- case "value":
- // Lazy load
- if !valLoaded {
- v, err := e.Storage.ReadValue(entry.ValueOffset)
- if err != nil { matchAll = false; break }
- valStr = v
- valLoaded = true
- }
- target := extractString(valRaw)
- switch op {
- case "=": if valStr != target { matchAll = false }
- case "like": if !WildcardMatch(valStr, target) { matchAll = false }
- }
- }
- if !matchAll { break }
- }
-
- if matchAll {
- // Load value if needed for result
- if !valLoaded {
- v, err := e.Storage.ReadValue(entry.ValueOffset)
- if err == nil { valStr = v }
- }
- mu.Lock()
- results = append(results, QueryResult{
- Key: key,
- Value: valStr,
- CommitIndex: entry.CommitIndex,
- })
- // Optimization: If simple LIMIT without Offset and no sorting requirements (default key sort is free in Radix),
- // we could stop early. But we need to support Offset and flexible Sort.
- // Currently Radix Walk is Key-Ordered.
- // If user doesn't require specific sort (or accepts key sort), we can stop.
- // Let's assume Key Sort is default.
- if limit > 0 && offset == 0 && len(results) >= limit {
- mu.Unlock()
- return false // Stop iteration
- }
- mu.Unlock()
- }
- return true // Continue
- }
- if usePrefix {
- e.Index.WalkPrefix(prefix, iterator)
- } else {
- e.Index.WalkPrefix("", iterator) // Full Scan
- }
- // Results are already sorted by Key due to Radix Tree Walk order!
- // So we can skip sort.Slice if we trust WalkPrefix.
- // My WalkPrefix implementation attempts to be ordered.
-
- // Pagination
- if offset > 0 {
- if offset >= len(results) {
- return []QueryResult{}, nil
- }
- results = results[offset:]
- }
- if limit >= 0 {
- if limit < len(results) {
- results = results[:limit]
- }
- }
- return results, nil
- }
- func (e *Engine) Snapshot() ([]byte, error) {
- // Not implemented for Radix Tree yet in this demo
- return nil, nil
- }
- func (e *Engine) Restore(data []byte) error {
- return nil
- }
|