fix: критични бъгове в storage, MVCC, auth, Raft, query executor
Поправени проблеми: - MVCC: aborted транзакции вече не стават видими (добавено abortedTxns множество) - LSM-Tree: поправена загуба на данни при immutable memtable overwrite - LSM-Tree: поправена SSTable search order (сортиране по id вместо minKey) - WAL: sync()/close() вече извикват posix.fsync() - Auth: simpleHash (djb2) заменен с HMAC-SHA256 - Recovery: summary() вече не мутира базата данни - Raft: поправена majority calculation за четен брой нодове - Query: EXISTS subqueries вече изпълняват подзаявката - Nimble: поправен build (-d:ssl) и bench task Добавен BUG_AUDIT.md с пълен доклад и план за подобрения. 292 теста, 0 failure-а.
This commit is contained in:
+196
@@ -0,0 +1,196 @@
|
|||||||
|
# BaraDB — Ревизия за бъгове и план за подобрения
|
||||||
|
|
||||||
|
> Дата: 2026-05-12
|
||||||
|
> Статус: В процес на поправяне
|
||||||
|
|
||||||
|
## Обобщение
|
||||||
|
|
||||||
|
| Категория | Брой | Статус |
|
||||||
|
|-----------|------|--------|
|
||||||
|
| 🔴 Критични | 9 | Незабавно поправяне |
|
||||||
|
| 🟠 Високи | 7 | Поправяне преди production |
|
||||||
|
| 🟡 Средни | 12 | Планирано поправяне |
|
||||||
|
| 🟢 Конфигурационни | 4 | Лесни за поправяне |
|
||||||
|
|
||||||
|
**Общ брой тестове:** 283 — всички минават, но тестовото покритие не покрива критични edge cases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔴 Критични бъгове
|
||||||
|
|
||||||
|
### 1. MVCC — Aborted транзакции стават видими
|
||||||
|
**Файл:** `src/barabadb/core/mvcc.nim`
|
||||||
|
**Проблем:** `abortTxn` изтрива транзакцията от `activeTxns`. В `isVisible` проверката е:
|
||||||
|
```nim
|
||||||
|
if creator in tm.activeTxns and tm.activeTxns[creator].state != tsCommitted:
|
||||||
|
return false
|
||||||
|
```
|
||||||
|
Ако транзакцията е aborted, тя вече не е в `activeTxns`, затова `isVisible` връща `true` и нейните писания стават видими за всички бъдещи четения.
|
||||||
|
|
||||||
|
**Fix:** Добави `abortedTxns` множество или проверявай `globalVersions` за състоянието на създателя.
|
||||||
|
|
||||||
|
### 2. LSM-Tree — Загуба на данни при immutable memtable overwrite
|
||||||
|
**Файл:** `src/barabadb/storage/lsm.nim`
|
||||||
|
**Проблем:** Когато `memTable` се напълни:
|
||||||
|
```nim
|
||||||
|
db.immutableMem = db.memTable
|
||||||
|
db.memTable = newMemTable(db.memMaxSize)
|
||||||
|
```
|
||||||
|
Ако втори `put` дойде преди `flush()` да е извикан, `immutableMem` се презаписва и данните от първия overflow се губят завинаги.
|
||||||
|
|
||||||
|
**Fix:** Проверявай `if db.immutableMem.len > 0: triggerFlush()` преди да присвояваш нова immutable memtable, или използвай опашка.
|
||||||
|
|
||||||
|
### 3. LSM-Tree — Счупена SSTable търсачка
|
||||||
|
**Файл:** `src/barabadb/storage/lsm.nim`
|
||||||
|
**Проблем:** `newLSMTree` сортира SSTables по `minKey`. `getUnsafe` търси `countdown(high, low)`, приемайки че по-висок индекс = по-нова таблица. След сортиране по `minKey` това вече не е вярно — стара SSTable може да маскира нова.
|
||||||
|
|
||||||
|
**Fix:** Сортирай по `maxTimestamp` или `id`, не по `minKey`.
|
||||||
|
|
||||||
|
### 4. Auth — JWT подписът е тривиално forgeable
|
||||||
|
**Файл:** `src/barabadb/protocol/auth.nim`
|
||||||
|
**Проблем:** `simpleHash()` е djb2-подобен hash, **не HMAC-SHA256**. Токените могат да бъдат фалшифицирани за под 1 секунда.
|
||||||
|
|
||||||
|
```nim
|
||||||
|
proc simpleHash(data: string, key: string): string =
|
||||||
|
var h: uint64 = 5381
|
||||||
|
for ch in prefix:
|
||||||
|
h = ((h shl 5) + h) + uint64(ord(ch))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix:** Замени с `std/sha256` или `hmac` от `checksums` пакета.
|
||||||
|
|
||||||
|
### 5. Auth — SCRAM-SHA-256 е фалшив
|
||||||
|
**Файл:** `src/barabadb/protocol/auth.nim`
|
||||||
|
**Проблем:** Няма challenge-response, salt, iteration count. Сравнява директно `stored == clientHash`. Еквивалентно на plaintext съхранение.
|
||||||
|
|
||||||
|
**Fix:** Имплементирай истински SCRAM-SHA-256 или премахни го.
|
||||||
|
|
||||||
|
### 6. Recovery — `summary()` мутира базата данни
|
||||||
|
**Файл:** `src/barabadb/storage/recovery.nim`
|
||||||
|
**Проблем:** `summary()` извиква `recover()`, който реплейва WAL entries в LSMTree. Простото извикване на `summary` променя данните и пише нови WAL entries.
|
||||||
|
|
||||||
|
**Fix:** Раздели `analyze()` (read-only) от `recover()` (mutating). `summary()` трябва да ползва само `analyze()`.
|
||||||
|
|
||||||
|
### 7. DistTxn — Rollback след commit attempt нарушава atomicity
|
||||||
|
**Файл:** `src/barabadb/core/disttxn.nim`
|
||||||
|
**Проблем:** В `commit()`, ако някои participants не acknowledge, coordinator се опитва да rollback-не nodes, които вече са върнали `committed = true`. Веднъж commit-нато, не може да се rollback-ва.
|
||||||
|
|
||||||
|
**Fix:** След първия successful commit на participant, не опитвай rollback. Ползвай heuristic recovery или блокирай докато всички acknowledge-нат.
|
||||||
|
|
||||||
|
### 8. Raft — Majority calculation bug за четен брой нодове
|
||||||
|
**Файл:** `src/barabadb/core/raft.nim`
|
||||||
|
**Проблем:** `handleAppendReply` използва `matchIndices.len div 2` за median. За 4 нода това дава индекс 2, т.е. искат се само 2 нода, но Raft изисква strict majority (3).
|
||||||
|
|
||||||
|
**Fix:** `matchIndices.len div 2 + 1` или `(matchIndices.len + 1) div 2`.
|
||||||
|
|
||||||
|
### 9. Query — `EXISTS` подзаявки винаги връщат false
|
||||||
|
**Файл:** `src/barabadb/query/executor.nim`
|
||||||
|
**Проблем:** `evalExpr` за `irekExists` хардкодира `return "false"`.
|
||||||
|
|
||||||
|
**Fix:** Изпълни подзаявката и провери дали връща редове.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟠 Високо приоритетни бъгове
|
||||||
|
|
||||||
|
| # | Модул | Проблем | Последствие |
|
||||||
|
|---|-------|---------|-------------|
|
||||||
|
| 10 | `storage/wal.nim` | `sync()` прави само `stream.flush()`, не `fsync` | Загуба на данни при power loss |
|
||||||
|
| 11 | `protocol/ssl.nim` | Command injection в `generateSelfSignedCert` — конкатенира пътища в shell | RCE ако `BARADB_CERT_FILE` е контролиран от attacker |
|
||||||
|
| 12 | `protocol/wire.nim` | `readString` заделя `newString(len)` от wire без лимит | OOM/DoS с `len = 0xFFFFFFFF` |
|
||||||
|
| 13 | `protocol/wire.nim` | Няма bounds checking при deserialization | Out-of-bounds reads от malformed messages |
|
||||||
|
| 14 | `query/executor.nim` | `exprToSql` не escape-ва quotes в string literals | SQL injection в views и schema DDL |
|
||||||
|
| 15 | `query/executor.nim` | `irLike`/`irILike` превръща `%`→`.*` без escaping на regex metachars | ReDoS / Catastrophic backtracking |
|
||||||
|
| 16 | `query/executor.nim` | Stale BTree indexes на UPDATE/DELETE — не изтрива стари индекси | Queries връщат изтрити/обновени редове |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 Средно приоритетни бъгове
|
||||||
|
|
||||||
|
| # | Модул | Проблем |
|
||||||
|
|---|-------|---------|
|
||||||
|
| 17 | `query/lexer.nim` | `true`/`false` се tokenizewat като `tkTrue`/`tkFalse`, но parser очаква `tkBoolLit` → `SELECT true` става `nkNullLit` |
|
||||||
|
| 18 | `query/lexer.nim` | `1.2.3` се приема като валиден `tkFloatLit` |
|
||||||
|
| 19 | `query/lexer.nim` | Незатворени block comments (`/*` без `*/`) не raise-ват error |
|
||||||
|
| 20 | `query/executor.nim` | Aggregate + `*` на empty result access-ва `sourceRows[0]` без проверка → crash |
|
||||||
|
| 21 | `query/ir.nim` | Unary minus (`-5`) се map-ва към boolean NOT вместо arithmetic negation |
|
||||||
|
| 22 | `query/udf.nim` | Non-aggregate UDFs в expression се treat-ват като NULL literals |
|
||||||
|
| 23 | `vector/engine.nim` | Dimension mismatch ползва `min(a.len, b.len)` вместо error |
|
||||||
|
| 24 | `vector/engine.nim` | Няма locking — concurrent insert + search корумпират HNSW |
|
||||||
|
| 25 | `fts/engine.nim` | Tokenize мангира UTF-8 (iterates by byte, not grapheme) |
|
||||||
|
| 26 | `graph/engine.nim` | `addEdge` без проверка дали node съществува → `KeyError` |
|
||||||
|
| 27 | `core/raft.nim` | Няма disk persistence — `currentTerm`, `votedFor`, log са in-memory |
|
||||||
|
| 28 | `core/server.nim` | `DISTTXN`/`REP` handlers чакат `recv()` без timeout → hang от malicious peer |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Конфигурационни проблеми
|
||||||
|
|
||||||
|
| # | Проблем | Файл | Fix |
|
||||||
|
|---|---------|------|-----|
|
||||||
|
| 29 | `nimble build` fail-ва без `-d:ssl`, въпреки `switch("define", "ssl")` в `.nimble` | `baradadb.nimble` | Добави `switch("define", "ssl")` в `nim.cfg` или използвай `before build:` hook |
|
||||||
|
| 30 | `bench` task сочи към `benchmarks/bench_storage.nim` който не съществува | `baradadb.nimble` | Промени на `benchmarks/bench_all.nim` |
|
||||||
|
| 31 | CI build benchmarks без `-d:ssl`, но `bench_all.nim` може да изисква SSL | `.github/workflows/ci.yml` | Добави `-d:ssl` |
|
||||||
|
| 32 | `threadpool` е deprecated (Nim 2.2) | `src/baradadb.nim` | Мигрирай към `malebolgia`/`weave`/`taskpools` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 План за подобрения
|
||||||
|
|
||||||
|
### Незабавни действия (Sprint 0 — 1-2 седмици)
|
||||||
|
|
||||||
|
- [ ] Поправи MVCC isVisible (aborted txns)
|
||||||
|
- [ ] Поправи LSM-Tree immutable memtable overwrite
|
||||||
|
- [ ] Поправи LSM-Tree SSTable search order
|
||||||
|
- [ ] Замени simpleHash с HMAC-SHA256 в auth
|
||||||
|
- [ ] Поправи recovery.summary() да не мутира
|
||||||
|
- [ ] Поправи Raft majority calculation
|
||||||
|
- [ ] Поправи WAL sync() да прави fsync
|
||||||
|
- [ ] Поправи nimble build (-d:ssl)
|
||||||
|
|
||||||
|
### Краткосрочни (Sprint 1-2 — 1 месец)
|
||||||
|
|
||||||
|
- [ ] Имплементирай истински SCRAM-SHA-256 или го премахни
|
||||||
|
- [ ] Добави bounds checking в wire protocol
|
||||||
|
- [ ] Escape-вай string literals в exprToSql
|
||||||
|
- [ ] Поправи EXISTS subqueries
|
||||||
|
- [ ] Поправи stale BTree indexes на UPDATE/DELETE
|
||||||
|
- [ ] Добави timeouts на всички network reads
|
||||||
|
- [ ] Поправи boolean literal parsing
|
||||||
|
- [ ] Добави locking в HNSW/vector engine
|
||||||
|
|
||||||
|
### Средносрочни (1-2 месеца)
|
||||||
|
|
||||||
|
- [ ] Raft disk persistence (write-ahead log за Raft state)
|
||||||
|
- [ ] Command injection fix в SSL модул
|
||||||
|
- [ ] ReDoS fix в LIKE (escape regex metachars или не използвай regex)
|
||||||
|
- [ ] UTF-8 tokenization в FTS
|
||||||
|
- [ ] Memory management: unbounded version chains в MVCC
|
||||||
|
- [ ] Rate limiter: enforce globalRate, fix memory leaks
|
||||||
|
- [ ] Property-based тестове за LSM-Tree edge cases
|
||||||
|
|
||||||
|
### Дългосрочни (Formal Verification)
|
||||||
|
|
||||||
|
| Задача | Приоритет |
|
||||||
|
|--------|-----------|
|
||||||
|
| `backup.tla` — restore atomicity | Висок |
|
||||||
|
| `recovery.tla` — WAL replay correctness | Висок |
|
||||||
|
| `crossmodal.tla` — cross-modal consistency | Среден |
|
||||||
|
| Symmetry reduction в `.cfg` файловете | Нисък (performance) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Ключеви метрики за проследяване
|
||||||
|
|
||||||
|
| Метрика | Цел |
|
||||||
|
|---------|-----|
|
||||||
|
| Тестово покритие | > 80% (сега вероятно < 50% за edge cases) |
|
||||||
|
| Fuzz тестове | Поне 1 fuzz target per storage engine |
|
||||||
|
| Security audit | Поправи всички 🔴 и 🟠 issues |
|
||||||
|
| Benchmark consistency | `nimble bench` да работи out-of-the-box |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Заключение
|
||||||
|
|
||||||
|
BaraDB има солидна архитектура и много функционалности, но има **критични бъгове в core storage, MVCC, auth и distributed транзакциите**, които я правят **неподходяща за production** в момента. Препоръчвам фокусиран sprint върху критичните issues преди добавяне на нови функции.
|
||||||
+1
-3
@@ -7,8 +7,6 @@ srcDir = "src"
|
|||||||
bin = @["baradadb"]
|
bin = @["baradadb"]
|
||||||
binDir = "build"
|
binDir = "build"
|
||||||
|
|
||||||
switch("define", "ssl")
|
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
requires "nim >= 2.2.0"
|
requires "nim >= 2.2.0"
|
||||||
requires "https://github.com/katehonz/hunos >= 1.2.0"
|
requires "https://github.com/katehonz/hunos >= 1.2.0"
|
||||||
@@ -26,4 +24,4 @@ task test, "Run all tests":
|
|||||||
exec "nim c --path:src -d:ssl -r tests/test_all.nim"
|
exec "nim c --path:src -d:ssl -r tests/test_all.nim"
|
||||||
|
|
||||||
task bench, "Run benchmarks":
|
task bench, "Run benchmarks":
|
||||||
exec "nim c --path:src -d:ssl -d:release -r benchmarks/bench_storage.nim"
|
exec "nim c --path:src -d:ssl -d:release -r benchmarks/bench_all.nim"
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ type
|
|||||||
nextLsn: uint64
|
nextLsn: uint64
|
||||||
activeTxns: Table[TxnId, Transaction]
|
activeTxns: Table[TxnId, Transaction]
|
||||||
committedTxns: seq[TxnId]
|
committedTxns: seq[TxnId]
|
||||||
|
committedTxnsSet: HashSet[TxnId]
|
||||||
|
abortedTxns: HashSet[TxnId]
|
||||||
globalVersions*: Table[string, seq[VersionedRecord]]
|
globalVersions*: Table[string, seq[VersionedRecord]]
|
||||||
txnTimeoutMs: int64
|
txnTimeoutMs: int64
|
||||||
deadlockDetector*: DeadlockDetector
|
deadlockDetector*: DeadlockDetector
|
||||||
@@ -59,6 +61,8 @@ proc newTxnManager*(txnTimeoutMs: int64 = 30000): TxnManager =
|
|||||||
result.nextLsn = 1
|
result.nextLsn = 1
|
||||||
result.activeTxns = initTable[TxnId, Transaction]()
|
result.activeTxns = initTable[TxnId, Transaction]()
|
||||||
result.committedTxns = @[]
|
result.committedTxns = @[]
|
||||||
|
result.committedTxnsSet = initHashSet[TxnId]()
|
||||||
|
result.abortedTxns = initHashSet[TxnId]()
|
||||||
result.globalVersions = initTable[string, seq[VersionedRecord]]()
|
result.globalVersions = initTable[string, seq[VersionedRecord]]()
|
||||||
result.txnTimeoutMs = txnTimeoutMs
|
result.txnTimeoutMs = txnTimeoutMs
|
||||||
result.deadlockDetector = newDeadlockDetector()
|
result.deadlockDetector = newDeadlockDetector()
|
||||||
@@ -112,8 +116,13 @@ proc isVisible(tm: TxnManager, txn: Transaction, version: VersionedRecord): bool
|
|||||||
return false
|
return false
|
||||||
if creator in txn.snapshotTxns:
|
if creator in txn.snapshotTxns:
|
||||||
return false
|
return false
|
||||||
|
if creator in tm.abortedTxns:
|
||||||
|
return false
|
||||||
if creator in tm.activeTxns and tm.activeTxns[creator].state != tsCommitted:
|
if creator in tm.activeTxns and tm.activeTxns[creator].state != tsCommitted:
|
||||||
return false
|
return false
|
||||||
|
# If creator is not active and not committed, it must be aborted
|
||||||
|
if creator notin tm.activeTxns and creator notin tm.committedTxnsSet:
|
||||||
|
return false
|
||||||
|
|
||||||
# Deleter must not be a visible committed txn
|
# Deleter must not be a visible committed txn
|
||||||
if deleter != TxnId(0):
|
if deleter != TxnId(0):
|
||||||
@@ -246,6 +255,7 @@ proc commit*(tm: TxnManager, txn: Transaction): bool =
|
|||||||
|
|
||||||
txn.state = tsCommitted
|
txn.state = tsCommitted
|
||||||
tm.committedTxns.add(txn.id)
|
tm.committedTxns.add(txn.id)
|
||||||
|
tm.committedTxnsSet.incl(txn.id)
|
||||||
tm.activeTxns.del(txn.id)
|
tm.activeTxns.del(txn.id)
|
||||||
tm.deadlockDetector.removeTxn(uint64(txn.id))
|
tm.deadlockDetector.removeTxn(uint64(txn.id))
|
||||||
release(tm.lock)
|
release(tm.lock)
|
||||||
@@ -258,6 +268,7 @@ proc abortTxn*(tm: TxnManager, txn: Transaction): bool =
|
|||||||
return false
|
return false
|
||||||
txn.state = tsAborted
|
txn.state = tsAborted
|
||||||
tm.activeTxns.del(txn.id)
|
tm.activeTxns.del(txn.id)
|
||||||
|
tm.abortedTxns.incl(txn.id)
|
||||||
tm.deadlockDetector.removeTxn(uint64(txn.id))
|
tm.deadlockDetector.removeTxn(uint64(txn.id))
|
||||||
release(tm.lock)
|
release(tm.lock)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ proc handleAppendReply*(node: RaftNode, peerId: string, reply: RaftMessage) =
|
|||||||
matchIndices.add(idx)
|
matchIndices.add(idx)
|
||||||
matchIndices.sort()
|
matchIndices.sort()
|
||||||
|
|
||||||
let medianIdx = matchIndices[matchIndices.len div 2]
|
let medianIdx = matchIndices[(matchIndices.len - 1) div 2]
|
||||||
if medianIdx > node.commitIndex:
|
if medianIdx > node.commitIndex:
|
||||||
if medianIdx <= node.lastLogIndex and
|
if medianIdx <= node.lastLogIndex and
|
||||||
node.log[medianIdx - 1].term == node.currentTerm:
|
node.log[medianIdx - 1].term == node.currentTerm:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import std/strutils
|
import std/strutils
|
||||||
import std/base64
|
import std/base64
|
||||||
import std/tables
|
import std/tables
|
||||||
|
import checksums/sha2
|
||||||
|
|
||||||
type
|
type
|
||||||
AuthMethod* = enum
|
AuthMethod* = enum
|
||||||
@@ -51,12 +52,33 @@ proc base64UrlDecode(data: string): string =
|
|||||||
s &= "="
|
s &= "="
|
||||||
return decode(s)
|
return decode(s)
|
||||||
|
|
||||||
proc simpleHash(data: string, key: string): string =
|
proc hmacSha256(key, message: string): string =
|
||||||
var prefix = data & key
|
var k = key
|
||||||
var h: uint64 = 5381
|
if k.len > 64:
|
||||||
for ch in prefix:
|
var ctx = initSha_256()
|
||||||
h = ((h shl 5) + h) + uint64(ord(ch))
|
ctx.update(k.toOpenArray(0, k.len-1))
|
||||||
return $h
|
let hash = ctx.digest()
|
||||||
|
k = $hash
|
||||||
|
while k.len < 64:
|
||||||
|
k &= "\x00"
|
||||||
|
|
||||||
|
var ipad = newString(64)
|
||||||
|
var opad = newString(64)
|
||||||
|
for i in 0..<64:
|
||||||
|
ipad[i] = chr(ord(k[i]) xor 0x36)
|
||||||
|
opad[i] = chr(ord(k[i]) xor 0x5c)
|
||||||
|
|
||||||
|
var innerCtx = initSha_256()
|
||||||
|
innerCtx.update(ipad.toOpenArray(0, ipad.len-1))
|
||||||
|
innerCtx.update(message.toOpenArray(0, message.len-1))
|
||||||
|
let innerHash = innerCtx.digest()
|
||||||
|
|
||||||
|
var outerCtx = initSha_256()
|
||||||
|
outerCtx.update(opad.toOpenArray(0, opad.len-1))
|
||||||
|
outerCtx.update(innerHash.toOpenArray(0, innerHash.len-1))
|
||||||
|
let outerHash = outerCtx.digest()
|
||||||
|
|
||||||
|
return $outerHash
|
||||||
|
|
||||||
proc createToken*(am: AuthManager, claims: JWTClaims): string =
|
proc createToken*(am: AuthManager, claims: JWTClaims): string =
|
||||||
let header = base64UrlEncode("{\"alg\":\"HS256\",\"typ\":\"JWT\"}")
|
let header = base64UrlEncode("{\"alg\":\"HS256\",\"typ\":\"JWT\"}")
|
||||||
@@ -64,7 +86,7 @@ proc createToken*(am: AuthManager, claims: JWTClaims): string =
|
|||||||
"{\"sub\":\"" & claims.sub & "\",\"role\":\"" & claims.role &
|
"{\"sub\":\"" & claims.sub & "\",\"role\":\"" & claims.role &
|
||||||
"\",\"database\":\"" & claims.database & "\"}")
|
"\",\"database\":\"" & claims.database & "\"}")
|
||||||
let data = header & "." & payload
|
let data = header & "." & payload
|
||||||
let signature = simpleHash(data, am.secretKey)
|
let signature = hmacSha256(am.secretKey, data)
|
||||||
am.tokens.add(data & "." & base64UrlEncode(signature))
|
am.tokens.add(data & "." & base64UrlEncode(signature))
|
||||||
return am.tokens[^1]
|
return am.tokens[^1]
|
||||||
|
|
||||||
@@ -73,7 +95,7 @@ proc verifyToken*(am: AuthManager, token: string): (bool, JWTClaims) =
|
|||||||
if parts.len != 3:
|
if parts.len != 3:
|
||||||
return (false, JWTClaims())
|
return (false, JWTClaims())
|
||||||
let data = parts[0] & "." & parts[1]
|
let data = parts[0] & "." & parts[1]
|
||||||
let sig = simpleHash(data, am.secretKey)
|
let sig = hmacSha256(am.secretKey, data)
|
||||||
if base64UrlEncode(sig) != parts[2]:
|
if base64UrlEncode(sig) != parts[2]:
|
||||||
return (false, JWTClaims())
|
return (false, JWTClaims())
|
||||||
# Parse payload
|
# Parse payload
|
||||||
@@ -132,8 +154,8 @@ proc validateCredentials*(am: AuthManager, creds: AuthCredentials): AuthResult =
|
|||||||
if creds.username in am.users:
|
if creds.username in am.users:
|
||||||
let stored = am.users[creds.username]
|
let stored = am.users[creds.username]
|
||||||
# SCRAM-SHA-256: client sends SHA-256(password) as payload
|
# SCRAM-SHA-256: client sends SHA-256(password) as payload
|
||||||
let clientHash = if creds.payload.len > 0: creds.payload else: simpleHash("", am.secretKey)
|
let clientHash = if creds.payload.len > 0: creds.payload else: hmacSha256(am.secretKey, "")
|
||||||
if stored == clientHash or stored == simpleHash(creds.payload, am.secretKey):
|
if stored == clientHash or stored == hmacSha256(am.secretKey, creds.payload):
|
||||||
return AuthResult(authenticated: true, username: creds.username,
|
return AuthResult(authenticated: true, username: creds.username,
|
||||||
role: "user", database: "default")
|
role: "user", database: "default")
|
||||||
return AuthResult(authenticated: false, error: "Invalid SCRAM credentials")
|
return AuthResult(authenticated: false, error: "Invalid SCRAM credentials")
|
||||||
|
|||||||
@@ -410,6 +410,8 @@ proc parseRowData(valStr: string): Table[string, string] =
|
|||||||
let v = part[eqPos+1..^1].strip()
|
let v = part[eqPos+1..^1].strip()
|
||||||
result[k] = v
|
result[k] = v
|
||||||
|
|
||||||
|
proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row]
|
||||||
|
|
||||||
proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext = nil): string =
|
proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext = nil): string =
|
||||||
if expr == nil: return ""
|
if expr == nil: return ""
|
||||||
case expr.kind
|
case expr.kind
|
||||||
@@ -593,7 +595,11 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
|
|||||||
let v = evalExpr(expr.unExpr, row)
|
let v = evalExpr(expr.unExpr, row)
|
||||||
return if not isNull(v): "true" else: "false"
|
return if not isNull(v): "true" else: "false"
|
||||||
else: return "false"
|
else: return "false"
|
||||||
of irekExists: return "false"
|
of irekExists:
|
||||||
|
if ctx != nil:
|
||||||
|
let rows = executePlan(ctx, expr.existsSubquery)
|
||||||
|
return if rows.len > 0: "true" else: "false"
|
||||||
|
return "false"
|
||||||
else: return ""
|
else: return ""
|
||||||
|
|
||||||
proc lowerExpr*(node: Node): IRExpr
|
proc lowerExpr*(node: Node): IRExpr
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type
|
|||||||
maxSize: int
|
maxSize: int
|
||||||
|
|
||||||
SSTable* = object
|
SSTable* = object
|
||||||
|
id*: int
|
||||||
path*: string
|
path*: string
|
||||||
index*: Table[string, int64]
|
index*: Table[string, int64]
|
||||||
bloom*: BloomFilter
|
bloom*: BloomFilter
|
||||||
@@ -186,6 +187,7 @@ proc writeSSTable*(entries: seq[Entry], path: string, level: int): SSTable =
|
|||||||
if maxK == "" or entry.key > maxK: maxK = entry.key
|
if maxK == "" or entry.key > maxK: maxK = entry.key
|
||||||
|
|
||||||
result = SSTable(
|
result = SSTable(
|
||||||
|
id: -1,
|
||||||
path: path,
|
path: path,
|
||||||
index: idxTable,
|
index: idxTable,
|
||||||
bloom: bloom,
|
bloom: bloom,
|
||||||
@@ -238,6 +240,7 @@ proc loadSSTable*(path: string): SSTable =
|
|||||||
bloom.deserialize(bloomData)
|
bloom.deserialize(bloomData)
|
||||||
|
|
||||||
result = SSTable(
|
result = SSTable(
|
||||||
|
id: -1,
|
||||||
path: path,
|
path: path,
|
||||||
index: idxTable,
|
index: idxTable,
|
||||||
bloom: bloom,
|
bloom: bloom,
|
||||||
@@ -294,6 +297,8 @@ proc close*(sst: var SSTable) =
|
|||||||
# LSMTree API
|
# LSMTree API
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
proc flushUnsafe(db: LSMTree) {.gcsafe.}
|
||||||
|
|
||||||
proc newLSMTree*(dir: string, memMaxSize: int = DefaultMemTableSize): LSMTree =
|
proc newLSMTree*(dir: string, memMaxSize: int = DefaultMemTableSize): LSMTree =
|
||||||
createDir(dir)
|
createDir(dir)
|
||||||
createDir(dir / "sstables")
|
createDir(dir / "sstables")
|
||||||
@@ -306,13 +311,14 @@ proc newLSMTree*(dir: string, memMaxSize: int = DefaultMemTableSize): LSMTree =
|
|||||||
if kind == pcFile and path.endsWith(".sst"):
|
if kind == pcFile and path.endsWith(".sst"):
|
||||||
try:
|
try:
|
||||||
var sst = loadSSTable(path)
|
var sst = loadSSTable(path)
|
||||||
sstables.add(sst)
|
|
||||||
let name = splitFile(path).name
|
let name = splitFile(path).name
|
||||||
nextId = max(nextId, parseInt(name) + 1)
|
sst.id = parseInt(name)
|
||||||
|
sstables.add(sst)
|
||||||
|
nextId = max(nextId, sst.id + 1)
|
||||||
except:
|
except:
|
||||||
discard # skip corrupt SSTables
|
discard # skip corrupt SSTables
|
||||||
|
|
||||||
sstables.sort(proc(a, b: SSTable): int = cmp(a.minKey, b.minKey))
|
sstables.sort(proc(a, b: SSTable): int = cmp(b.id, a.id))
|
||||||
|
|
||||||
new(result)
|
new(result)
|
||||||
initLock(result.lock)
|
initLock(result.lock)
|
||||||
@@ -328,8 +334,9 @@ proc newLSMTree*(dir: string, memMaxSize: int = DefaultMemTableSize): LSMTree =
|
|||||||
# WAL crash recovery — replay unflushed entries into memTable
|
# WAL crash recovery — replay unflushed entries into memTable
|
||||||
let walPath = dir / "wal" / "wal.log"
|
let walPath = dir / "wal" / "wal.log"
|
||||||
if fileExists(walPath):
|
if fileExists(walPath):
|
||||||
|
var stream: FileStream = nil
|
||||||
try:
|
try:
|
||||||
let stream = newFileStream(walPath, fmRead)
|
stream = newFileStream(walPath, fmRead)
|
||||||
if stream != nil:
|
if stream != nil:
|
||||||
var magic: uint32 = 0
|
var magic: uint32 = 0
|
||||||
var version: uint32 = 0
|
var version: uint32 = 0
|
||||||
@@ -352,16 +359,20 @@ proc newLSMTree*(dir: string, memMaxSize: int = DefaultMemTableSize): LSMTree =
|
|||||||
if stream.readData(addr value[0], valLen.int) != valLen.int: break
|
if stream.readData(addr value[0], valLen.int) != valLen.int: break
|
||||||
case WalEntryKind(kind)
|
case WalEntryKind(kind)
|
||||||
of wekPut:
|
of wekPut:
|
||||||
|
if not result.memTable.put(key, value, timestamp):
|
||||||
|
result.flushUnsafe()
|
||||||
discard result.memTable.put(key, value, timestamp)
|
discard result.memTable.put(key, value, timestamp)
|
||||||
of wekDelete:
|
of wekDelete:
|
||||||
discard result.memTable.put(key, @[], timestamp) # tombstone
|
if not result.memTable.put(key, @[], timestamp, deleted = true):
|
||||||
|
result.flushUnsafe()
|
||||||
|
discard result.memTable.put(key, @[], timestamp, deleted = true)
|
||||||
of wekCommit:
|
of wekCommit:
|
||||||
discard
|
discard
|
||||||
of wekCheckpoint:
|
of wekCheckpoint:
|
||||||
discard
|
discard
|
||||||
|
finally:
|
||||||
|
if stream != nil:
|
||||||
stream.close()
|
stream.close()
|
||||||
except:
|
|
||||||
discard
|
|
||||||
|
|
||||||
proc put*(db: LSMTree, key: string, value: seq[byte]) =
|
proc put*(db: LSMTree, key: string, value: seq[byte]) =
|
||||||
acquire(db.lock)
|
acquire(db.lock)
|
||||||
@@ -369,6 +380,8 @@ proc put*(db: LSMTree, key: string, value: seq[byte]) =
|
|||||||
let ts = uint64(getMonoTime().ticks())
|
let ts = uint64(getMonoTime().ticks())
|
||||||
db.wal.writePut(cast[seq[byte]](key), value, ts)
|
db.wal.writePut(cast[seq[byte]](key), value, ts)
|
||||||
if not db.memTable.put(key, value, ts):
|
if not db.memTable.put(key, value, ts):
|
||||||
|
if db.immutableMem.len > 0:
|
||||||
|
db.flushUnsafe()
|
||||||
db.immutableMem = db.memTable
|
db.immutableMem = db.memTable
|
||||||
db.memTable = newMemTable(db.memMaxSize)
|
db.memTable = newMemTable(db.memMaxSize)
|
||||||
discard db.memTable.put(key, value, ts)
|
discard db.memTable.put(key, value, ts)
|
||||||
@@ -378,6 +391,11 @@ proc delete*(db: LSMTree, key: string) =
|
|||||||
defer: release(db.lock)
|
defer: release(db.lock)
|
||||||
let ts = uint64(getMonoTime().ticks())
|
let ts = uint64(getMonoTime().ticks())
|
||||||
db.wal.writeDelete(cast[seq[byte]](key), ts)
|
db.wal.writeDelete(cast[seq[byte]](key), ts)
|
||||||
|
if not db.memTable.put(key, @[], ts, deleted = true):
|
||||||
|
if db.immutableMem.len > 0:
|
||||||
|
db.flushUnsafe()
|
||||||
|
db.immutableMem = db.memTable
|
||||||
|
db.memTable = newMemTable(db.memMaxSize)
|
||||||
discard db.memTable.put(key, @[], ts, deleted = true)
|
discard db.memTable.put(key, @[], ts, deleted = true)
|
||||||
|
|
||||||
proc getUnsafe(db: LSMTree, key: string): (bool, seq[byte]) =
|
proc getUnsafe(db: LSMTree, key: string): (bool, seq[byte]) =
|
||||||
@@ -438,8 +456,9 @@ proc flushUnsafe(db: LSMTree) =
|
|||||||
inc db.nextSSTableId
|
inc db.nextSSTableId
|
||||||
|
|
||||||
var sst = writeSSTable(toFlush.entries, path, level = 0)
|
var sst = writeSSTable(toFlush.entries, path, level = 0)
|
||||||
|
sst.id = db.nextSSTableId - 1
|
||||||
db.sstables.add(sst)
|
db.sstables.add(sst)
|
||||||
db.sstables.sort(proc(a, b: SSTable): int = cmp(a.minKey, b.minKey))
|
# SSTables are kept in insertion order (newest last) so getUnsafe can search newest-first
|
||||||
|
|
||||||
db.wal.writeCommit(uint64(getMonoTime().ticks()))
|
db.wal.writeCommit(uint64(getMonoTime().ticks()))
|
||||||
db.wal.sync()
|
db.wal.sync()
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ proc recover*(rec: CrashRecovery, db: LSMTree = nil): RecoveryResult =
|
|||||||
proc totalEntries*(rec: CrashRecovery): int = rec.entries.len
|
proc totalEntries*(rec: CrashRecovery): int = rec.entries.len
|
||||||
|
|
||||||
proc summary*(rec: CrashRecovery): string =
|
proc summary*(rec: CrashRecovery): string =
|
||||||
let r = rec.recover()
|
let r = rec.analyze()
|
||||||
result = "WAL Recovery Summary:\n"
|
result = "WAL Recovery Summary:\n"
|
||||||
result &= " Total entries: " & $r.totalEntries & "\n"
|
result &= " Total entries: " & $r.totalEntries & "\n"
|
||||||
result &= " Redone (committed): " & $r.redone & "\n"
|
result &= " Redone (committed): " & $r.redone & "\n"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
## WAL — Write-Ahead Log for durability
|
## WAL — Write-Ahead Log for durability
|
||||||
import std/os
|
import std/os
|
||||||
import std/streams
|
import std/streams
|
||||||
|
import std/posix
|
||||||
|
|
||||||
const
|
const
|
||||||
WALMagic* = 0x42415241'u32 # "BARA"
|
WALMagic* = 0x42415241'u32 # "BARA"
|
||||||
@@ -77,9 +78,17 @@ proc writeCommit*(wal: var WriteAheadLog, timestamp: uint64) =
|
|||||||
|
|
||||||
proc sync*(wal: var WriteAheadLog) =
|
proc sync*(wal: var WriteAheadLog) =
|
||||||
wal.stream.flush()
|
wal.stream.flush()
|
||||||
|
let fd = posix.open(wal.path, O_RDONLY)
|
||||||
|
if fd != -1:
|
||||||
|
discard posix.fsync(fd)
|
||||||
|
discard posix.close(fd)
|
||||||
|
|
||||||
proc close*(wal: var WriteAheadLog) =
|
proc close*(wal: var WriteAheadLog) =
|
||||||
wal.stream.flush()
|
wal.stream.flush()
|
||||||
|
let fd = posix.open(wal.path, O_RDONLY)
|
||||||
|
if fd != -1:
|
||||||
|
discard posix.fsync(fd)
|
||||||
|
discard posix.close(fd)
|
||||||
wal.stream.close()
|
wal.stream.close()
|
||||||
|
|
||||||
proc entryCount*(wal: WriteAheadLog): uint64 = wal.entryCount
|
proc entryCount*(wal: WriteAheadLog): uint64 = wal.entryCount
|
||||||
|
|||||||
Reference in New Issue
Block a user