feat: Phase 2 — MVCC transactions wired into server pipeline
- Added TxnManager to server and ExecutionContext - Per-connection ExecutionContext with isolated transaction state - BEGIN creates new transaction via MVCC - INSERT/DELETE uses transaction write buffer when active - COMMIT flushes write set to LSM-Tree - ROLLBACK aborts and discards pending writes - cloneForConnection() shares tables/btrees/data, isolates transactions - All 216 tests pass
This commit is contained in:
@@ -4,6 +4,7 @@ import std/asyncnet
|
|||||||
import std/strutils
|
import std/strutils
|
||||||
import std/os
|
import std/os
|
||||||
import std/endians
|
import std/endians
|
||||||
|
import std/tables
|
||||||
import config
|
import config
|
||||||
import ../protocol/wire
|
import ../protocol/wire
|
||||||
import ../query/lexer
|
import ../query/lexer
|
||||||
@@ -11,6 +12,7 @@ import ../query/parser
|
|||||||
import ../query/ast
|
import ../query/ast
|
||||||
import ../query/executor
|
import ../query/executor
|
||||||
import ../storage/lsm
|
import ../storage/lsm
|
||||||
|
import ../core/mvcc
|
||||||
|
|
||||||
type
|
type
|
||||||
Server* = ref object
|
Server* = ref object
|
||||||
@@ -18,15 +20,20 @@ type
|
|||||||
running: bool
|
running: bool
|
||||||
db: LSMTree
|
db: LSMTree
|
||||||
ctx: ExecutionContext
|
ctx: ExecutionContext
|
||||||
|
txnManager: TxnManager
|
||||||
|
|
||||||
ClientConnection = ref object
|
ClientConnection = ref object
|
||||||
socket: AsyncSocket
|
socket: AsyncSocket
|
||||||
id: int
|
id: int
|
||||||
|
currentTxn: Transaction
|
||||||
|
|
||||||
proc newServer*(config: BaraConfig): Server =
|
proc newServer*(config: BaraConfig): Server =
|
||||||
let dataDir = config.dataDir / "server"
|
let dataDir = config.dataDir / "server"
|
||||||
let db = newLSMTree(dataDir)
|
let db = newLSMTree(dataDir)
|
||||||
Server(config: config, running: false, db: db, ctx: newExecutionContext(db))
|
let ctx = newExecutionContext(db)
|
||||||
|
ctx.txnManager = newTxnManager()
|
||||||
|
Server(config: config, running: false, db: db, ctx: ctx,
|
||||||
|
txnManager: ctx.txnManager)
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
# Wire Protocol Helpers
|
# Wire Protocol Helpers
|
||||||
@@ -141,6 +148,7 @@ proc recvExact(client: AsyncSocket, size: int): Future[string] {.async.} =
|
|||||||
|
|
||||||
proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.} =
|
proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.} =
|
||||||
echo "Client ", clientId, " connected"
|
echo "Client ", clientId, " connected"
|
||||||
|
var connCtx = cloneForConnection(server.ctx)
|
||||||
try:
|
try:
|
||||||
while true:
|
while true:
|
||||||
# Read 12-byte header
|
# Read 12-byte header
|
||||||
@@ -166,7 +174,7 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
|
|||||||
let queryStr = readString(cast[seq[byte]](payload), pos)
|
let queryStr = readString(cast[seq[byte]](payload), pos)
|
||||||
echo "[", clientId, "] Query: ", queryStr
|
echo "[", clientId, "] Query: ", queryStr
|
||||||
|
|
||||||
let (success, result, errorMsg) = executeQuery(server.db, server.ctx, queryStr)
|
let (success, result, errorMsg) = executeQuery(server.db, connCtx, queryStr)
|
||||||
if success:
|
if success:
|
||||||
if result.rows.len > 0:
|
if result.rows.len > 0:
|
||||||
let dataMsg = serializeResult(result, header.requestId)
|
let dataMsg = serializeResult(result, header.requestId)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ir
|
|||||||
import ../core/types
|
import ../core/types
|
||||||
import ../storage/lsm
|
import ../storage/lsm
|
||||||
import ../storage/btree
|
import ../storage/btree
|
||||||
|
import ../core/mvcc
|
||||||
|
|
||||||
type
|
type
|
||||||
IndexEntry = ref object
|
IndexEntry = ref object
|
||||||
@@ -17,6 +18,8 @@ type
|
|||||||
db*: LSMTree
|
db*: LSMTree
|
||||||
tables*: Table[string, TableDef] # table name -> definition
|
tables*: Table[string, TableDef] # table name -> definition
|
||||||
btrees*: Table[string, BTreeIndex[string, IndexEntry]] # index name -> btree
|
btrees*: Table[string, BTreeIndex[string, IndexEntry]] # index name -> btree
|
||||||
|
txnManager*: TxnManager
|
||||||
|
pendingTxn*: Transaction # active transaction for this context
|
||||||
|
|
||||||
TableDef* = object
|
TableDef* = object
|
||||||
name*: string
|
name*: string
|
||||||
@@ -37,6 +40,12 @@ proc newExecutionContext*(db: LSMTree): ExecutionContext =
|
|||||||
ExecutionContext(db: db, tables: initTable[string, TableDef](),
|
ExecutionContext(db: db, tables: initTable[string, TableDef](),
|
||||||
btrees: initTable[string, BTreeIndex[string, IndexEntry]]())
|
btrees: initTable[string, BTreeIndex[string, IndexEntry]]())
|
||||||
|
|
||||||
|
proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
|
||||||
|
## Create a per-connection context that shares the same data but has its own transaction.
|
||||||
|
ExecutionContext(db: ctx.db, tables: ctx.tables,
|
||||||
|
btrees: ctx.btrees, txnManager: ctx.txnManager,
|
||||||
|
pendingTxn: nil)
|
||||||
|
|
||||||
proc execScan(ctx: ExecutionContext, table: string): seq[Row] =
|
proc execScan(ctx: ExecutionContext, table: string): seq[Row] =
|
||||||
## Full table scan via LSM-Tree memtable scan.
|
## Full table scan via LSM-Tree memtable scan.
|
||||||
## Rows are stored as: "{table}.{key}" -> value
|
## Rows are stored as: "{table}.{key}" -> value
|
||||||
@@ -66,7 +75,6 @@ proc execPointRead(ctx: ExecutionContext, table: string, key: string): seq[Row]
|
|||||||
return @[]
|
return @[]
|
||||||
|
|
||||||
proc execInsert(ctx: ExecutionContext, table: string, fields: seq[string], values: seq[seq[string]]): int =
|
proc execInsert(ctx: ExecutionContext, table: string, fields: seq[string], values: seq[seq[string]]): int =
|
||||||
## Insert rows into LSM-Tree with constraint validation and B-Tree index population.
|
|
||||||
var count = 0
|
var count = 0
|
||||||
for rowVals in values:
|
for rowVals in values:
|
||||||
var key = ""
|
var key = ""
|
||||||
@@ -83,9 +91,14 @@ proc execInsert(ctx: ExecutionContext, table: string, fields: seq[string], value
|
|||||||
valParts.add(f & "=")
|
valParts.add(f & "=")
|
||||||
let valStr = valParts.join(",")
|
let valStr = valParts.join(",")
|
||||||
let fullKey = table & "." & key
|
let fullKey = table & "." & key
|
||||||
|
|
||||||
|
# Use MVCC transaction if active, otherwise write directly
|
||||||
|
if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive:
|
||||||
|
discard ctx.txnManager.write(ctx.pendingTxn, fullKey, cast[seq[byte]](valStr))
|
||||||
|
else:
|
||||||
ctx.db.put(fullKey, cast[seq[byte]](valStr))
|
ctx.db.put(fullKey, cast[seq[byte]](valStr))
|
||||||
|
|
||||||
# Populate B-Tree indexes
|
# Populate B-Tree indexes (always direct, not transactional for now)
|
||||||
for colName, btIdx in ctx.btrees:
|
for colName, btIdx in ctx.btrees:
|
||||||
if colName.startsWith(table & "."):
|
if colName.startsWith(table & "."):
|
||||||
let colOnly = colName[table.len + 1..^1]
|
let colOnly = colName[table.len + 1..^1]
|
||||||
@@ -228,6 +241,9 @@ proc execDelete(ctx: ExecutionContext, table: string, key: string): int =
|
|||||||
let fullKey = table & "." & key
|
let fullKey = table & "." & key
|
||||||
let (found, _) = ctx.db.get(fullKey)
|
let (found, _) = ctx.db.get(fullKey)
|
||||||
if found:
|
if found:
|
||||||
|
if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive:
|
||||||
|
discard ctx.txnManager.delete(ctx.pendingTxn, fullKey)
|
||||||
|
else:
|
||||||
ctx.db.delete(fullKey)
|
ctx.db.delete(fullKey)
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
@@ -580,13 +596,33 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node): (bool, string, int) =
|
|||||||
return (true, "", 0)
|
return (true, "", 0)
|
||||||
|
|
||||||
of nkBeginTxn:
|
of nkBeginTxn:
|
||||||
|
if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive:
|
||||||
|
# Auto-commit previous pending transaction
|
||||||
|
discard ctx.txnManager.commit(ctx.pendingTxn)
|
||||||
|
ctx.pendingTxn = ctx.txnManager.beginTxn(ilReadCommitted)
|
||||||
return (true, "", 0)
|
return (true, "", 0)
|
||||||
|
|
||||||
of nkCommitTxn:
|
of nkCommitTxn:
|
||||||
|
if ctx.pendingTxn != nil and ctx.pendingTxn.state == tsActive:
|
||||||
|
# Flush write set to LSM-Tree
|
||||||
|
for key, version in ctx.pendingTxn.writeSet:
|
||||||
|
if version.value == @[]:
|
||||||
|
ctx.db.delete(key)
|
||||||
|
else:
|
||||||
|
ctx.db.put(key, version.value)
|
||||||
|
discard ctx.txnManager.commit(ctx.pendingTxn)
|
||||||
|
ctx.pendingTxn = nil
|
||||||
return (true, "", 0)
|
return (true, "", 0)
|
||||||
|
else:
|
||||||
|
return (false, "No active transaction to commit", 0)
|
||||||
|
|
||||||
of nkRollbackTxn:
|
of nkRollbackTxn:
|
||||||
|
if ctx.pendingTxn != nil:
|
||||||
|
discard ctx.txnManager.abortTxn(ctx.pendingTxn)
|
||||||
|
ctx.pendingTxn = nil
|
||||||
return (true, "", 0)
|
return (true, "", 0)
|
||||||
|
else:
|
||||||
|
return (false, "No active transaction to rollback", 0)
|
||||||
|
|
||||||
of nkCreateType:
|
of nkCreateType:
|
||||||
return (true, "", 0)
|
return (true, "", 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user