query.go raw

   1  package memory
   2  
   3  import (
   4  	"bytes"
   5  	"encoding/json"
   6  
   7  	"git.mleku.dev/mleku/dendrite/pkg/ratio"
   8  	"github.com/dgraph-io/badger/v4"
   9  )
  10  
  11  // TypePoint is a single generation's type count for a tag.
  12  type TypePoint struct {
  13  	Gen   uint32
  14  	Count uint32
  15  }
  16  
  17  // MissingPoint is a single generation's missing count for a tag.
  18  type MissingPoint struct {
  19  	Gen   uint32
  20  	Count uint32
  21  }
  22  
  23  // FitnessPoint is a single generation's fitness score.
  24  type FitnessPoint struct {
  25  	Gen   uint32
  26  	Score ratio.Ratio
  27  }
  28  
  29  // HealthPoint is a single generation's health snapshot.
  30  type HealthPoint struct {
  31  	Gen            uint32
  32  	Occupied       uint32
  33  	Total          uint32
  34  	AvgLockIn      ratio.Ratio
  35  }
  36  
  37  // HexPoint is a single generation's operation count.
  38  type HexPoint struct {
  39  	Gen   uint32
  40  	Count uint32
  41  }
  42  
  43  // QueryTypeTrend returns type counts for a tag across the last N generations,
  44  // ordered by generation (ascending).
  45  func (d *DB) QueryTypeTrend(tag string, lastN int) []TypePoint {
  46  	h := TagHash(tag)
  47  	prefix := TypPrefix(h)
  48  	return collectTypPoints(d.db, prefix, lastN)
  49  }
  50  
  51  // QueryMissingTrend returns missing counts for a tag across the last N
  52  // generations, ordered by generation (ascending).
  53  func (d *DB) QueryMissingTrend(tag string, lastN int) []MissingPoint {
  54  	h := TagHash(tag)
  55  	prefix := MisPrefix(h)
  56  	return collectMisPoints(d.db, prefix, lastN)
  57  }
  58  
  59  // QueryFitnessTrajectory returns overall fitness scores across the last N
  60  // generations, ordered by generation (ascending).
  61  func (d *DB) QueryFitnessTrajectory(lastN int) []FitnessPoint {
  62  	prefix := FitDimPrefix(DimOverall)
  63  	return collectFitPoints(d.db, prefix, lastN)
  64  }
  65  
  66  // QueryFitnessDimension returns scores for a specific fitness dimension
  67  // across the last N generations.
  68  func (d *DB) QueryFitnessDimension(dim byte, lastN int) []FitnessPoint {
  69  	prefix := FitDimPrefix(dim)
  70  	return collectFitPoints(d.db, prefix, lastN)
  71  }
  72  
  73  // QueryHealthHistory returns health snapshots across the last N generations,
  74  // ordered by generation (ascending).
  75  func (d *DB) QueryHealthHistory(lastN int) []HealthPoint {
  76  	prefix := PrefixHlt[:]
  77  	var points []HealthPoint
  78  
  79  	_ = d.db.View(func(txn *badger.Txn) error {
  80  		opts := badger.DefaultIteratorOptions
  81  		opts.Reverse = true
  82  		opts.Prefix = prefix
  83  		it := txn.NewIterator(opts)
  84  		defer it.Close()
  85  
  86  		// Seek to end of prefix range.
  87  		end := PrefixEnd(prefix)
  88  		it.Seek(end)
  89  
  90  		for it.Valid() {
  91  			item := it.Item()
  92  			key := item.Key()
  93  			if !bytes.HasPrefix(key, prefix) {
  94  				break
  95  			}
  96  			gen := DecodeHlt(key)
  97  			var occ, tot uint32
  98  			var num, denom int64
  99  			_ = item.Value(func(val []byte) error {
 100  				occ, tot, num, denom = DecodeHltValue(val)
 101  				return nil
 102  			})
 103  			points = append(points, HealthPoint{
 104  				Gen:       gen,
 105  				Occupied:  occ,
 106  				Total:     tot,
 107  				AvgLockIn: ratio.New(num, denom),
 108  			})
 109  			if lastN > 0 && len(points) >= lastN {
 110  				break
 111  			}
 112  			it.Next()
 113  		}
 114  		return nil
 115  	})
 116  
 117  	// Reverse to ascending order.
 118  	reverse(points)
 119  	return points
 120  }
 121  
 122  // QueryBondCount returns the number of bonds for a tag in a specific generation.
 123  func (d *DB) QueryBondCount(tag string, gen uint32) int {
 124  	h := TagHash(tag)
 125  	prefix := BndGenPrefix(h, gen)
 126  	count := 0
 127  
 128  	_ = d.db.View(func(txn *badger.Txn) error {
 129  		opts := badger.DefaultIteratorOptions
 130  		opts.PrefetchValues = false
 131  		opts.Prefix = prefix
 132  		it := txn.NewIterator(opts)
 133  		defer it.Close()
 134  
 135  		for it.Seek(prefix); it.Valid(); it.Next() {
 136  			if !bytes.HasPrefix(it.Item().Key(), prefix) {
 137  				break
 138  			}
 139  			count++
 140  		}
 141  		return nil
 142  	})
 143  	return count
 144  }
 145  
 146  // QueryBondHistory returns bond counts for a tag across the last N generations.
 147  // Uses the bnd prefix to scan all generations and count per-gen entries.
 148  func (d *DB) QueryBondHistory(tag string, lastN int) []TypePoint {
 149  	h := TagHash(tag)
 150  	prefix := BndPrefix(h)
 151  
 152  	// Collect all (gen → count) by scanning all bonds for this tag.
 153  	genCounts := make(map[uint32]uint32)
 154  
 155  	_ = d.db.View(func(txn *badger.Txn) error {
 156  		opts := badger.DefaultIteratorOptions
 157  		opts.PrefetchValues = false
 158  		opts.Prefix = prefix
 159  		it := txn.NewIterator(opts)
 160  		defer it.Close()
 161  
 162  		for it.Seek(prefix); it.Valid(); it.Next() {
 163  			key := it.Item().Key()
 164  			if !bytes.HasPrefix(key, prefix) {
 165  				break
 166  			}
 167  			_, gen, _ := DecodeBnd(key)
 168  			genCounts[gen]++
 169  		}
 170  		return nil
 171  	})
 172  
 173  	// Sort by generation descending, take lastN.
 174  	var points []TypePoint
 175  	for gen, count := range genCounts {
 176  		points = append(points, TypePoint{Gen: gen, Count: count})
 177  	}
 178  	// Sort ascending by gen.
 179  	sortTypePoints(points)
 180  
 181  	if lastN > 0 && len(points) > lastN {
 182  		points = points[len(points)-lastN:]
 183  	}
 184  	return points
 185  }
 186  
 187  // QueryHexagramOps returns operation counts for a specific operation across
 188  // the last N generations.
 189  func (d *DB) QueryHexagramOps(op byte, lastN int) []HexPoint {
 190  	prefix := make([]byte, 4)
 191  	copy(prefix, PrefixHex[:])
 192  	prefix[3] = op
 193  
 194  	var points []HexPoint
 195  
 196  	_ = d.db.View(func(txn *badger.Txn) error {
 197  		opts := badger.DefaultIteratorOptions
 198  		opts.Reverse = true
 199  		opts.Prefix = prefix
 200  		it := txn.NewIterator(opts)
 201  		defer it.Close()
 202  
 203  		end := PrefixEnd(prefix)
 204  		it.Seek(end)
 205  
 206  		for it.Valid() {
 207  			item := it.Item()
 208  			key := item.Key()
 209  			if !bytes.HasPrefix(key, prefix) {
 210  				break
 211  			}
 212  			_, gen := DecodeHex(key)
 213  			var count uint32
 214  			_ = item.Value(func(val []byte) error {
 215  				count = DecodeU32Value(val)
 216  				return nil
 217  			})
 218  			points = append(points, HexPoint{Gen: gen, Count: count})
 219  			if lastN > 0 && len(points) >= lastN {
 220  				break
 221  			}
 222  			it.Next()
 223  		}
 224  		return nil
 225  	})
 226  
 227  	reverse(points)
 228  	return points
 229  }
 230  
 231  // QueryGeneration returns generation metadata. Returns nil if not found.
 232  func (d *DB) QueryGeneration(gen uint32) *genMeta {
 233  	key := GenKey(gen)
 234  	var meta genMeta
 235  	err := d.db.View(func(txn *badger.Txn) error {
 236  		item, err := txn.Get(key)
 237  		if err != nil {
 238  			return err
 239  		}
 240  		return item.Value(func(val []byte) error {
 241  			return json.Unmarshal(val, &meta)
 242  		})
 243  	})
 244  	if err != nil {
 245  		return nil
 246  	}
 247  	return &meta
 248  }
 249  
 250  // ADSRPoint is a single generation's ADSR phase distribution.
 251  type ADSRPoint struct {
 252  	Gen    uint32
 253  	Counts [4]uint32 // [Attack, Decay, Sustain, Release]
 254  }
 255  
 256  // QueryADSRHistory returns ADSR distributions across the last N generations,
 257  // ordered by generation (ascending).
 258  func (d *DB) QueryADSRHistory(lastN int) []ADSRPoint {
 259  	prefix := PrefixAdr[:]
 260  	var points []ADSRPoint
 261  
 262  	_ = d.db.View(func(txn *badger.Txn) error {
 263  		opts := badger.DefaultIteratorOptions
 264  		opts.Reverse = true
 265  		opts.Prefix = prefix
 266  		it := txn.NewIterator(opts)
 267  		defer it.Close()
 268  
 269  		end := PrefixEnd(prefix)
 270  		it.Seek(end)
 271  
 272  		for it.Valid() {
 273  			item := it.Item()
 274  			key := item.Key()
 275  			if !bytes.HasPrefix(key, prefix) {
 276  				break
 277  			}
 278  			gen := DecodeGen(key)
 279  			var counts [4]uint32
 280  			_ = item.Value(func(val []byte) error {
 281  				counts = DecodeAdrValue(val)
 282  				return nil
 283  			})
 284  			points = append(points, ADSRPoint{Gen: gen, Counts: counts})
 285  			if lastN > 0 && len(points) >= lastN {
 286  				break
 287  			}
 288  			it.Next()
 289  		}
 290  		return nil
 291  	})
 292  
 293  	reverse(points)
 294  	return points
 295  }
 296  
 297  // --- internal helpers ---
 298  
 299  func collectTypPoints(db *badger.DB, prefix []byte, lastN int) []TypePoint {
 300  	var points []TypePoint
 301  
 302  	_ = db.View(func(txn *badger.Txn) error {
 303  		opts := badger.DefaultIteratorOptions
 304  		opts.Reverse = true
 305  		opts.PrefetchValues = false
 306  		opts.Prefix = prefix
 307  		it := txn.NewIterator(opts)
 308  		defer it.Close()
 309  
 310  		end := PrefixEnd(prefix)
 311  		it.Seek(end)
 312  
 313  		for it.Valid() {
 314  			key := it.Item().Key()
 315  			if !bytes.HasPrefix(key, prefix) {
 316  				break
 317  			}
 318  			_, count, gen := DecodeTyp(key)
 319  			points = append(points, TypePoint{Gen: gen, Count: count})
 320  			if lastN > 0 && len(points) >= lastN {
 321  				break
 322  			}
 323  			it.Next()
 324  		}
 325  		return nil
 326  	})
 327  
 328  	reverse(points)
 329  	return points
 330  }
 331  
 332  func collectMisPoints(db *badger.DB, prefix []byte, lastN int) []MissingPoint {
 333  	var points []MissingPoint
 334  
 335  	_ = db.View(func(txn *badger.Txn) error {
 336  		opts := badger.DefaultIteratorOptions
 337  		opts.Reverse = true
 338  		opts.PrefetchValues = false
 339  		opts.Prefix = prefix
 340  		it := txn.NewIterator(opts)
 341  		defer it.Close()
 342  
 343  		end := PrefixEnd(prefix)
 344  		it.Seek(end)
 345  
 346  		for it.Valid() {
 347  			key := it.Item().Key()
 348  			if !bytes.HasPrefix(key, prefix) {
 349  				break
 350  			}
 351  			_, gen, count := DecodeMis(key)
 352  			points = append(points, MissingPoint{Gen: gen, Count: count})
 353  			if lastN > 0 && len(points) >= lastN {
 354  				break
 355  			}
 356  			it.Next()
 357  		}
 358  		return nil
 359  	})
 360  
 361  	reverse(points)
 362  	return points
 363  }
 364  
 365  func collectFitPoints(db *badger.DB, prefix []byte, lastN int) []FitnessPoint {
 366  	var points []FitnessPoint
 367  
 368  	_ = db.View(func(txn *badger.Txn) error {
 369  		opts := badger.DefaultIteratorOptions
 370  		opts.Reverse = true
 371  		opts.Prefix = prefix
 372  		it := txn.NewIterator(opts)
 373  		defer it.Close()
 374  
 375  		end := PrefixEnd(prefix)
 376  		it.Seek(end)
 377  
 378  		for it.Valid() {
 379  			item := it.Item()
 380  			key := item.Key()
 381  			if !bytes.HasPrefix(key, prefix) {
 382  				break
 383  			}
 384  			_, gen := DecodeFit(key)
 385  			var num, denom int64
 386  			_ = item.Value(func(val []byte) error {
 387  				num, denom = DecodeFitValue(val)
 388  				return nil
 389  			})
 390  			points = append(points, FitnessPoint{
 391  				Gen:   gen,
 392  				Score: ratio.New(num, denom),
 393  			})
 394  			if lastN > 0 && len(points) >= lastN {
 395  				break
 396  			}
 397  			it.Next()
 398  		}
 399  		return nil
 400  	})
 401  
 402  	reverse(points)
 403  	return points
 404  }
 405  
 406  // reverse reverses a slice in place. Works with any slice type via generics.
 407  func reverse[T any](s []T) {
 408  	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
 409  		s[i], s[j] = s[j], s[i]
 410  	}
 411  }
 412  
 413  // sortTypePoints sorts by generation ascending (insertion sort, small slices).
 414  func sortTypePoints(pts []TypePoint) {
 415  	for i := 1; i < len(pts); i++ {
 416  		for j := i; j > 0 && pts[j].Gen < pts[j-1].Gen; j-- {
 417  			pts[j], pts[j-1] = pts[j-1], pts[j]
 418  		}
 419  	}
 420  }
 421