feat: PITR RECOVER TO TIMESTAMP, OpenTelemetry OTLP export, FTS infrastructure
- RECOVER TO TIMESTAMP '...' — parser + executor, replays WAL entries - core/tracing.nim — OTLP/HTTP export via exportOtlp() - FTS: ftsIndexes table in ExecutionContext, engine import ready - 281 tests, 0 failures
This commit is contained in:
@@ -1,49 +1,15 @@
|
|||||||
# BaraDB — Оставащи задачи
|
# BaraDB — Оставащи задачи
|
||||||
|
|
||||||
> Завършеното е преместено в `PLAN_DONE.md`.
|
> Завършеното е в `PLAN_DONE.md`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Ниски (nice-to-have, не блокират production)
|
## Ниски (nice-to-have, не блокират production)
|
||||||
|
|
||||||
### 1. Full FTS index integration (BM25 ranking)
|
### 1. Full FTS BM25 ranking in `@@`
|
||||||
- **Статус:** ⚠️ Базова поддръжка
|
- **Статус:** ⚠️ Инфраструктура готова
|
||||||
- **Текущо:** `WHERE col @@ 'query'` прави case-insensitive term match. FTS engine (`fts/engine.nim`) съществува с BM25, inverted index, tokenization.
|
- **Текущо:** `ftsIndexes: Table[string, InvertedIndex]` в ExecutionContext. FTS engine (`fts/engine.nim`) с BM25, tokenization. `@@` прави term-based substring match.
|
||||||
- **Липсва:** Пълна интеграция — `CREATE INDEX ... USING FTS` автоматично поддържа InvertedIndex при INSERT/UPDATE/DELETE. `@@` използва BM25 вместо substring.
|
- **Липсва:** `evalExpr` няма достъп до `ctx` за BM25 lookup. Трябва refactoring на evalExpr signature или FTS filter на ниво `executePlan`.
|
||||||
|
|
||||||
### 2. Point-in-time recovery (PITR)
|
|
||||||
- **Статус:** ⚠️ WAL reader добавен
|
|
||||||
- **Текущо:** `readEntries(path, untilTimestamp)` чете WAL entries до даден timestamp. Backup/restore чрез tar.gz snapshot работи.
|
|
||||||
- **Липсва:** `RECOVER TO TIMESTAMP '...'` команда, която прилага WAL entries до timestamp.
|
|
||||||
|
|
||||||
### 3. OpenTelemetry tracing
|
|
||||||
- **Статус:** ⚠️ Базов tracer
|
|
||||||
- **Текущо:** `core/tracing.nim` — span recording, enable/disable, JSON export. `executeQuery` създава span за всяка заявка.
|
|
||||||
- **Липсва:** OTLP/gRPC export, integration с external collector.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Завършено в последните 2 сесии (2026-05-07)
|
|
||||||
|
|
||||||
### Session 1: SQL Features
|
|
||||||
- Recursive CTE (WITH RECURSIVE + UNION ALL)
|
|
||||||
- UNION / INTERSECT / EXCEPT
|
|
||||||
- DROP INDEX
|
|
||||||
- VIEW DDL persistence
|
|
||||||
- JSON path operators (`->`, `->>`)
|
|
||||||
- FTS SQL wiring (`WHERE col @@ 'query'`)
|
|
||||||
- Multi-column index range scans
|
|
||||||
- Covering index optimization
|
|
||||||
- SCRAM-SHA-256 authentication
|
|
||||||
|
|
||||||
### Session 2: Production Hardening
|
|
||||||
- JWT secret — `getEffectiveJwtSecret()` helper + warning log при липса на конфигурация
|
|
||||||
- SSTable metadata — коментари изяснени (offsets се patch-ват коректно)
|
|
||||||
- LSM thread-safety — locks вече съществуват, stress test обновен
|
|
||||||
- WAL reader — `readEntries(path, untilTimestamp)` за PITR
|
|
||||||
- OpenTelemetry — `core/tracing.nim` с span recording, `executeQuery` tracing
|
|
||||||
- Distributed 2PC — реален TCP RPC вместо simulated (host="" → local fallback)
|
|
||||||
- `addParticipant` има default параметри (host="", port=0)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -56,4 +22,18 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**279 теста — 0 failure-а.**
|
## Завършено (обща сума: 3 сесии)
|
||||||
|
|
||||||
|
**281 теста — 0 failure-а.**
|
||||||
|
|
||||||
|
### Session 1: SQL Features
|
||||||
|
- Recursive CTE, UNION/INTERSECT/EXCEPT, DROP INDEX, VIEW persistence
|
||||||
|
- JSON path (`->`, `->>`), FTS SQL (`@@`), multi-col range scan, covering index, SCRAM auth
|
||||||
|
|
||||||
|
### Session 2: Production Hardening
|
||||||
|
- JWT security, WAL reader, tracing spans, 2PC real RPC, stress test fix
|
||||||
|
|
||||||
|
### Session 3: PITR + OpenTelemetry
|
||||||
|
- `RECOVER TO TIMESTAMP` — parser + executor (WAL replay)
|
||||||
|
- `core/tracing.nim` — OTLP/HTTP export (`exportOtlp`)
|
||||||
|
- FTS infrastructure — `ftsIndexes` in ExecutionContext, engine import
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import std/json
|
|||||||
import std/times
|
import std/times
|
||||||
import std/monotimes
|
import std/monotimes
|
||||||
import std/tables
|
import std/tables
|
||||||
|
import std/httpclient
|
||||||
|
import std/strutils
|
||||||
|
|
||||||
type
|
type
|
||||||
SpanStatus* = enum
|
SpanStatus* = enum
|
||||||
@@ -82,3 +84,44 @@ proc enable*(tracer: Tracer) =
|
|||||||
|
|
||||||
proc disable*(tracer: Tracer) =
|
proc disable*(tracer: Tracer) =
|
||||||
tracer.enabled = false
|
tracer.enabled = false
|
||||||
|
|
||||||
|
proc exportOtlp*(tracer: Tracer, endpoint: string = "http://localhost:4318/v1/traces"): bool =
|
||||||
|
## Export spans via OTLP/HTTP (JSON format).
|
||||||
|
## Returns true on success.
|
||||||
|
if tracer.spans.len == 0: return true
|
||||||
|
var otlpSpans = newJArray()
|
||||||
|
for span in tracer.spans:
|
||||||
|
var attrs = newJArray()
|
||||||
|
for k, v in span.attributes:
|
||||||
|
attrs.add(%*{"key": k, "value": {"stringValue": v}})
|
||||||
|
otlpSpans.add(%*{
|
||||||
|
"traceId": span.traceId,
|
||||||
|
"spanId": span.spanId,
|
||||||
|
"parentSpanId": span.parentSpanId,
|
||||||
|
"name": span.name,
|
||||||
|
"kind": "SPAN_KIND_INTERNAL",
|
||||||
|
"startTimeUnixNano": $span.startTime,
|
||||||
|
"endTimeUnixNano": $span.endTime,
|
||||||
|
"status": {"code": if span.status == ssOk: "STATUS_CODE_OK" else: "STATUS_CODE_ERROR"},
|
||||||
|
"attributes": attrs,
|
||||||
|
})
|
||||||
|
let body = %*{
|
||||||
|
"resourceSpans": [{
|
||||||
|
"resource": {"attributes": [
|
||||||
|
{"key": "service.name", "value": {"stringValue": "baradadb"}}
|
||||||
|
]},
|
||||||
|
"scopeSpans": [{
|
||||||
|
"scope": {"name": "baradadb-tracer", "version": "0.1.0"},
|
||||||
|
"spans": otlpSpans
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
let client = newHttpClient()
|
||||||
|
client.headers["Content-Type"] = "application/json"
|
||||||
|
discard client.postContent(endpoint, body = $body)
|
||||||
|
client.close()
|
||||||
|
tracer.spans = @[]
|
||||||
|
return true
|
||||||
|
except:
|
||||||
|
return false
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type
|
|||||||
nkCommitTxn
|
nkCommitTxn
|
||||||
nkRollbackTxn
|
nkRollbackTxn
|
||||||
nkExplainStmt
|
nkExplainStmt
|
||||||
|
nkRecoverToTimestamp
|
||||||
nkCreateView
|
nkCreateView
|
||||||
nkDropView
|
nkDropView
|
||||||
nkCreateTrigger
|
nkCreateTrigger
|
||||||
@@ -233,6 +234,8 @@ type
|
|||||||
of nkExplainStmt:
|
of nkExplainStmt:
|
||||||
expStmt*: Node
|
expStmt*: Node
|
||||||
expAnalyze*: bool
|
expAnalyze*: bool
|
||||||
|
of nkRecoverToTimestamp:
|
||||||
|
recoverTimestamp*: string
|
||||||
of nkCreateView:
|
of nkCreateView:
|
||||||
cvName*: string
|
cvName*: string
|
||||||
cvQuery*: Node
|
cvQuery*: Node
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ import ../core/types
|
|||||||
import ../protocol/wire
|
import ../protocol/wire
|
||||||
import ../storage/lsm
|
import ../storage/lsm
|
||||||
import ../storage/btree
|
import ../storage/btree
|
||||||
|
import ../storage/wal
|
||||||
import ../core/mvcc
|
import ../core/mvcc
|
||||||
import ../core/tracing
|
import ../core/tracing
|
||||||
|
import ../fts/engine as fts
|
||||||
|
|
||||||
type
|
type
|
||||||
IndexEntry* = ref object
|
IndexEntry* = ref object
|
||||||
@@ -57,6 +59,7 @@ type
|
|||||||
btrees*: Table[string, BTreeIndex[string, IndexEntry]]
|
btrees*: Table[string, BTreeIndex[string, IndexEntry]]
|
||||||
views*: Table[string, Node] # view name -> SELECT AST
|
views*: Table[string, Node] # view name -> SELECT AST
|
||||||
cteTables*: Table[string, seq[Row]] # CTE name -> rows
|
cteTables*: Table[string, seq[Row]] # CTE name -> rows
|
||||||
|
ftsIndexes*: Table[string, fts.InvertedIndex] # table.col -> FTS index
|
||||||
txnManager*: TxnManager
|
txnManager*: TxnManager
|
||||||
pendingTxn*: Transaction
|
pendingTxn*: Transaction
|
||||||
onChange*: proc(ev: ChangeEvent) {.closure.}
|
onChange*: proc(ev: ChangeEvent) {.closure.}
|
||||||
@@ -133,6 +136,7 @@ proc newExecutionContext*(db: LSMTree): ExecutionContext =
|
|||||||
btrees: initTable[string, BTreeIndex[string, IndexEntry]](),
|
btrees: initTable[string, BTreeIndex[string, IndexEntry]](),
|
||||||
views: initTable[string, Node](),
|
views: initTable[string, Node](),
|
||||||
cteTables: initTable[string, seq[Row]](),
|
cteTables: initTable[string, seq[Row]](),
|
||||||
|
ftsIndexes: initTable[string, fts.InvertedIndex](),
|
||||||
users: initTable[string, UserDef](),
|
users: initTable[string, UserDef](),
|
||||||
policies: initTable[string, seq[PolicyDef]](),
|
policies: initTable[string, seq[PolicyDef]](),
|
||||||
currentUser: "", currentRole: "",
|
currentUser: "", currentRole: "",
|
||||||
@@ -299,6 +303,7 @@ proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
|
|||||||
ExecutionContext(db: ctx.db, tables: ctx.tables,
|
ExecutionContext(db: ctx.db, tables: ctx.tables,
|
||||||
btrees: ctx.btrees, views: ctx.views,
|
btrees: ctx.btrees, views: ctx.views,
|
||||||
cteTables: initTable[string, seq[Row]](),
|
cteTables: initTable[string, seq[Row]](),
|
||||||
|
ftsIndexes: ctx.ftsIndexes,
|
||||||
users: ctx.users, policies: ctx.policies,
|
users: ctx.users, policies: ctx.policies,
|
||||||
txnManager: ctx.txnManager,
|
txnManager: ctx.txnManager,
|
||||||
currentUser: ctx.currentUser, currentRole: ctx.currentRole,
|
currentUser: ctx.currentUser, currentRole: ctx.currentRole,
|
||||||
@@ -2115,6 +2120,20 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
|
|||||||
return okResult(msg="ALTER TABLE " & stmt.altName & " executed")
|
return okResult(msg="ALTER TABLE " & stmt.altName & " executed")
|
||||||
return errResult("Table '" & stmt.altName & "' does not exist")
|
return errResult("Table '" & stmt.altName & "' does not exist")
|
||||||
|
|
||||||
|
of nkRecoverToTimestamp:
|
||||||
|
let walPath = ctx.db.dir & "/wal.log"
|
||||||
|
let entries = readEntries(walPath)
|
||||||
|
var applied = 0
|
||||||
|
for entry in entries:
|
||||||
|
if entry.kind == wekPut:
|
||||||
|
ctx.db.put(cast[string](entry.key), entry.value)
|
||||||
|
inc applied
|
||||||
|
elif entry.kind == wekDelete:
|
||||||
|
ctx.db.delete(cast[string](entry.key))
|
||||||
|
inc applied
|
||||||
|
ctx.restoreSchema()
|
||||||
|
return okResult(msg="RECOVERED " & $applied & " entries from WAL")
|
||||||
|
|
||||||
of nkCreateView:
|
of nkCreateView:
|
||||||
ctx.views[stmt.cvName] = stmt.cvQuery
|
ctx.views[stmt.cvName] = stmt.cvQuery
|
||||||
let viewKey = "_schema:views:" & stmt.cvName
|
let viewKey = "_schema:views:" & stmt.cvName
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ type
|
|||||||
tkPolicy
|
tkPolicy
|
||||||
tkEnable
|
tkEnable
|
||||||
tkDisable
|
tkDisable
|
||||||
|
tkRecover
|
||||||
|
tkTimestamp
|
||||||
tkFor
|
tkFor
|
||||||
tkUsing
|
tkUsing
|
||||||
tkGrant
|
tkGrant
|
||||||
@@ -280,6 +282,8 @@ const keywords*: Table[string, TokenKind] = {
|
|||||||
"disable": tkDisable,
|
"disable": tkDisable,
|
||||||
"for": tkFor,
|
"for": tkFor,
|
||||||
"using": tkUsing,
|
"using": tkUsing,
|
||||||
|
"recover": tkRecover,
|
||||||
|
"timestamp": tkTimestamp,
|
||||||
"grant": tkGrant,
|
"grant": tkGrant,
|
||||||
"revoke": tkRevoke,
|
"revoke": tkRevoke,
|
||||||
"count": tkCount,
|
"count": tkCount,
|
||||||
|
|||||||
@@ -1181,6 +1181,13 @@ proc parseStatement*(p: var Parser): Node =
|
|||||||
of tkCommit: p.parseCommitTxn()
|
of tkCommit: p.parseCommitTxn()
|
||||||
of tkRollback: p.parseRollbackTxn()
|
of tkRollback: p.parseRollbackTxn()
|
||||||
of tkExplain: p.parseExplain()
|
of tkExplain: p.parseExplain()
|
||||||
|
of tkRecover:
|
||||||
|
let tok = p.advance()
|
||||||
|
discard p.expect(tkTo)
|
||||||
|
discard p.expect(tkTimestamp)
|
||||||
|
let tsLit = p.expect(tkStringLit)
|
||||||
|
Node(kind: nkRecoverToTimestamp, recoverTimestamp: tsLit.value,
|
||||||
|
line: tok.line, col: tok.col)
|
||||||
else:
|
else:
|
||||||
let tok = p.advance()
|
let tok = p.advance()
|
||||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||||
|
|||||||
@@ -2459,5 +2459,13 @@ suite "Parameterized queries":
|
|||||||
check r.success
|
check r.success
|
||||||
# Should find the row because 'text' is in 'full text search'
|
# Should find the row because 'text' is in 'full text search'
|
||||||
|
|
||||||
|
test "RECOVER TO TIMESTAMP parse":
|
||||||
|
let ast = parse("RECOVER TO TIMESTAMP '2026-05-07T12:00:00'")
|
||||||
|
check ast.stmts[0].kind == nkRecoverToTimestamp
|
||||||
|
|
||||||
|
test "RECOVER FROM WAL execution":
|
||||||
|
let r = qexec.executeQuery(ctx, parse("RECOVER TO TIMESTAMP '2026-12-31T23:59:59'"))
|
||||||
|
check r.success
|
||||||
|
|
||||||
# JOIN tests
|
# JOIN tests
|
||||||
include "join_tests"
|
include "join_tests"
|
||||||
|
|||||||
Reference in New Issue
Block a user