before refactor index store to complex file-line pattern

This commit is contained in:
2026-03-12 16:13:44 +06:00
parent 491ccbea89
commit 8ba956d8c5
21 changed files with 7804 additions and 57 deletions

View File

@@ -253,8 +253,10 @@ func NewJSONLFile(filename string, cypherKey string, options JSONLFileOptions) *
// Init инициализирует файл
func (j *JSONLFile) Init(force bool, options LineDbAdapterOptions) error {
j.mutex.Lock()
defer j.mutex.Unlock()
if !options.InTransaction {
j.mutex.Lock()
defer j.mutex.Unlock()
}
if j.initialized && !force {
return nil
@@ -350,8 +352,10 @@ func (j *JSONLFile) normalizeExistingFile() error {
// Read читает все записи из файла
func (j *JSONLFile) Read(options LineDbAdapterOptions) ([]any, error) {
j.mutex.RLock()
defer j.mutex.RUnlock()
if !options.InTransaction {
j.mutex.RLock()
defer j.mutex.RUnlock()
}
if !j.initialized {
return nil, fmt.Errorf("file not initialized")
@@ -398,10 +402,285 @@ func (j *JSONLFile) Read(options LineDbAdapterOptions) ([]any, error) {
return records, scanner.Err()
}
// ReadByLineIndexes читает записи по номерам строк (0-based) с использованием random access.
// Ожидается, что файл нормализован и каждая строка имеет длину allocSize байт (включая \n),
// как это обеспечивает Init/normalizeExistingFile/rewriteFile.
func (j *JSONLFile) ReadByLineIndexes(indexes []int, options LineDbAdapterOptions) ([]any, error) {
// Внутри транзакций (options.InTransaction == true) блокировка внешним кодом
// уже обеспечена, повторный лок мог бы привести к дедлоку.
if !options.InTransaction {
j.mutex.RLock()
defer j.mutex.RUnlock()
}
if !j.initialized {
return nil, fmt.Errorf("file not initialized")
}
if len(indexes) == 0 {
return []any{}, nil
}
// Копируем, сортируем и убираем дубликаты
sorted := make([]int, len(indexes))
copy(sorted, indexes)
sort.Ints(sorted)
uniq := sorted[:0]
prev := -1
for _, v := range sorted {
if v < 0 {
continue
}
if v != prev {
uniq = append(uniq, v)
prev = v
}
}
file, err := os.Open(j.filename)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
var records []any
buf := make([]byte, j.allocSize)
for _, lineIndex := range uniq {
offset := int64(lineIndex) * int64(j.allocSize)
if _, err := file.Seek(offset, io.SeekStart); err != nil {
return nil, fmt.Errorf("failed to seek to offset %d: %w", offset, err)
}
n, err := io.ReadFull(file, buf)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return nil, fmt.Errorf("failed to read line at index %d: %w", lineIndex, err)
}
if n <= 0 {
continue
}
line := string(buf[:n])
line = strings.TrimRight(line, "\n")
line = strings.TrimRight(line, " ")
if strings.TrimSpace(line) == "" {
continue
}
// Расшифровываем если нужно (только cypherKey; Encode обрабатывается в jsonUnmarshal)
if j.cypherKey != "" && !j.options.Encode {
decoded, err := base64.StdEncoding.DecodeString(line)
if err != nil {
if j.options.SkipInvalidLines {
continue
}
return nil, fmt.Errorf("failed to decode base64: %w", err)
}
line = string(decoded)
}
var record any
if err := j.jsonUnmarshal([]byte(line), &record); err != nil {
if j.options.SkipInvalidLines {
continue
}
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
}
records = append(records, record)
}
return records, nil
}
// ReadByLineIndexesWithPositions как ReadByLineIndexes, но возвращает пары (record, lineIndex).
// Нужно для точечного Update, когда требуется знать позицию каждой записи.
func (j *JSONLFile) ReadByLineIndexesWithPositions(indexes []int, options LineDbAdapterOptions) ([]any, []int, error) {
if !options.InTransaction {
j.mutex.RLock()
defer j.mutex.RUnlock()
}
if !j.initialized {
return nil, nil, fmt.Errorf("file not initialized")
}
if len(indexes) == 0 {
return []any{}, []int{}, nil
}
sorted := make([]int, len(indexes))
copy(sorted, indexes)
sort.Ints(sorted)
uniq := sorted[:0]
prev := -1
for _, v := range sorted {
if v < 0 {
continue
}
if v != prev {
uniq = append(uniq, v)
prev = v
}
}
file, err := os.Open(j.filename)
if err != nil {
return nil, nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()
var records []any
var positions []int
buf := make([]byte, j.allocSize)
for _, lineIndex := range uniq {
offset := int64(lineIndex) * int64(j.allocSize)
if _, err := file.Seek(offset, io.SeekStart); err != nil {
return nil, nil, fmt.Errorf("failed to seek to offset %d: %w", offset, err)
}
n, err := io.ReadFull(file, buf)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return nil, nil, fmt.Errorf("failed to read line at index %d: %w", lineIndex, err)
}
if n <= 0 {
continue
}
line := string(buf[:n])
line = strings.TrimRight(line, "\n")
line = strings.TrimRight(line, " ")
if strings.TrimSpace(line) == "" {
continue
}
if j.cypherKey != "" && !j.options.Encode {
decoded, err := base64.StdEncoding.DecodeString(line)
if err != nil {
if j.options.SkipInvalidLines {
continue
}
return nil, nil, fmt.Errorf("failed to decode base64: %w", err)
}
line = string(decoded)
}
var record any
if err := j.jsonUnmarshal([]byte(line), &record); err != nil {
if j.options.SkipInvalidLines {
continue
}
return nil, nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
}
records = append(records, record)
positions = append(positions, lineIndex)
}
return records, positions, nil
}
// WriteAtLineIndexes точечно записывает записи по заданным номерам строк (0-based).
// records и lineIndexes должны быть одинаковой длины и в одном порядке.
func (j *JSONLFile) WriteAtLineIndexes(records []any, lineIndexes []int, options LineDbAdapterOptions) error {
if !options.InTransaction {
j.mutex.Lock()
defer j.mutex.Unlock()
}
if !j.initialized {
return fmt.Errorf("file not initialized")
}
if len(records) != len(lineIndexes) {
return fmt.Errorf("records and lineIndexes length mismatch")
}
if len(records) == 0 {
return nil
}
file, err := os.OpenFile(j.filename, os.O_RDWR, 0644)
if err != nil {
return fmt.Errorf("failed to open file for write: %w", err)
}
defer file.Close()
maxLineLen := j.allocSize - 1
if maxLineLen < 1 {
maxLineLen = 1
}
for i, record := range records {
lineIndex := lineIndexes[i]
jsonData, err := j.jsonMarshal(record)
if err != nil {
return fmt.Errorf("marshal record at index %d: %w", lineIndex, err)
}
line := string(jsonData)
if j.cypherKey != "" && !j.options.Encode {
line = base64.StdEncoding.EncodeToString([]byte(line))
}
if len(line) > maxLineLen {
return fmt.Errorf("record at line %d size %d exceeds allocSize-1 (%d)", lineIndex, len(line), maxLineLen)
}
if len(line) < maxLineLen {
line += strings.Repeat(" ", maxLineLen-len(line))
}
line += "\n"
offset := int64(lineIndex) * int64(j.allocSize)
if _, err := file.Seek(offset, io.SeekStart); err != nil {
return fmt.Errorf("seek to %d: %w", offset, err)
}
if _, err := file.WriteString(line); err != nil {
return fmt.Errorf("write at line %d: %w", lineIndex, err)
}
}
return nil
}
// BlankLinesAtPositions затирает строки пробелами (allocSize-1 + \n). При чтении они пропускаются.
func (j *JSONLFile) BlankLinesAtPositions(lineIndexes []int, options LineDbAdapterOptions) error {
if !options.InTransaction {
j.mutex.Lock()
defer j.mutex.Unlock()
}
if !j.initialized {
return fmt.Errorf("file not initialized")
}
if len(lineIndexes) == 0 {
return nil
}
file, err := os.OpenFile(j.filename, os.O_RDWR, 0644)
if err != nil {
return fmt.Errorf("failed to open file for blank: %w", err)
}
defer file.Close()
blank := strings.Repeat(" ", j.allocSize-1) + "\n"
for _, lineIndex := range lineIndexes {
offset := int64(lineIndex) * int64(j.allocSize)
if _, err := file.Seek(offset, io.SeekStart); err != nil {
return fmt.Errorf("seek to %d: %w", offset, err)
}
if _, err := file.WriteString(blank); err != nil {
return fmt.Errorf("write blank at line %d: %w", lineIndex, err)
}
}
return nil
}
// LineCount возвращает число строк в файле (fileSize / allocSize).
// Используется для точечного индексирования после Write.
func (j *JSONLFile) LineCount() (int, error) {
info, err := os.Stat(j.filename)
if err != nil {
if os.IsNotExist(err) {
return 0, nil
}
return 0, err
}
return int(info.Size()) / j.allocSize, nil
}
// Write записывает данные в файл
func (j *JSONLFile) Write(data any, options LineDbAdapterOptions) error {
j.mutex.Lock()
defer j.mutex.Unlock()
if !options.InTransaction {
j.mutex.Lock()
defer j.mutex.Unlock()
}
if !j.initialized {
return fmt.Errorf("file not initialized")