2026-02-10 14:02:11 +00:00
|
|
|
|
package atree
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
2026-02-23 19:37:05 +00:00
|
|
|
|
"hash/crc32"
|
2026-02-10 14:02:11 +00:00
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
|
|
|
|
"gordenko.dev/dima/qb/bin"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const (
|
2026-02-23 19:37:05 +00:00
|
|
|
|
PageTypeData = 1
|
|
|
|
|
|
PageTypeIndex = 2
|
|
|
|
|
|
|
2026-02-10 14:02:11 +00:00
|
|
|
|
filePerm = 0770
|
|
|
|
|
|
|
2026-02-23 19:37:05 +00:00
|
|
|
|
// common
|
2026-05-10 00:59:47 +00:00
|
|
|
|
crc32Idx = PageSize - 4
|
|
|
|
|
|
pageTypeIdx = PageSize - 5
|
2026-02-23 19:37:05 +00:00
|
|
|
|
|
2026-02-10 14:02:11 +00:00
|
|
|
|
// index page
|
2026-05-17 12:58:46 +00:00
|
|
|
|
indexCRC32Idx = IndexPageSize - 4
|
|
|
|
|
|
isLastLevelIdx = IndexPageSize - 5
|
|
|
|
|
|
indexRecordsQtyIdx = IndexPageSize - 7
|
|
|
|
|
|
|
|
|
|
|
|
indexRecordSize = 8
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
|
|
|
|
|
// data page
|
2026-05-17 12:58:46 +00:00
|
|
|
|
dataCRC32Idx = DataPageSize - 4
|
|
|
|
|
|
prevPageIdx = DataPageSize - 8
|
|
|
|
|
|
valuesSizeIdx = DataPageSize - 10
|
|
|
|
|
|
timestampsSizeIdx = DataPageSize - 12
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
|
|
|
|
|
timestampSize = 4
|
|
|
|
|
|
pairSize = timestampSize + PageNoSize
|
|
|
|
|
|
indexFooterIdx = indexRecordsQtyIdx
|
|
|
|
|
|
dataFooterIdx = timestampsSizeIdx
|
|
|
|
|
|
|
2026-05-17 12:58:46 +00:00
|
|
|
|
PageSize = 8192
|
|
|
|
|
|
DataPageSize = 8192
|
|
|
|
|
|
IndexPageSize = 1024
|
|
|
|
|
|
PageNoSize = 4
|
2026-02-10 14:02:11 +00:00
|
|
|
|
//
|
|
|
|
|
|
DataPagePayloadSize int = dataFooterIdx
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-23 19:37:05 +00:00
|
|
|
|
const (
|
|
|
|
|
|
FlagReused byte = 1 // сторінка із FreeList
|
|
|
|
|
|
FlagNewRoot byte = 2 // новая страница
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
castagnoliTable = crc32.MakeTable(crc32.Castagnoli)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
func calcChecksum(page []byte) uint32 {
|
|
|
|
|
|
return crc32.Checksum(page, castagnoliTable)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type PageToWrite struct {
|
|
|
|
|
|
PageNo uint32
|
|
|
|
|
|
Data []byte
|
|
|
|
|
|
IsReused bool
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 14:02:11 +00:00
|
|
|
|
type FreeList interface {
|
2026-02-23 11:02:29 +00:00
|
|
|
|
// використовується в allocPage
|
2026-02-10 14:02:11 +00:00
|
|
|
|
ReservePage() uint32
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type _page struct {
|
|
|
|
|
|
PageNo uint32
|
|
|
|
|
|
Buf []byte
|
|
|
|
|
|
ReferenceCount int
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type Atree struct {
|
2026-02-23 19:37:05 +00:00
|
|
|
|
file *os.File
|
|
|
|
|
|
mutex sync.Mutex
|
2026-05-10 00:59:47 +00:00
|
|
|
|
freelist FreeList
|
2026-02-23 19:37:05 +00:00
|
|
|
|
allocatedPagesQty uint32
|
|
|
|
|
|
pages map[uint32]*_page
|
|
|
|
|
|
pageWaits map[uint32][]chan readResult
|
|
|
|
|
|
pagesToRead []uint32
|
|
|
|
|
|
readSignalCh chan struct{}
|
|
|
|
|
|
writeSignalCh chan struct{}
|
2026-05-10 00:59:47 +00:00
|
|
|
|
//writeTasksQueue []WriteTask
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type Options struct {
|
2026-02-23 19:37:05 +00:00
|
|
|
|
Dir string
|
|
|
|
|
|
DatabaseName string
|
|
|
|
|
|
FreeList FreeList
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func New(opt Options) (*Atree, error) {
|
|
|
|
|
|
if opt.Dir == "" {
|
|
|
|
|
|
return nil, errors.New("Dir option is required")
|
|
|
|
|
|
}
|
2026-02-23 19:37:05 +00:00
|
|
|
|
// if opt.RedoDir == "" {
|
|
|
|
|
|
// return nil, errors.New("RedoDir option is required")
|
|
|
|
|
|
// }
|
2026-02-10 14:02:11 +00:00
|
|
|
|
if opt.DatabaseName == "" {
|
|
|
|
|
|
return nil, errors.New("DatabaseName option is required")
|
|
|
|
|
|
}
|
2026-02-23 19:37:05 +00:00
|
|
|
|
if opt.FreeList == nil {
|
|
|
|
|
|
return nil, errors.New("FreeList option is required")
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
// открываю или создаю dbName.data и dbName.index файлы
|
|
|
|
|
|
var (
|
2026-02-23 19:37:05 +00:00
|
|
|
|
fileName = filepath.Join(opt.Dir, opt.DatabaseName+".db")
|
|
|
|
|
|
file *os.File
|
|
|
|
|
|
allocatedPagesQty uint32
|
2026-02-10 14:02:11 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// При создании data файла сразу создается индекс, поэтому корректное
|
|
|
|
|
|
// состояние БД: либо оба файла есть, либо ни одного файла нет.
|
2026-02-23 19:37:05 +00:00
|
|
|
|
isDataExist, err := isFileExist(fileName)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("check data file is exist: %s", err)
|
|
|
|
|
|
}
|
2026-02-23 19:37:05 +00:00
|
|
|
|
if isDataExist {
|
|
|
|
|
|
file, allocatedPagesQty, err = openFile(fileName, PageSize)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, fmt.Errorf("open data file: %s", err)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-02-23 19:37:05 +00:00
|
|
|
|
// нет файла
|
|
|
|
|
|
file, err = os.OpenFile(fileName, os.O_CREATE|os.O_RDWR, filePerm)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tree := &Atree{
|
2026-05-10 00:59:47 +00:00
|
|
|
|
freelist: opt.FreeList,
|
2026-02-23 19:37:05 +00:00
|
|
|
|
file: file,
|
|
|
|
|
|
allocatedPagesQty: allocatedPagesQty,
|
|
|
|
|
|
pages: make(map[uint32]*_page),
|
|
|
|
|
|
pageWaits: make(map[uint32][]chan readResult),
|
|
|
|
|
|
readSignalCh: make(chan struct{}, 1),
|
|
|
|
|
|
writeSignalCh: make(chan struct{}, 1),
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return tree, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Atree) Run() {
|
2026-05-10 00:59:47 +00:00
|
|
|
|
//go s.pageWriter()
|
2026-02-10 14:02:11 +00:00
|
|
|
|
go s.pageReader()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// FIND
|
|
|
|
|
|
|
|
|
|
|
|
func (s *Atree) findDataPage(rootPageNo uint32, timestamp uint32) (uint32, []byte, error) {
|
|
|
|
|
|
indexPageNo := rootPageNo
|
|
|
|
|
|
for {
|
|
|
|
|
|
buf, err := s.fetchIndexPage(indexPageNo)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return 0, nil, fmt.Errorf("fetchIndexPage(%d): %s", indexPageNo, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
foundPageNo := findPageNo(buf, timestamp)
|
2026-02-23 19:37:05 +00:00
|
|
|
|
s.releasePage(indexPageNo)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
// fix
|
2026-05-17 12:58:46 +00:00
|
|
|
|
if buf[isLastLevelIdx] == 1 {
|
2026-02-10 14:02:11 +00:00
|
|
|
|
buf, err := s.fetchDataPage(foundPageNo)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return 0, nil, fmt.Errorf("fetchDataPage(%d): %s", foundPageNo, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return foundPageNo, buf, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
// вглубь
|
|
|
|
|
|
indexPageNo = foundPageNo
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
type PathLeg struct {
|
2026-02-10 14:02:11 +00:00
|
|
|
|
PageNo uint32
|
|
|
|
|
|
Data []byte
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
type PathToDataPage struct {
|
|
|
|
|
|
Legs []PathLeg
|
2026-02-10 14:02:11 +00:00
|
|
|
|
LastPageNo uint32
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
func (s *Atree) FindPathToLastPage(rootPageNo uint32) (_ PathToDataPage, err error) {
|
2026-02-10 14:02:11 +00:00
|
|
|
|
var (
|
|
|
|
|
|
pageNo = rootPageNo
|
2026-05-10 00:59:47 +00:00
|
|
|
|
legs []PathLeg
|
2026-02-10 14:02:11 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
|
var buf []byte
|
|
|
|
|
|
buf, err = s.fetchIndexPage(pageNo)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
err = fmt.Errorf("FetchIndexPage(%d): %s", pageNo, err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
legs = append(legs, PathLeg{
|
2026-02-10 14:02:11 +00:00
|
|
|
|
PageNo: pageNo,
|
|
|
|
|
|
Data: buf,
|
|
|
|
|
|
// childIdx не нужен
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
foundPageNo := getLastPageNo(buf)
|
|
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
// fix
|
2026-05-17 12:58:46 +00:00
|
|
|
|
if buf[isLastLevelIdx] == 1 {
|
2026-05-10 00:59:47 +00:00
|
|
|
|
return PathToDataPage{
|
2026-02-10 14:02:11 +00:00
|
|
|
|
Legs: legs,
|
|
|
|
|
|
LastPageNo: foundPageNo,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
// вглубь
|
|
|
|
|
|
pageNo = foundPageNo
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// APPEND DATA PAGE
|
|
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
//MetricID uint32
|
|
|
|
|
|
//Timestamp uint32
|
|
|
|
|
|
//Value float64
|
|
|
|
|
|
//RootPageNo uint32
|
|
|
|
|
|
//PrevPageNo uint32
|
|
|
|
|
|
|
2026-02-10 14:02:11 +00:00
|
|
|
|
type AppendDataPageReq struct {
|
|
|
|
|
|
Since uint32
|
|
|
|
|
|
TimestampsChunks [][]byte
|
|
|
|
|
|
TimestampsSize uint16
|
|
|
|
|
|
ValuesChunks [][]byte
|
|
|
|
|
|
ValuesSize uint16
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-23 19:37:05 +00:00
|
|
|
|
// type ChangedPage struct {
|
|
|
|
|
|
// PageNo uint32
|
|
|
|
|
|
// Data []byte
|
|
|
|
|
|
// IsReused bool
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
// AppendDataPage - метод не записує дані в data-файл, а лише змінює дані
|
|
|
|
|
|
// в page cache та freeList і повертає звіт що змінено.
|
|
|
|
|
|
// Цей звіт txlog має записати в transaction log і лише потім можна змінювати data файл.
|
|
|
|
|
|
// Є ідея - записати у index файли заглушки 255,255,255,255 замість номерів сторінок і зберегти зміщення.
|
|
|
|
|
|
// А потім одним викликом отримати із FreeList список вільних сторінок.
|
|
|
|
|
|
// Тому що є проблема із відновленням FreeList після збою, якщо з нього будуть паралельно
|
|
|
|
|
|
// забирати та добавляти номери сторінок інші потоки.
|
|
|
|
|
|
// Це буде працювати, якщо додавання в txlog і маніпуляції із freeList будуть відбуватись в одному потоці
|
2026-05-10 00:59:47 +00:00
|
|
|
|
// NotLinkedDataPage - це data сторінка із payload, але без встановленого prevPageNo та без розрахованого CRC32
|
|
|
|
|
|
type NotLinkedDataPage struct {
|
|
|
|
|
|
Since uint32
|
|
|
|
|
|
Data []byte
|
|
|
|
|
|
}
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
func (s NotLinkedDataPage) SetPrevPageNo(prevPageNo uint32) {
|
|
|
|
|
|
bin.PutUint32(s.Data[prevPageIdx:], prevPageNo)
|
|
|
|
|
|
bin.PutUint32(s.Data[crc32Idx:], calcChecksum(s.Data[:crc32Idx]))
|
|
|
|
|
|
}
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
type AppendDataPagesReq struct {
|
|
|
|
|
|
LastPageNo uint32
|
|
|
|
|
|
Legs []PathLeg
|
|
|
|
|
|
DataPages []NotLinkedDataPage
|
|
|
|
|
|
}
|
2026-02-23 19:37:05 +00:00
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
type Report struct {
|
|
|
|
|
|
NewRootPageNo uint32
|
|
|
|
|
|
LastPageNo uint32
|
|
|
|
|
|
Pages []PageToWrite
|
|
|
|
|
|
}
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
// Можливо atree не треба блокувати при читанні. Я можу створити копії index сторінок,
|
|
|
|
|
|
// а потім під 1 мутексом замінити в pages.
|
|
|
|
|
|
func (s *Atree) AppendDataPages(req AppendDataPagesReq) Report {
|
|
|
|
|
|
var (
|
|
|
|
|
|
//pagesToRelease []uint32
|
|
|
|
|
|
newRootPageNo uint32
|
|
|
|
|
|
lastPageNo = req.LastPageNo
|
|
|
|
|
|
pages []PageToWrite
|
|
|
|
|
|
legs = req.Legs
|
|
|
|
|
|
changed = make(map[uint32]PathLeg)
|
|
|
|
|
|
)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
for _, p := range req.DataPages {
|
|
|
|
|
|
newDataPageNo, isReused := s.allocPageNumber() // alloc only number
|
|
|
|
|
|
// pagesToRelease = append(pagesToRelease, newDataPage.PageNo)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
// set prevPageNo
|
|
|
|
|
|
p.SetPrevPageNo(lastPageNo)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
pages = append(pages, PageToWrite{
|
|
|
|
|
|
PageNo: newDataPageNo,
|
|
|
|
|
|
Data: p.Data,
|
|
|
|
|
|
IsReused: isReused,
|
|
|
|
|
|
})
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
// FIX - після додавання index page потрібно модифікувати path,
|
|
|
|
|
|
// або будувати щось типу дерева знизу вверх
|
|
|
|
|
|
|
|
|
|
|
|
if len(legs) > 0 {
|
|
|
|
|
|
newPageNo := newDataPageNo
|
|
|
|
|
|
lastIdx := len(legs) - 1
|
|
|
|
|
|
|
|
|
|
|
|
for legIdx := lastIdx; legIdx >= 0; legIdx-- {
|
|
|
|
|
|
leg := req.Legs[legIdx]
|
|
|
|
|
|
ok := appendPair(leg.Data, p.Since, newPageNo)
|
|
|
|
|
|
if ok {
|
|
|
|
|
|
// index FIX
|
|
|
|
|
|
// потрібно запам'ятати змінені сторінки, але їх можуть змінювати
|
|
|
|
|
|
// кілька ітерацій, тому додавати в Pages не можна.
|
|
|
|
|
|
// на індексній сторінці достатньо місця. Запис вставлено.
|
|
|
|
|
|
changed[leg.PageNo] = leg
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
// на індексній сторінці НЕ достатньо місця. Створюю нову.
|
|
|
|
|
|
newIndexPage := s.allocPage()
|
|
|
|
|
|
//pagesToRelease = append(pagesToRelease, newIndexPage.PageNo)
|
|
|
|
|
|
appendPair(newIndexPage.Data, p.Since, newPageNo)
|
|
|
|
|
|
// ставлю мітку що всі pageNo на сторінці - це data pageNo
|
|
|
|
|
|
// fix - єдина оптимізація від існування isDataPageNumbersIdx - getAllPages не завантажує data pages
|
|
|
|
|
|
// if legIdx == lastIdx {
|
|
|
|
|
|
// newIndexPage.Data[isDataPageNumbersIdx] = 1
|
2026-02-23 19:37:05 +00:00
|
|
|
|
// }
|
2026-05-10 00:59:47 +00:00
|
|
|
|
pages = append(pages, PageToWrite{
|
|
|
|
|
|
PageNo: newIndexPage.PageNo,
|
|
|
|
|
|
Data: newIndexPage.Data,
|
|
|
|
|
|
IsReused: newIndexPage.IsReused,
|
2026-02-23 19:37:05 +00:00
|
|
|
|
})
|
2026-05-10 00:59:47 +00:00
|
|
|
|
// замінюю крок в path на новий
|
|
|
|
|
|
legs[legIdx] = PathLeg{
|
|
|
|
|
|
PageNo: newIndexPage.PageNo,
|
|
|
|
|
|
Data: newIndexPage.Data,
|
|
|
|
|
|
}
|
|
|
|
|
|
//
|
|
|
|
|
|
newPageNo = newIndexPage.PageNo
|
|
|
|
|
|
|
|
|
|
|
|
if legIdx == 0 {
|
|
|
|
|
|
newRoot := s.allocPage()
|
|
|
|
|
|
//pagesToRelease = append(pagesToRelease, newRoot.PageNo)
|
|
|
|
|
|
appendPair(newRoot.Data, getSince(leg.Data), leg.PageNo) // old rootPageNo
|
|
|
|
|
|
appendPair(newRoot.Data, p.Since, newIndexPage.PageNo)
|
|
|
|
|
|
|
|
|
|
|
|
// Фиксирую новый root в REDO логе
|
|
|
|
|
|
pages = append(pages, PageToWrite{
|
|
|
|
|
|
PageNo: newRoot.PageNo,
|
|
|
|
|
|
Data: newRoot.Data,
|
|
|
|
|
|
IsReused: newRoot.IsReused,
|
|
|
|
|
|
})
|
|
|
|
|
|
newRootPageNo = newRoot.PageNo
|
|
|
|
|
|
// додаю в початок списку кроків нову root сторінку
|
|
|
|
|
|
legs = append([]PathLeg{
|
|
|
|
|
|
{
|
|
|
|
|
|
PageNo: newRoot.PageNo,
|
|
|
|
|
|
Data: newRoot.Data,
|
|
|
|
|
|
},
|
|
|
|
|
|
}, legs...)
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
2026-05-10 00:59:47 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
// індексну root сторінку створюю одразу для першої data сторінки,
|
|
|
|
|
|
// root data сторінки, як в B+Tree не буває.
|
|
|
|
|
|
newRoot := s.allocPage()
|
|
|
|
|
|
//pagesToRelease = append(pagesToRelease, newRoot.PageNo)
|
|
|
|
|
|
//newRoot.Data[isDataPageNumbersIdx] = 1
|
|
|
|
|
|
appendPair(newRoot.Data, p.Since, newDataPageNo)
|
|
|
|
|
|
|
|
|
|
|
|
pages = append(pages, PageToWrite{
|
|
|
|
|
|
PageNo: newRoot.PageNo,
|
|
|
|
|
|
Data: newRoot.Data,
|
|
|
|
|
|
IsReused: newRoot.IsReused,
|
|
|
|
|
|
})
|
|
|
|
|
|
newRootPageNo = newRoot.PageNo
|
|
|
|
|
|
// додаю в початок списку кроків нову root сторінку
|
|
|
|
|
|
legs = append([]PathLeg{
|
|
|
|
|
|
{
|
|
|
|
|
|
PageNo: newRoot.PageNo,
|
|
|
|
|
|
Data: newRoot.Data,
|
|
|
|
|
|
},
|
|
|
|
|
|
}, legs...)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
2026-05-10 00:59:47 +00:00
|
|
|
|
|
|
|
|
|
|
// На данний момен схема - наступна. Всі сторінки - data та index - зафіксовані в кеші.
|
|
|
|
|
|
// Отже запис на диск пройде максимально швидко. Після цього ReferenceCount кожної
|
|
|
|
|
|
// сторінки зменшиться на 1. Оскільки на метрику утримується XLock, сторінки мають
|
|
|
|
|
|
// ReferenceCount = 1 (немає інших читачів).
|
|
|
|
|
|
// for _, pageNo := range indexPagesToRelease {
|
|
|
|
|
|
// s.releasePage(pageNo)
|
2026-02-23 19:37:05 +00:00
|
|
|
|
// }
|
2026-05-10 00:59:47 +00:00
|
|
|
|
lastPageNo = newDataPageNo
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, leg := range changed {
|
|
|
|
|
|
// fix recalc checksum
|
|
|
|
|
|
pages = append(pages, PageToWrite{
|
|
|
|
|
|
PageNo: leg.PageNo,
|
|
|
|
|
|
Data: leg.Data,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return Report{
|
|
|
|
|
|
NewRootPageNo: newRootPageNo,
|
|
|
|
|
|
LastPageNo: lastPageNo,
|
|
|
|
|
|
Pages: pages,
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// DELETE
|
|
|
|
|
|
|
|
|
|
|
|
type Level struct {
|
|
|
|
|
|
PageNo uint32
|
|
|
|
|
|
PageData []byte
|
|
|
|
|
|
Idx int
|
|
|
|
|
|
ChildQty int
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
func (s *Atree) GetAllPages(rootPageNo uint32) (_ []uint32, err error) {
|
2026-02-10 14:02:11 +00:00
|
|
|
|
var (
|
2026-05-10 00:59:47 +00:00
|
|
|
|
pageNumbers []uint32
|
|
|
|
|
|
levels []*Level
|
2026-02-10 14:02:11 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
buf, err := s.fetchIndexPage(rootPageNo)
|
|
|
|
|
|
if err != nil {
|
2026-05-10 00:59:47 +00:00
|
|
|
|
return nil, fmt.Errorf("fetchIndexPage(%d): %s", rootPageNo, err)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
2026-05-10 00:59:47 +00:00
|
|
|
|
pageNumbers = append(pageNumbers, rootPageNo)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
// if buf[isDataPageNumbersIdx] == 1 {
|
|
|
|
|
|
// pageNumbers := listPageNumbers(buf)
|
|
|
|
|
|
// dataPages = append(dataPages, pageNumbers...)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
// s.releasePage(rootPageNo)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
// return PageLists{
|
|
|
|
|
|
// DataPages: dataPages,
|
|
|
|
|
|
// IndexPages: indexPages,
|
|
|
|
|
|
// }, nil
|
|
|
|
|
|
// }
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
|
|
|
|
|
levels = append(levels, &Level{
|
|
|
|
|
|
PageNo: rootPageNo,
|
|
|
|
|
|
PageData: buf,
|
|
|
|
|
|
Idx: 0,
|
|
|
|
|
|
ChildQty: bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]),
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
|
if len(levels) == 0 {
|
2026-05-10 00:59:47 +00:00
|
|
|
|
return pageNumbers, nil
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
lastIdx := len(levels) - 1
|
|
|
|
|
|
level := levels[lastIdx]
|
|
|
|
|
|
|
|
|
|
|
|
if level.Idx < level.ChildQty {
|
|
|
|
|
|
pageNo := getPageNo(level.PageData, level.Idx)
|
|
|
|
|
|
level.Idx++
|
|
|
|
|
|
|
|
|
|
|
|
var buf []byte
|
2026-05-10 00:59:47 +00:00
|
|
|
|
buf, err = s.fetchPage(pageNo)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
if err != nil {
|
2026-05-10 00:59:47 +00:00
|
|
|
|
return nil, fmt.Errorf("fetchPage(%d): %s", pageNo, err)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
}
|
2026-05-10 00:59:47 +00:00
|
|
|
|
pageNumbers = append(pageNumbers, pageNo)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
|
2026-05-10 00:59:47 +00:00
|
|
|
|
if buf[pageTypeIdx] == PageTypeData {
|
|
|
|
|
|
//pageNumbers := listPageNumbers(buf)
|
|
|
|
|
|
//dataPages = append(dataPages, pageNumbers...)
|
2026-02-23 19:37:05 +00:00
|
|
|
|
s.releasePage(pageNo)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
levels = append(levels, &Level{
|
|
|
|
|
|
PageNo: pageNo,
|
|
|
|
|
|
PageData: buf,
|
|
|
|
|
|
Idx: 0,
|
|
|
|
|
|
ChildQty: bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-02-23 19:37:05 +00:00
|
|
|
|
s.releasePage(level.PageNo)
|
2026-02-10 14:02:11 +00:00
|
|
|
|
levels = levels[:lastIdx]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|