before refactor index store to complex file-line pattern
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user