2 Commits

Author SHA1 Message Date
5f753c3e93 fixed compare behaviour 2026-04-24 16:32:45 +06:00
9861e09246 fixed bag with alloc size in partition 2026-04-23 16:50:07 +06:00
3 changed files with 145 additions and 9 deletions

View File

@@ -1103,14 +1103,18 @@ func (j *JSONLFile) valuesMatch(a, b any, strictCompare bool) bool {
return a == b return a == b
} }
// Для строк - нечувствительное к регистру сравнение if a == b {
return true
}
// Для строк - нечувствительное к регистру сравнение (равенство, не подстрока)
if aStr, ok := a.(string); ok { if aStr, ok := a.(string); ok {
if bStr, ok := b.(string); ok { if bStr, ok := b.(string); ok {
return strings.Contains(strings.ToLower(aStr), strings.ToLower(bStr)) return matchStringByPattern(aStr, bStr, strictCompare)
} }
} }
return a == b return false
} }
// rewriteFile перезаписывает файл новыми данными // rewriteFile перезаписывает файл новыми данными

View File

@@ -88,6 +88,8 @@ func (db *LineDb) Init(force bool, initOptions *LineDbInitOptions) error {
if dbFolder == "" { if dbFolder == "" {
dbFolder = "linedb" dbFolder = "linedb"
} }
// Нормализуем и сохраняем фактический путь, чтобы партиции создавались в той же папке
db.initOptions.DBFolder = dbFolder
if err := os.MkdirAll(dbFolder, 0755); err != nil { if err := os.MkdirAll(dbFolder, 0755); err != nil {
return fmt.Errorf("failed to create database folder: %w", err) return fmt.Errorf("failed to create database folder: %w", err)
@@ -229,7 +231,13 @@ func (db *LineDb) seedLastIDPartitioned(dbFolder, baseName string) error {
if existing, ok := db.adapters[partName]; ok { if existing, ok := db.adapters[partName]; ok {
adapter = existing adapter = existing
} else { } else {
adapter = NewJSONLFile(path, "", JSONLFileOptions{CollectionName: partName}) // Партиции должны наследовать опции базовой коллекции (allocSize/encode/crypto/marshal/etc.)
opts := JSONLFileOptions{CollectionName: partName}
if baseOpts := db.getCollectionOptions(baseName); baseOpts != nil {
opts = *baseOpts
opts.CollectionName = partName
}
adapter = NewJSONLFile(path, opts.EncryptKeyForLineDb, opts)
if err := adapter.Init(false, LineDbAdapterOptions{}); err != nil { if err := adapter.Init(false, LineDbAdapterOptions{}); err != nil {
continue continue
} }
@@ -655,7 +663,7 @@ func (db *LineDb) ReadByFilter(filter any, collectionName string, options LineDb
if err != nil || (options.FailOnFailureIndexRead && !hit) { if err != nil || (options.FailOnFailureIndexRead && !hit) {
return nil, fmt.Errorf("index read failed: %w", err) return nil, fmt.Errorf("index read failed: %w", err)
} }
if hit && err == nil { if hit {
if db.cacheExternal != nil && !options.InTransaction { if db.cacheExternal != nil && !options.InTransaction {
db.cacheExternal.Set(db.generateCacheKey(filter, collectionName), result) db.cacheExternal.Set(db.generateCacheKey(filter, collectionName), result)
} }
@@ -686,7 +694,7 @@ func (db *LineDb) ReadByFilter(filter any, collectionName string, options LineDb
if err != nil || (options.FailOnFailureIndexRead && !hit) { if err != nil || (options.FailOnFailureIndexRead && !hit) {
return nil, fmt.Errorf("index read failed: %w", err) return nil, fmt.Errorf("index read failed: %w", err)
} }
if hit && err == nil { if hit {
if db.cacheExternal != nil && !options.InTransaction { if db.cacheExternal != nil && !options.InTransaction {
db.cacheExternal.Set(db.generateCacheKey(filter, collectionName), result) db.cacheExternal.Set(db.generateCacheKey(filter, collectionName), result)
} }
@@ -1148,7 +1156,7 @@ func (db *LineDb) getCollectionOptions(collectionName string) *JSONLFileOptions
} }
// isValueEmpty проверяет, считается ли значение "пустым" (nil или "" для string) — для обратной совместимости // isValueEmpty проверяет, считается ли значение "пустым" (nil или "" для string) — для обратной совместимости
func (db *LineDb) isValueEmpty(v any) bool { func (db *LineDb) IsValueEmpty(v any) bool {
if v == nil { if v == nil {
return true return true
} }
@@ -1386,7 +1394,13 @@ func (db *LineDb) getPartitionAdapter(data any, collectionName string) (*JSONLFi
if !exists { if !exists {
// Создаем новый адаптер для партиции // Создаем новый адаптер для партиции
filename := filepath.Join(db.initOptions.DBFolder, partitionName+".jsonl") filename := filepath.Join(db.initOptions.DBFolder, partitionName+".jsonl")
adapter = NewJSONLFile(filename, "", JSONLFileOptions{CollectionName: partitionName}) // Партиции должны наследовать опции базовой коллекции (allocSize/encode/crypto/marshal/etc.)
opts := JSONLFileOptions{CollectionName: partitionName}
if baseOpts := db.getCollectionOptions(collectionName); baseOpts != nil {
opts = *baseOpts
opts.CollectionName = partitionName
}
adapter = NewJSONLFile(filename, opts.EncryptKeyForLineDb, opts)
if err := adapter.Init(false, LineDbAdapterOptions{}); err != nil { if err := adapter.Init(false, LineDbAdapterOptions{}); err != nil {
return nil, fmt.Errorf("failed to init partition adapter: %w", err) return nil, fmt.Errorf("failed to init partition adapter: %w", err)
@@ -1460,7 +1474,7 @@ func (db *LineDb) valuesMatch(a, b any, strictCompare bool) bool {
// Сравнение строк // Сравнение строк
if aStr, ok := a.(string); ok { if aStr, ok := a.(string); ok {
if bStr, ok := b.(string); ok { if bStr, ok := b.(string); ok {
return strings.EqualFold(aStr, bStr) return matchStringByPattern(aStr, bStr, strictCompare)
} }
} }

118
pkg/linedb/string_match.go Normal file
View File

@@ -0,0 +1,118 @@
package linedb
import "strings"
type stringMatchMode int
const (
stringMatchEqual stringMatchMode = iota
stringMatchContains
stringMatchHasPrefix
stringMatchHasSuffix
)
func matchStringByPattern(value string, pattern string, strictCompare bool) bool {
mode, lit := parsePercentPattern(pattern)
if !strictCompare {
// For substring checks we use ToLower (close enough to EqualFold intent).
value = strings.ToLower(value)
lit = strings.ToLower(lit)
}
switch mode {
case stringMatchContains:
return strings.Contains(value, lit)
case stringMatchHasPrefix:
return strings.HasPrefix(value, lit)
case stringMatchHasSuffix:
return strings.HasSuffix(value, lit)
default:
if strictCompare {
return value == lit
}
return strings.EqualFold(value, lit)
}
}
// parsePercentPattern interprets unescaped % at the start/end of pattern:
// - %foo% => contains "foo"
// - foo% => hasPrefix "foo"
// - %foo => hasSuffix "foo"
// - foo => equal "foo"
// Escaping: \% means literal %, \\ means literal \ (only affects escape processing).
func parsePercentPattern(pattern string) (stringMatchMode, string) {
if pattern == "" {
return stringMatchEqual, ""
}
leading := pattern[0] == '%'
trailing := len(pattern) > 0 && pattern[len(pattern)-1] == '%' && !isEscapedAt(pattern, len(pattern)-1)
start := 0
end := len(pattern)
if leading {
start = 1
}
if trailing && end > start {
end--
}
lit := unescapePercents(pattern[start:end])
switch {
case leading && trailing:
return stringMatchContains, lit
case trailing:
return stringMatchHasPrefix, lit
case leading:
return stringMatchHasSuffix, lit
default:
return stringMatchEqual, unescapePercents(pattern)
}
}
func isEscapedAt(s string, idx int) bool {
// idx is escaped if preceded by an odd number of backslashes.
if idx <= 0 || idx >= len(s) {
return false
}
n := 0
for i := idx - 1; i >= 0 && s[i] == '\\'; i-- {
n++
}
return n%2 == 1
}
func unescapePercents(s string) string {
if s == "" {
return ""
}
var b strings.Builder
b.Grow(len(s))
esc := false
for i := 0; i < len(s); i++ {
ch := s[i]
if esc {
// Only unescape % and \; keep backslash for other chars.
if ch == '%' || ch == '\\' {
b.WriteByte(ch)
} else {
b.WriteByte('\\')
b.WriteByte(ch)
}
esc = false
continue
}
if ch == '\\' {
esc = true
continue
}
b.WriteByte(ch)
}
if esc {
// dangling backslash
b.WriteByte('\\')
}
return b.String()
}