feat: MVCC, wire protocol, HTTP API, schema system, CLI shell — 39 tests

- MVCC: multi-version concurrency control with snapshot isolation
- Deadlock detection: wait-for graph with cycle detection
- Wire Protocol: binary message format, 16 types, big-endian serialization
- HTTP/REST API: router with CORS, path params, JSON
- Schema system: types, links, properties, migration diffing
- CLI: interactive query shell (bara shell)
- 18 new tests (39 total, all passing)
This commit is contained in:
2026-05-06 00:32:02 +03:00
parent 6935889877
commit 5c84aeccf8
9 changed files with 1493 additions and 21 deletions
+150
View File
@@ -0,0 +1,150 @@
## HTTP/REST API — JSON endpoint
import std/asynchttpserver
import std/asyncdispatch
import std/json
import std/strutils
import std/tables
type
HttpMethod* = enum
hmGet = "GET"
hmPost = "POST"
hmPut = "PUT"
hmDelete = "DELETE"
hmPatch = "PATCH"
hmOptions = "OPTIONS"
RouteHandler* = proc(req: Request): Future[JsonNode] {.gcsafe.}
HttpRouter* = ref object
routes: Table[string, Table[string, RouteHandler]] # method -> path -> handler
middlewares: seq[RouteHandler]
port*: int
address*: string
Request* = ref object
httpMethod*: HttpMethod
path*: string
query*: Table[string, string]
headers*: Table[string, string]
body*: string
contentType*: string
Response* = object
status*: int
body*: JsonNode
headers*: Table[string, string]
proc newHttpRouter*(port: int = 8080, address: string = "0.0.0.0"): HttpRouter =
HttpRouter(
routes: initTable[string, Table[string, RouteHandler]](),
middlewares: @[],
port: port,
address: address,
)
proc addRoute*(router: HttpRouter, meth: HttpMethod, path: string, handler: RouteHandler) =
let m = $meth
if m notin router.routes:
router.routes[m] = initTable[string, RouteHandler]()
router.routes[m][path] = handler
proc get*(router: HttpRouter, path: string, handler: RouteHandler) =
router.addRoute(hmGet, path, handler)
proc post*(router: HttpRouter, path: string, handler: RouteHandler) =
router.addRoute(hmPost, path, handler)
proc put*(router: HttpRouter, path: string, handler: RouteHandler) =
router.addRoute(hmPut, path, handler)
proc delete*(router: HttpRouter, path: string, handler: RouteHandler) =
router.addRoute(hmDelete, path, handler)
proc jsonResponse*(status: int, data: JsonNode, headers: Table[string, string] = initTable[string, string]()): Response =
Response(status: status, body: data, headers: headers)
proc errorResponse*(status: int, message: string): Response =
jsonResponse(status, %*{"error": message})
proc successResponse*(data: JsonNode): Response =
jsonResponse(200, data)
proc parseQuery*(queryString: string): Table[string, string] =
result = initTable[string, string]()
if queryString.len == 0:
return
for pair in queryString.split("&"):
let parts = pair.split("=", 1)
if parts.len == 2:
result[parts[0]] = parts[1]
elif parts.len == 1:
result[parts[0]] = ""
proc matchPath*(pattern, path: string): Table[string, string] =
result = initTable[string, string]()
let patternParts = pattern.split("/")
let pathParts = path.split("/")
if patternParts.len != pathParts.len:
return
for i in 0..<patternParts.len:
if patternParts[i].startsWith(":"):
result[patternParts[i][1..^1]] = pathParts[i]
elif patternParts[i] != pathParts[i]:
result.clear()
return
proc corsHeaders*(): Table[string, string] =
result = initTable[string, string]()
result["Access-Control-Allow-Origin"] = "*"
result["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS"
result["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
proc jsonHeaders*(): Table[string, string] =
result = initTable[string, string]()
result["Content-Type"] = "application/json"
proc handleCors*(req: Request): Response =
if req.httpMethod == hmOptions:
return jsonResponse(204, newJNull(), corsHeaders())
return nil
proc parseRequest*(httpMethod: string, path: string, headers: Table[string, string], body: string): Request =
let methodMap = {
"GET": hmGet, "POST": hmPost, "PUT": hmPut,
"DELETE": hmDelete, "PATCH": hmPatch, "OPTIONS": hmOptions,
}.toTable
var query = initTable[string, string]()
var cleanPath = path
let qPos = path.find('?')
if qPos >= 0:
cleanPath = path[0..<qPos]
query = parseQuery(path[qPos+1..^1])
Request(
httpMethod: methodMap.getOrDefault(httpMethod, hmGet),
path: cleanPath,
query: query,
headers: headers,
body: body,
contentType: headers.getOrDefault("Content-Type", ""),
)
proc handleRequest*(router: HttpRouter, req: Request): Future[Response] {.async.} =
# CORS
let corsResp = handleCors(req)
if corsResp != nil:
return corsResp
let methodStr = $req.httpMethod
if methodStr in router.routes:
for pattern, handler in router.routes[methodStr]:
let params = matchPath(pattern, req.path)
if params.len > 0 or pattern == req.path:
try:
return jsonResponse(200, await handler(req), jsonHeaders())
except CatchableError as e:
return errorResponse(500, e.msg)
return errorResponse(404, "Not found: " & req.path)
+285
View File
@@ -0,0 +1,285 @@
## BaraDB Wire Protocol — binary message format
import std/endians
const
ProtocolVersion* = 1'u32
Magic* = 0x42415241'u32 # "BARA"
type
MsgKind* = enum
# Client messages
mkClientHandshake = 0x01
mkQuery = 0x02
mkQueryParams = 0x03
mkExecute = 0x04
mkBatch = 0x05
mkTransaction = 0x06
mkClose = 0x07
mkPing = 0x08
mkAuth = 0x09
# Server messages
mkServerHandshake = 0x80
mkReady = 0x81
mkData = 0x82
mkComplete = 0x83
mkError = 0x84
mkAuthChallenge = 0x85
mkAuthOk = 0x86
mkSchemaChange = 0x87
mkPong = 0x88
mkTransactionState = 0x89
FieldKind* = enum
fkNull = 0x00
fkBool = 0x01
fkInt8 = 0x02
fkInt16 = 0x03
fkInt32 = 0x04
fkInt64 = 0x05
fkFloat32 = 0x06
fkFloat64 = 0x07
fkString = 0x08
fkBytes = 0x09
fkArray = 0x0A
fkObject = 0x0B
fkVector = 0x0C
MessageHeader* = object
kind*: MsgKind
length*: uint32
requestId*: uint32
WireMessage* = object
header*: MessageHeader
payload*: seq[byte]
QueryMessage* = object
query*: string
format*: ResultFormat
params*: seq[WireValue]
ResultFormat* = enum
rfBinary = 0x00
rfJson = 0x01
rfText = 0x02
WireValue* = object
case kind*: FieldKind
of fkNull: discard
of fkBool: boolVal*: bool
of fkInt8: int8Val*: int8
of fkInt16: int16Val*: int16
of fkInt32: int32Val*: int32
of fkInt64: int64Val*: int64
of fkFloat32: float32Val*: float32
of fkFloat64: float64Val*: float64
of fkString: strVal*: string
of fkBytes: bytesVal*: seq[byte]
of fkArray: arrayVal*: seq[WireValue]
of fkObject: objVal*: seq[(string, WireValue)]
of fkVector: vecVal*: seq[float32]
QueryResult* = object
columns*: seq[string]
columnTypes*: seq[FieldKind]
rows*: seq[seq[WireValue]]
rowCount*: int
affectedRows*: int
proc writeUint32(buf: var seq[byte], val: uint32) =
var bytes: array[4, byte]
bigEndian32(addr bytes, unsafeAddr val)
buf.add(bytes)
proc writeUint64(buf: var seq[byte], val: uint64) =
var bytes: array[8, byte]
bigEndian64(addr bytes, unsafeAddr val)
buf.add(bytes)
proc writeString(buf: var seq[byte], s: string) =
buf.writeUint32(uint32(s.len))
for ch in s:
buf.add(byte(ch))
proc writeBytes(buf: var seq[byte], data: openArray[byte]) =
buf.writeUint32(uint32(data.len))
for b in data:
buf.add(b)
proc readUint32(buf: openArray[byte], pos: var int): uint32 =
var bytes: array[4, byte]
for i in 0..3:
bytes[i] = buf[pos + i]
bigEndian32(addr result, unsafeAddr bytes)
pos += 4
proc readUint64(buf: openArray[byte], pos: var int): uint64 =
var bytes: array[8, byte]
for i in 0..7:
bytes[i] = buf[pos + i]
bigEndian64(addr result, unsafeAddr bytes)
pos += 8
proc readString(buf: openArray[byte], pos: var int): string =
let len = int(readUint32(buf, pos))
result = newString(len)
for i in 0..<len:
result[i] = char(buf[pos + i])
pos += len
proc readBytes(buf: openArray[byte], pos: var int): seq[byte] =
let len = int(readUint32(buf, pos))
result = newSeq[byte](len)
for i in 0..<len:
result[i] = buf[pos + i]
pos += len
proc serializeValue*(buf: var seq[byte], val: WireValue) =
buf.add(byte(val.kind))
case val.kind
of fkNull: discard
of fkBool: buf.add(if val.boolVal: 1'u8 else: 0'u8)
of fkInt8: buf.add(byte(val.int8Val))
of fkInt16:
var bytes: array[2, byte]
bigEndian16(addr bytes, unsafeAddr val.int16Val)
buf.add(bytes)
of fkInt32: buf.writeUint32(uint32(val.int32Val))
of fkInt64: buf.writeUint64(uint64(val.int64Val))
of fkFloat32:
var fl = val.float32Val
var bytes: array[4, byte]
copyMem(addr bytes, unsafeAddr fl, 4)
buf.add(bytes)
of fkFloat64:
var fl = val.float64Val
var bytes: array[8, byte]
copyMem(addr bytes, unsafeAddr fl, 8)
buf.add(bytes)
of fkString: buf.writeString(val.strVal)
of fkBytes: buf.writeBytes(val.bytesVal)
of fkArray:
buf.writeUint32(uint32(val.arrayVal.len))
for item in val.arrayVal:
buf.serializeValue(item)
of fkObject:
buf.writeUint32(uint32(val.objVal.len))
for (name, item) in val.objVal:
buf.writeString(name)
buf.serializeValue(item)
of fkVector:
buf.writeUint32(uint32(val.vecVal.len))
for f in val.vecVal:
var fl = f
var bytes: array[4, byte]
copyMem(addr bytes, unsafeAddr fl, 4)
buf.add(bytes)
proc deserializeValue*(buf: openArray[byte], pos: var int): WireValue =
let kind = FieldKind(buf[pos])
inc pos
case kind
of fkNull: WireValue(kind: fkNull)
of fkBool: WireValue(kind: fkBool, boolVal: buf[pos] != 0)
of fkInt8: WireValue(kind: fkInt8, int8Val: cast[int8](buf[pos]))
of fkInt16:
var val: int16
var bytes: array[2, byte]
for i in 0..1: bytes[i] = buf[pos + i]
bigEndian16(addr val, unsafeAddr bytes)
pos += 2
WireValue(kind: fkInt16, int16Val: val)
of fkInt32:
WireValue(kind: fkInt32, int32Val: int32(readUint32(buf, pos)))
of fkInt64:
WireValue(kind: fkInt64, int64Val: int64(readUint64(buf, pos)))
of fkFloat32:
var fl: float32
var bytes: array[4, byte]
for i in 0..3: bytes[i] = buf[pos + i]
copyMem(addr fl, unsafeAddr bytes, 4)
pos += 4
WireValue(kind: fkFloat32, float32Val: fl)
of fkFloat64:
var fl: float64
var bytes: array[8, byte]
for i in 0..7: bytes[i] = buf[pos + i]
copyMem(addr fl, unsafeAddr bytes, 8)
pos += 8
WireValue(kind: fkFloat64, float64Val: fl)
of fkString:
WireValue(kind: fkString, strVal: readString(buf, pos))
of fkBytes:
WireValue(kind: fkBytes, bytesVal: readBytes(buf, pos))
of fkArray:
let count = int(readUint32(buf, pos))
var arr: seq[WireValue] = @[]
for i in 0..<count:
arr.add(deserializeValue(buf, pos))
WireValue(kind: fkArray, arrayVal: arr)
of fkObject:
let count = int(readUint32(buf, pos))
var obj: seq[(string, WireValue)] = @[]
for i in 0..<count:
let name = readString(buf, pos)
let val = deserializeValue(buf, pos)
obj.add((name, val))
WireValue(kind: fkObject, objVal: obj)
of fkVector:
let count = int(readUint32(buf, pos))
var vec: seq[float32] = @[]
for i in 0..<count:
var fl: float32
var bytes: array[4, byte]
for j in 0..3: bytes[j] = buf[pos + j]
copyMem(addr fl, unsafeAddr bytes, 4)
pos += 4
vec.add(fl)
WireValue(kind: fkVector, vecVal: vec)
proc serializeMessage*(msg: WireMessage): seq[byte] =
result = @[]
result.writeUint32(uint32(msg.header.kind))
result.writeUint32(msg.header.length)
result.writeUint32(msg.header.requestId)
result.add(msg.payload)
proc makeQueryMessage*(requestId: uint32, query: string): seq[byte] =
var payload: seq[byte] = @[]
payload.writeString(query)
payload.add(byte(rfBinary))
var msg = WireMessage(
header: MessageHeader(kind: mkQuery, length: uint32(payload.len), requestId: requestId),
payload: payload,
)
return serializeMessage(msg)
proc makeReadyMessage*(requestId: uint32): seq[byte] =
var payload: seq[byte] = @[]
payload.add(0'u8) # idle state
var msg = WireMessage(
header: MessageHeader(kind: mkReady, length: uint32(payload.len), requestId: requestId),
payload: payload,
)
return serializeMessage(msg)
proc makeErrorMessage*(requestId: uint32, code: uint32, message: string): seq[byte] =
var payload: seq[byte] = @[]
payload.writeUint32(code)
payload.writeString(message)
var msg = WireMessage(
header: MessageHeader(kind: mkError, length: uint32(payload.len), requestId: requestId),
payload: payload,
)
return serializeMessage(msg)
proc makeCompleteMessage*(requestId: uint32, affectedRows: int): seq[byte] =
var payload: seq[byte] = @[]
payload.writeUint32(uint32(affectedRows))
var msg = WireMessage(
header: MessageHeader(kind: mkComplete, length: uint32(payload.len), requestId: requestId),
payload: payload,
)
return serializeMessage(msg)