before merge to main
This commit is contained in:
1
pkg/linedb/Untitled
Normal file
1
pkg/linedb/Untitled
Normal file
@@ -0,0 +1 @@
|
||||
д
|
||||
@@ -2,11 +2,17 @@ package linedb
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -23,6 +29,151 @@ func defaultJSONUnmarshal(data []byte, v any) error {
|
||||
return goccyjson.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// makeKeyOrderMarshal создаёт JSONMarshal с заданным порядком ключей
|
||||
func makeKeyOrderMarshal(keyOrder []KeyOrder) func(any) ([]byte, error) {
|
||||
return func(v any) ([]byte, error) {
|
||||
if m, ok := v.(map[string]any); ok {
|
||||
return marshalMapSorted(m, keyOrder)
|
||||
}
|
||||
return goccyjson.Marshal(v)
|
||||
}
|
||||
}
|
||||
|
||||
func marshalMapSorted(m map[string]any, keyOrder []KeyOrder) ([]byte, error) {
|
||||
orderMap := make(map[string]int)
|
||||
for _, ko := range keyOrder {
|
||||
orderMap[ko.Key] = ko.Order
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
oi, hasI := orderMap[keys[i]]
|
||||
oj, hasJ := orderMap[keys[j]]
|
||||
group := func(o int, has bool) int {
|
||||
if !has {
|
||||
return 1
|
||||
}
|
||||
if o >= 0 {
|
||||
return 0
|
||||
}
|
||||
return 2
|
||||
}
|
||||
gi, gj := group(oi, hasI), group(oj, hasJ)
|
||||
if gi != gj {
|
||||
return gi < gj
|
||||
}
|
||||
switch gi {
|
||||
case 0:
|
||||
return oi < oj
|
||||
case 1:
|
||||
return keys[i] < keys[j]
|
||||
default:
|
||||
return oi < oj
|
||||
}
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('{')
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
keyEscaped, _ := goccyjson.Marshal(k)
|
||||
buf.Write(keyEscaped)
|
||||
buf.WriteByte(':')
|
||||
val := m[k]
|
||||
if nested, ok := val.(map[string]any); ok {
|
||||
valBytes, err := marshalMapSorted(nested, keyOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf.Write(valBytes)
|
||||
} else {
|
||||
valBytes, err := goccyjson.Marshal(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf.Write(valBytes)
|
||||
}
|
||||
}
|
||||
buf.WriteByte('}')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// encodeKeyBytes превращает строку ключа в 32 байта (SHA256) для AES-256
|
||||
func encodeKeyBytes(keyStr string) []byte {
|
||||
h := sha256.Sum256([]byte(keyStr))
|
||||
return h[:]
|
||||
}
|
||||
|
||||
// aesGCMEncrypt — встроенное AES-256-GCM шифрование
|
||||
func aesGCMEncrypt(plaintext, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aesgcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce := make([]byte, aesgcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aesgcm.Seal(nonce, nonce, plaintext, nil), nil
|
||||
}
|
||||
|
||||
// aesGCMDecrypt — встроенная расшифровка AES-256-GCM
|
||||
func aesGCMDecrypt(ciphertext, key []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aesgcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonceSize := aesgcm.NonceSize()
|
||||
if len(ciphertext) < nonceSize {
|
||||
return nil, fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||||
return aesgcm.Open(nil, nonce, ciphertext, nil)
|
||||
}
|
||||
|
||||
// makeEncodedMarshal оборачивает marshal в шифрование: marshal -> encrypt -> base64
|
||||
func makeEncodedMarshal(marshal func(any) ([]byte, error), encFn func([]byte, []byte) ([]byte, error), key []byte) func(any) ([]byte, error) {
|
||||
return func(v any) ([]byte, error) {
|
||||
jsonData, err := marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encrypted, err := encFn(jsonData, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(base64.StdEncoding.EncodeToString(encrypted)), nil
|
||||
}
|
||||
}
|
||||
|
||||
// makeEncodedUnmarshal оборачивает unmarshal в дешифрование: base64 -> decrypt -> unmarshal
|
||||
func makeEncodedUnmarshal(unmarshal func([]byte, any) error, decFn func([]byte, []byte) ([]byte, error), key []byte) func([]byte, any) error {
|
||||
return func(data []byte, v any) error {
|
||||
encrypted, err := base64.StdEncoding.DecodeString(string(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plaintext, err := decFn(encrypted, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshal(plaintext, v)
|
||||
}
|
||||
}
|
||||
|
||||
// JSONLFile представляет адаптер для работы с JSONL файлами
|
||||
type JSONLFile struct {
|
||||
filename string
|
||||
@@ -61,14 +212,31 @@ func NewJSONLFile(filename string, cypherKey string, options JSONLFileOptions) *
|
||||
jsonMarshal := defaultJSONMarshal
|
||||
jsonUnmarshal := defaultJSONUnmarshal
|
||||
|
||||
// Используем пользовательские функции если они предоставлены
|
||||
if options.JSONMarshal != nil {
|
||||
jsonMarshal = options.JSONMarshal
|
||||
} else if len(options.KeyOrder) > 0 {
|
||||
jsonMarshal = makeKeyOrderMarshal(options.KeyOrder)
|
||||
}
|
||||
|
||||
if options.JSONUnmarshal != nil {
|
||||
jsonUnmarshal = options.JSONUnmarshal
|
||||
}
|
||||
|
||||
// Encode: оборачиваем marshal/unmarshal в шифрование
|
||||
if options.Encode && options.EncodeKey != "" {
|
||||
key := encodeKeyBytes(options.EncodeKey)
|
||||
encFn := options.EncryptFn
|
||||
decFn := options.DecryptFn
|
||||
if encFn == nil {
|
||||
encFn = aesGCMEncrypt
|
||||
}
|
||||
if decFn == nil {
|
||||
decFn = aesGCMDecrypt
|
||||
}
|
||||
jsonMarshal = makeEncodedMarshal(jsonMarshal, encFn, key)
|
||||
jsonUnmarshal = makeEncodedUnmarshal(jsonUnmarshal, decFn, key)
|
||||
}
|
||||
|
||||
return &JSONLFile{
|
||||
filename: filename,
|
||||
cypherKey: cypherKey,
|
||||
@@ -99,18 +267,87 @@ func (j *JSONLFile) Init(force bool, options LineDbAdapterOptions) error {
|
||||
}
|
||||
|
||||
// Создаем файл если не существует
|
||||
if _, err := os.Stat(j.filename); os.IsNotExist(err) {
|
||||
if st, err := os.Stat(j.filename); os.IsNotExist(err) {
|
||||
file, err := os.Create(j.filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
file.Close()
|
||||
} else if err == nil && st.Size() > 0 {
|
||||
// Файл существует и не пустой — проверяем и нормализуем записи
|
||||
if err := j.normalizeExistingFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
j.initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeExistingFile проверяет размеры записей и приводит к allocSize-1
|
||||
func (j *JSONLFile) normalizeExistingFile() error {
|
||||
data, err := os.ReadFile(j.filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
lines := strings.Split(strings.TrimSuffix(string(data), "\n"), "\n")
|
||||
var nonEmpty []string
|
||||
for _, l := range lines {
|
||||
if l == "" {
|
||||
continue
|
||||
}
|
||||
nonEmpty = append(nonEmpty, l)
|
||||
}
|
||||
if len(nonEmpty) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Максимальная длина среди записей
|
||||
maxLen := 0
|
||||
for _, l := range nonEmpty {
|
||||
if len(l) > maxLen {
|
||||
maxLen = len(l)
|
||||
}
|
||||
}
|
||||
|
||||
// Приводим короткие к maxLen (добавляем пробелы)
|
||||
for i, l := range nonEmpty {
|
||||
if len(l) < maxLen {
|
||||
nonEmpty[i] = l + strings.Repeat(" ", maxLen-len(l))
|
||||
}
|
||||
}
|
||||
|
||||
targetLen := j.allocSize - 1
|
||||
if targetLen < 1 {
|
||||
targetLen = 1
|
||||
}
|
||||
|
||||
if targetLen < maxLen {
|
||||
// Уменьшаем: можно только если данные (без trailing spaces) помещаются
|
||||
for i := range nonEmpty {
|
||||
trimmed := strings.TrimRight(nonEmpty[i], " ")
|
||||
if len(trimmed) > targetLen {
|
||||
return fmt.Errorf("init failed: record data size %d exceeds configured allocSize-1 (%d), cannot reduce without data loss",
|
||||
len(trimmed), targetLen)
|
||||
}
|
||||
nonEmpty[i] = trimmed + strings.Repeat(" ", targetLen-len(trimmed))
|
||||
}
|
||||
} else if targetLen > maxLen {
|
||||
// Расширяем
|
||||
for i := range nonEmpty {
|
||||
nonEmpty[i] = nonEmpty[i] + strings.Repeat(" ", targetLen-len(nonEmpty[i]))
|
||||
}
|
||||
}
|
||||
|
||||
// Перезаписываем файл
|
||||
var buf bytes.Buffer
|
||||
for _, l := range nonEmpty {
|
||||
buf.WriteString(l)
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
return os.WriteFile(j.filename, buf.Bytes(), 0644)
|
||||
}
|
||||
|
||||
// Read читает все записи из файла
|
||||
func (j *JSONLFile) Read(options LineDbAdapterOptions) ([]any, error) {
|
||||
j.mutex.RLock()
|
||||
@@ -135,8 +372,8 @@ func (j *JSONLFile) Read(options LineDbAdapterOptions) ([]any, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Расшифровываем если нужно
|
||||
if j.cypherKey != "" {
|
||||
// Расшифровываем если нужно (только cypherKey; Encode обрабатывается в jsonUnmarshal)
|
||||
if j.cypherKey != "" && !j.options.Encode {
|
||||
decoded, err := base64.StdEncoding.DecodeString(line)
|
||||
if err != nil {
|
||||
if j.options.SkipInvalidLines {
|
||||
@@ -189,14 +426,18 @@ func (j *JSONLFile) Write(data any, options LineDbAdapterOptions) error {
|
||||
|
||||
line := string(jsonData)
|
||||
|
||||
// Шифруем если нужно
|
||||
if j.cypherKey != "" {
|
||||
// Шифруем если нужно (только cypherKey; Encode уже в jsonMarshal)
|
||||
if j.cypherKey != "" && !j.options.Encode {
|
||||
line = base64.StdEncoding.EncodeToString([]byte(line))
|
||||
}
|
||||
|
||||
// Дополняем до allocSize
|
||||
if len(line) < j.allocSize {
|
||||
line += strings.Repeat(" ", j.allocSize-len(line)-1)
|
||||
maxLineLen := j.allocSize - 1
|
||||
if len(line) > maxLineLen {
|
||||
return fmt.Errorf("record size %d exceeds allocSize-1 (%d)", len(line), maxLineLen)
|
||||
}
|
||||
// Дополняем до allocSize-1
|
||||
if len(line) < maxLineLen {
|
||||
line += strings.Repeat(" ", maxLineLen-len(line))
|
||||
}
|
||||
|
||||
if _, err := file.WriteString(line + "\n"); err != nil {
|
||||
@@ -406,14 +647,18 @@ func (j *JSONLFile) rewriteFile(records []any) error {
|
||||
|
||||
line := string(jsonData)
|
||||
|
||||
// Шифруем если нужно
|
||||
if j.cypherKey != "" {
|
||||
// Шифруем если нужно (только cypherKey; Encode уже в jsonMarshal)
|
||||
if j.cypherKey != "" && !j.options.Encode {
|
||||
line = base64.StdEncoding.EncodeToString([]byte(line))
|
||||
}
|
||||
|
||||
// Дополняем до allocSize
|
||||
if len(line) < j.allocSize {
|
||||
line += strings.Repeat(" ", j.allocSize-len(line)-1)
|
||||
maxLineLen := j.allocSize - 1
|
||||
if len(line) > maxLineLen {
|
||||
return fmt.Errorf("record size %d exceeds allocSize-1 (%d)", len(line), maxLineLen)
|
||||
}
|
||||
// Дополняем до allocSize-1
|
||||
if len(line) < maxLineLen {
|
||||
line += strings.Repeat(" ", maxLineLen-len(line))
|
||||
}
|
||||
|
||||
if _, err := file.WriteString(line + "\n"); err != nil {
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
goccyjson "github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
// LineDb представляет основную базу данных
|
||||
@@ -159,9 +161,9 @@ func (db *LineDb) Insert(data any, collectionName string, options LineDbAdapterO
|
||||
resultDataArray := make([]any, 0, len(dataArray))
|
||||
|
||||
for _, item := range dataArray {
|
||||
itemMap, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid data format")
|
||||
itemMap, err := db.toMap(item)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid data format: %w", err)
|
||||
}
|
||||
|
||||
// Генерируем ID если отсутствует
|
||||
@@ -220,7 +222,12 @@ func (db *LineDb) Insert(data any, collectionName string, options LineDbAdapterO
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем уникальность полей из UniqueFields
|
||||
// Проверяем обязательные поля (required)
|
||||
if err := db.checkRequiredFieldsInsert(itemMap, collectionName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Проверяем уникальность полей
|
||||
if err := db.checkUniqueFieldsInsert(itemMap, collectionName, resultDataArray, options); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -292,24 +299,32 @@ func (db *LineDb) Update(data any, collectionName string, filter any, options Li
|
||||
collectionName = db.getFirstCollection()
|
||||
}
|
||||
|
||||
// Конвертируем data в map (struct или map)
|
||||
dataMap, err := db.toMap(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid update data format: %w", err)
|
||||
}
|
||||
|
||||
// Проверяем конфликт ID
|
||||
if dataMap, ok := data.(map[string]any); ok {
|
||||
if filterMap, ok := filter.(map[string]any); ok {
|
||||
if dataMap["id"] != nil && filterMap["id"] != nil {
|
||||
if !db.compareIDs(dataMap["id"], filterMap["id"]) {
|
||||
return nil, fmt.Errorf("you can not update record id with filter by another id. Use delete and insert instead")
|
||||
}
|
||||
if filterMap, ok := filter.(map[string]any); ok {
|
||||
if dataMap["id"] != nil && filterMap["id"] != nil {
|
||||
if !db.compareIDs(dataMap["id"], filterMap["id"]) {
|
||||
return nil, fmt.Errorf("you can not update record id with filter by another id. Use delete and insert instead")
|
||||
}
|
||||
}
|
||||
// Проверяем уникальность полей из UniqueFields
|
||||
if err := db.checkUniqueFieldsUpdate(dataMap, filter, collectionName, options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Проверяем обязательные поля (required)
|
||||
if err := db.checkRequiredFieldsUpdate(dataMap, collectionName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Проверяем уникальность полей
|
||||
if err := db.checkUniqueFieldsUpdate(dataMap, filter, collectionName, options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем партиционирование
|
||||
if db.isCollectionPartitioned(collectionName) {
|
||||
return db.updatePartitioned(data, collectionName, filter, options)
|
||||
return db.updatePartitioned(dataMap, collectionName, filter, options)
|
||||
}
|
||||
|
||||
// Обычное обновление
|
||||
@@ -318,7 +333,7 @@ func (db *LineDb) Update(data any, collectionName string, filter any, options Li
|
||||
return nil, fmt.Errorf("collection %s not found", collectionName)
|
||||
}
|
||||
|
||||
return adapter.Update(data, filter, options)
|
||||
return adapter.Update(dataMap, filter, options)
|
||||
}
|
||||
|
||||
// Delete удаляет записи из коллекции
|
||||
@@ -499,7 +514,7 @@ func (db *LineDb) getCollectionOptions(collectionName string) *JSONLFileOptions
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValueEmpty проверяет, считается ли значение "пустым" (пропускаем проверку уникальности для пустых)
|
||||
// isValueEmpty проверяет, считается ли значение "пустым" (nil или "" для string) — для обратной совместимости
|
||||
func (db *LineDb) isValueEmpty(v any) bool {
|
||||
if v == nil {
|
||||
return true
|
||||
@@ -510,20 +525,119 @@ func (db *LineDb) isValueEmpty(v any) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// isEmptyByMode проверяет, пусто ли значение по заданному режиму
|
||||
func (db *LineDb) isEmptyByMode(m map[string]any, key string, mode EmptyValueMode) bool {
|
||||
if mode == 0 {
|
||||
mode = DefaultEmptyModeForUnique
|
||||
}
|
||||
v, exists := m[key]
|
||||
if mode&EmptyModeAbsentKey != 0 && !exists {
|
||||
return true
|
||||
}
|
||||
if mode&EmptyModeNil != 0 && v == nil {
|
||||
return true
|
||||
}
|
||||
if mode&EmptyModeZeroValue != 0 {
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
return val == ""
|
||||
case int:
|
||||
return val == 0
|
||||
case int64:
|
||||
return val == 0
|
||||
case float64:
|
||||
return val == 0
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getUniqueFieldConstraints возвращает поля с unique и их EmptyMode (из FieldConstraints или UniqueFields)
|
||||
func (db *LineDb) getUniqueFieldConstraints(collectionName string) []FieldConstraint {
|
||||
opts := db.getCollectionOptions(collectionName)
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
if len(opts.FieldConstraints) > 0 {
|
||||
var out []FieldConstraint
|
||||
for _, fc := range opts.FieldConstraints {
|
||||
if fc.Unique {
|
||||
mode := fc.EmptyMode
|
||||
if mode == 0 {
|
||||
mode = DefaultEmptyModeForUnique
|
||||
}
|
||||
out = append(out, FieldConstraint{Name: fc.Name, Unique: true, EmptyMode: mode})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
// Legacy UniqueFields
|
||||
var out []FieldConstraint
|
||||
for _, name := range opts.UniqueFields {
|
||||
out = append(out, FieldConstraint{Name: name, Unique: true, EmptyMode: DefaultEmptyModeForUnique})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// getRequiredFieldConstraints возвращает required поля и их EmptyMode
|
||||
func (db *LineDb) getRequiredFieldConstraints(collectionName string) []FieldConstraint {
|
||||
opts := db.getCollectionOptions(collectionName)
|
||||
if opts == nil {
|
||||
return nil
|
||||
}
|
||||
var out []FieldConstraint
|
||||
for _, fc := range opts.FieldConstraints {
|
||||
if fc.Required {
|
||||
mode := fc.EmptyMode
|
||||
if mode == 0 {
|
||||
mode = DefaultEmptyModeForRequired
|
||||
}
|
||||
out = append(out, FieldConstraint{Name: fc.Name, Required: true, EmptyMode: mode})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// checkRequiredFieldsInsert проверяет обязательные поля при вставке
|
||||
func (db *LineDb) checkRequiredFieldsInsert(itemMap map[string]any, collectionName string) error {
|
||||
required := db.getRequiredFieldConstraints(collectionName)
|
||||
for _, fc := range required {
|
||||
if db.isEmptyByMode(itemMap, fc.Name, fc.EmptyMode) {
|
||||
return fmt.Errorf("required field %q is empty in collection %q", fc.Name, collectionName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkRequiredFieldsUpdate проверяет, что при Update не устанавливаются пустые значения в required полях
|
||||
func (db *LineDb) checkRequiredFieldsUpdate(data map[string]any, collectionName string) error {
|
||||
required := db.getRequiredFieldConstraints(collectionName)
|
||||
for _, fc := range required {
|
||||
if _, inData := data[fc.Name]; !inData {
|
||||
continue // поле не обновляется — не проверяем
|
||||
}
|
||||
if db.isEmptyByMode(data, fc.Name, fc.EmptyMode) {
|
||||
return fmt.Errorf("required field %q cannot be set to empty in collection %q", fc.Name, collectionName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkUniqueFieldsInsert проверяет уникальность полей при вставке
|
||||
func (db *LineDb) checkUniqueFieldsInsert(itemMap map[string]any, collectionName string, resultDataArray []any, options LineDbAdapterOptions) error {
|
||||
if options.SkipCheckExistingForWrite {
|
||||
return nil
|
||||
}
|
||||
opts := db.getCollectionOptions(collectionName)
|
||||
if opts == nil || len(opts.UniqueFields) == 0 {
|
||||
uniqueFields := db.getUniqueFieldConstraints(collectionName)
|
||||
if len(uniqueFields) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, fieldName := range opts.UniqueFields {
|
||||
value := itemMap[fieldName]
|
||||
if db.isValueEmpty(value) {
|
||||
for _, fc := range uniqueFields {
|
||||
fieldName := fc.Name
|
||||
if db.isEmptyByMode(itemMap, fieldName, fc.EmptyMode) {
|
||||
continue
|
||||
}
|
||||
value := itemMap[fieldName]
|
||||
// Проверяем в batch (уже добавляемые записи)
|
||||
for _, resultItem := range resultDataArray {
|
||||
if resultMap, ok := resultItem.(map[string]any); ok {
|
||||
@@ -552,8 +666,8 @@ func (db *LineDb) checkUniqueFieldsUpdate(data map[string]any, filter any, colle
|
||||
if options.SkipCheckExistingForWrite {
|
||||
return nil
|
||||
}
|
||||
opts := db.getCollectionOptions(collectionName)
|
||||
if opts == nil || len(opts.UniqueFields) == 0 {
|
||||
uniqueFields := db.getUniqueFieldConstraints(collectionName)
|
||||
if len(uniqueFields) == 0 {
|
||||
return nil
|
||||
}
|
||||
recordsToUpdate, err := db.ReadByFilter(filter, collectionName, LineDbAdapterOptions{InTransaction: true})
|
||||
@@ -566,9 +680,10 @@ func (db *LineDb) checkUniqueFieldsUpdate(data map[string]any, filter any, colle
|
||||
updatingIDs[m["id"]] = true
|
||||
}
|
||||
}
|
||||
for _, fieldName := range opts.UniqueFields {
|
||||
for _, fc := range uniqueFields {
|
||||
fieldName := fc.Name
|
||||
value, inData := data[fieldName]
|
||||
if !inData || db.isValueEmpty(value) {
|
||||
if !inData || db.isEmptyByMode(data, fieldName, fc.EmptyMode) {
|
||||
continue
|
||||
}
|
||||
existing, err := db.ReadByFilter(map[string]any{fieldName: value}, collectionName, LineDbAdapterOptions{InTransaction: true})
|
||||
@@ -707,6 +822,19 @@ func (db *LineDb) toNumber(value any) (float64, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// toMap конвертирует struct или map в map[string]any (для Insert/Update)
|
||||
func (db *LineDb) toMap(v any) (map[string]any, error) {
|
||||
if m, ok := v.(map[string]any); ok {
|
||||
return m, nil
|
||||
}
|
||||
data, err := goccyjson.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var result map[string]any
|
||||
return result, goccyjson.Unmarshal(data, &result)
|
||||
}
|
||||
|
||||
func (db *LineDb) normalizeDataArray(data any) []any {
|
||||
switch v := data.(type) {
|
||||
case []any:
|
||||
|
||||
@@ -54,13 +54,50 @@ type LineDbInitOptions struct {
|
||||
Partitions []PartitionCollection `json:"partitions,omitempty"`
|
||||
}
|
||||
|
||||
// EmptyValueMode определяет, что считать "пустым" при проверке required/unique
|
||||
type EmptyValueMode uint
|
||||
|
||||
const (
|
||||
EmptyModeAbsentKey EmptyValueMode = 1 << iota // ключ отсутствует в записи
|
||||
EmptyModeNil // значение nil
|
||||
EmptyModeZeroValue // "" для string, 0 для int/float; для bool не используется
|
||||
)
|
||||
|
||||
// DefaultEmptyModeForRequired — режим по умолчанию для required (все виды пустоты)
|
||||
const DefaultEmptyModeForRequired = EmptyModeAbsentKey | EmptyModeNil | EmptyModeZeroValue
|
||||
|
||||
// DefaultEmptyModeForUnique — режим по умолчанию для unique (nil и "" — пропускаем проверку)
|
||||
const DefaultEmptyModeForUnique = EmptyModeNil | EmptyModeZeroValue
|
||||
|
||||
// FieldConstraint — конфигурация ограничения на поле (required/unique)
|
||||
type FieldConstraint struct {
|
||||
Name string `json:"name"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
Unique bool `json:"unique,omitempty"`
|
||||
EmptyMode EmptyValueMode `json:"emptyMode,omitempty"` // для required: что считать пустым; для unique: когда пропускать проверку
|
||||
}
|
||||
|
||||
// KeyOrder задаёт порядок ключей при сериализации JSON.
|
||||
// Order >= 0: с начала (0, 1, 2...). Order < 0: с конца (-1 = последний, -2 = предпоследний).
|
||||
// Ключи не из списка — в середине, по алфавиту.
|
||||
type KeyOrder struct {
|
||||
Key string `json:"key"`
|
||||
Order int `json:"order"`
|
||||
}
|
||||
|
||||
// JSONLFileOptions представляет опции для JSONL файла
|
||||
// Соответствует TypeScript интерфейсу JSONLFileOptions
|
||||
type JSONLFileOptions struct {
|
||||
CollectionName string `json:"collectionName,omitempty"`
|
||||
AllocSize int `json:"allocSize,omitempty"`
|
||||
IndexedFields []string `json:"indexedFields,omitempty"`
|
||||
UniqueFields []string `json:"uniqueFields,omitempty"` // Поля с ограничением уникальности
|
||||
CollectionName string `json:"collectionName,omitempty"`
|
||||
AllocSize int `json:"allocSize,omitempty"`
|
||||
IndexedFields []string `json:"indexedFields,omitempty"`
|
||||
FieldConstraints []FieldConstraint `json:"fieldConstraints,omitempty"` // Ограничения полей (required/unique)
|
||||
UniqueFields []string `json:"uniqueFields,omitempty"` // Устаревшее: поля с уникальностью (совместимость)
|
||||
KeyOrder []KeyOrder `json:"keyOrder,omitempty"` // Порядок ключей при сериализации (если задан — используется кастомная сериализация)
|
||||
Encode bool `json:"encode,omitempty"` // Шифровать записи после сериализации
|
||||
EncodeKey string `json:"encodeKey,omitempty"` // Ключ (строка -> SHA256 для байтового ключа), требуется при Encode
|
||||
EncryptFn func(plaintext []byte, key []byte) ([]byte, error) `json:"-"` // Кастомное шифрование (если nil — встроенное AES-GCM)
|
||||
DecryptFn func(ciphertext []byte, key []byte) ([]byte, error) `json:"-"` // Кастомная расшифровка (если nil — встроенное AES-GCM)
|
||||
EncryptKeyForLineDb string `json:"encryptKeyForLineDb,omitempty"`
|
||||
SkipInvalidLines bool `json:"skipInvalidLines,omitempty"`
|
||||
DecryptKey string `json:"decryptKey,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user