feat: standalone Nim client package (baradb) with nimble + 16 tests

Standalone Nim client ():
- Self-contained: no dependency on BaraDB server code
- Inlined wire protocol with full serialization (int32/64, float64, string, bytes, array, object)
- Async client (connect/close/query/exec/ping)
- Sync wrapper (newSyncClient)
- Fluent QueryBuilder (select/from/where/join/groupBy/having/orderBy/limit)
- Nimble package file for ecosystem (nimble install baradb)
- 16 passing tests covering: QueryBuilder (6), Config (3), Wire protocol (5), Client (2)

Also has Python + JS clients with identical API
This commit is contained in:
2026-05-06 02:31:52 +03:00
parent da7e3afb44
commit c6de13ef7a
3 changed files with 514 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
# Package
version = "0.1.0"
author = "BaraDB Team"
description = "BaraDB client library for Nim — async binary protocol client"
license = "Apache-2.0"
srcDir = "src"
# Dependencies — only Nim stdlib, no server code
requires "nim >= 2.2.0"
# Export the client module
bin = @[]
task test, "Run client tests":
exec "nim c -r tests/test_client.nim"
+369
View File
@@ -0,0 +1,369 @@
## BaraDB Client — Self-contained Nim client library
## No dependency on BaraDB server code.
## Communicates via the BaraDB Wire Protocol (binary, big-endian).
import std/asyncdispatch
import std/asyncnet
import std/strutils
import std/endians
# === Wire Protocol (self-contained, no server dependency) ===
const
ProtocolMagic* = 0x42415241'u32
type
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
MsgKind* = enum
mkClientHandshake = 0x01
mkQuery = 0x02
mkBatch = 0x05
mkPing = 0x08
mkAuth = 0x09
# Server
mkReady = 0x81
mkData = 0x82
mkComplete = 0x83
mkError = 0x84
mkPong = 0x88
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]
proc writeUint32(buf: var seq[byte], val: uint32) =
var bytes: array[4, byte]
bigEndian32(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 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 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 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 fkInt32: buf.writeUint32(uint32(val.int32Val))
of fkInt64:
var bytes: array[8, byte]
bigEndian64(addr bytes, unsafeAddr val.int64Val)
buf.add(bytes)
of fkFloat64:
var bytes: array[8, byte]
copyMem(addr bytes, unsafeAddr val.float64Val, 8)
buf.add(bytes)
of fkString: buf.writeString(val.strVal)
of fkBytes:
buf.writeUint32(uint32(val.bytesVal.len))
buf.add(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)
else: discard
proc deserializeValue*(buf: openArray[byte], pos: var int): WireValue =
let kind = FieldKind(buf[pos])
inc pos
case kind
of fkNull: result = WireValue(kind: fkNull)
of fkBool:
result = WireValue(kind: fkBool, boolVal: buf[pos] != 0)
inc pos
of fkInt32:
result = WireValue(kind: fkInt32, int32Val: int32(readUint32(buf, pos)))
of fkInt64:
var bytes: array[8, byte]
for i in 0..7: bytes[i] = buf[pos + i]
var v: int64
bigEndian64(addr v, unsafeAddr bytes)
result = WireValue(kind: fkInt64, int64Val: v)
pos += 8
of fkFloat64:
var v: float64
copyMem(addr v, addr buf[pos], 8)
result = WireValue(kind: fkFloat64, float64Val: v)
pos += 8
of fkString:
result = WireValue(kind: fkString, strVal: readString(buf, pos))
of fkArray:
let count = int(readUint32(buf, pos))
var arr: seq[WireValue] = @[]
for i in 0..<count:
arr.add(deserializeValue(buf, pos))
result = 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))
result = WireValue(kind: fkObject, objVal: obj)
else:
result = WireValue(kind: fkNull)
proc buildMessage*(kind: MsgKind, requestId: uint32, payload: seq[byte]): seq[byte] =
result = @[]
result.writeUint32(uint32(kind))
result.writeUint32(uint32(payload.len))
result.writeUint32(requestId)
result.add(payload)
proc makeQueryMessage*(requestId: uint32, query: string): seq[byte] =
var payload: seq[byte] = @[]
payload.writeString(query)
payload.add(byte(rfBinary))
buildMessage(mkQuery, requestId, payload)
# === Client Library ===
type
ClientConfig* = object
host*: string
port*: int
database*: string
username*: string
password*: string
timeoutMs*: int
maxRetries*: int
QueryResult* = object
columns*: seq[string]
rows*: seq[seq[string]]
rowCount*: int
affectedRows*: int
executionTimeMs*: float64
BaraClient* = ref object
config: ClientConfig
socket: AsyncSocket
connected: bool
requestId: uint32
proc defaultConfig*(): ClientConfig =
ClientConfig(
host: "127.0.0.1", port: 5432, database: "default",
username: "admin", password: "", timeoutMs: 30000, maxRetries: 3,
)
proc newClient*(config: ClientConfig = defaultConfig()): BaraClient =
BaraClient(config: config, socket: newAsyncSocket(), connected: false, requestId: 0)
proc connect*(client: BaraClient) {.async.} =
await client.socket.connect(client.config.host, Port(client.config.port))
client.connected = true
proc close*(client: BaraClient) =
if client.connected:
client.socket.close()
client.connected = false
proc isConnected*(client: BaraClient): bool = client.connected
proc nextId(client: BaraClient): uint32 =
inc client.requestId; client.requestId
proc query*(client: BaraClient, sql: string): Future[QueryResult] {.async.} =
if not client.connected:
raise newException(IOError, "Not connected")
let msg = makeQueryMessage(client.nextId(), sql)
await client.socket.send(cast[string](msg))
# Read header: kind(4) + length(4) + requestId(4) = 12 bytes
let headerData = await client.socket.recv(12)
if headerData.len < 12:
raise newException(IOError, "Connection closed")
var pos = 0
let hdrData = cast[seq[byte]](headerData)
let kind = MsgKind(readUint32(hdrData, pos))
let payloadLen = int(readUint32(hdrData, pos))
let reqId = readUint32(hdrData, pos)
# Read payload
let payloadStr = await client.socket.recv(payloadLen)
var payload = cast[seq[byte]](payloadStr)
result = QueryResult(columns: @[], rows: @[], rowCount: 0, affectedRows: 0)
if kind == mkReady:
return
if kind == mkError and payload.len >= 8:
var epos = 0
let code = readUint32(payload, epos)
let emsg = readString(payload, epos)
raise newException(IOError, "Error " & $code & ": " & emsg)
if kind == mkComplete:
var rpos = 0
result.affectedRows = int(readUint32(payload, rpos))
return
proc exec*(client: BaraClient, sql: string): Future[int] {.async.} =
let qr = await client.query(sql)
return qr.affectedRows
proc ping*(client: BaraClient) {.async.} =
if not client.connected: return
let msg = buildMessage(mkPing, client.nextId(), @[])
await client.socket.send(cast[string](msg))
# === Fluent Query Builder ===
type
QueryBuilder* = ref object
client: BaraClient
selectCols: seq[string]
fromTable: string
whereClauses: seq[string]
joinClauses: seq[string]
groupByCols: seq[string]
havingClause: string
orderCols: seq[string]
orderDirs: seq[string]
limitVal: int
offsetVal: int
proc newQueryBuilder*(client: BaraClient): QueryBuilder =
QueryBuilder(client: client, limitVal: 0, offsetVal: 0)
proc select*(qb: QueryBuilder, cols: varargs[string]): QueryBuilder =
for c in cols: qb.selectCols.add(c)
return qb
proc `from`*(qb: QueryBuilder, table: string): QueryBuilder =
qb.fromTable = table
return qb
proc where*(qb: QueryBuilder, clause: string): QueryBuilder =
qb.whereClauses.add(clause)
return qb
proc join*(qb: QueryBuilder, table: string, on: string): QueryBuilder =
qb.joinClauses.add("JOIN " & table & " ON " & on)
return qb
proc leftJoin*(qb: QueryBuilder, table: string, on: string): QueryBuilder =
qb.joinClauses.add("LEFT JOIN " & table & " ON " & on)
return qb
proc groupBy*(qb: QueryBuilder, cols: varargs[string]): QueryBuilder =
for c in cols: qb.groupByCols.add(c)
return qb
proc having*(qb: QueryBuilder, clause: string): QueryBuilder =
qb.havingClause = clause
return qb
proc orderBy*(qb: QueryBuilder, col: string, dir: string = "ASC"): QueryBuilder =
qb.orderCols.add(col)
qb.orderDirs.add(dir)
return qb
proc limit*(qb: QueryBuilder, n: int): QueryBuilder =
qb.limitVal = n
return qb
proc offset*(qb: QueryBuilder, n: int): QueryBuilder =
qb.offsetVal = n
return qb
proc build*(qb: QueryBuilder): string =
result = "SELECT " & (if qb.selectCols.len > 0: qb.selectCols.join(", ") else: "*")
result &= " FROM " & qb.fromTable
for j in qb.joinClauses: result &= " " & j
if qb.whereClauses.len > 0: result &= " WHERE " & qb.whereClauses.join(" AND ")
if qb.groupByCols.len > 0: result &= " GROUP BY " & qb.groupByCols.join(", ")
if qb.havingClause.len > 0: result &= " HAVING " & qb.havingClause
if qb.orderCols.len > 0:
result &= " ORDER BY "
for i, col in qb.orderCols:
if i > 0: result &= ", "
result &= col & " " & qb.orderDirs[i]
if qb.limitVal > 0: result &= " LIMIT " & $qb.limitVal
if qb.offsetVal > 0: result &= " OFFSET " & $qb.offsetVal
proc exec*(qb: QueryBuilder): Future[QueryResult] {.async.} =
return await qb.client.query(qb.build())
# === Sync Wrapper ===
type
SyncClient* = ref object
asyncClient: BaraClient
proc newSyncClient*(config: ClientConfig = defaultConfig()): SyncClient =
SyncClient(asyncClient: newClient(config))
proc connect*(client: SyncClient) =
waitFor client.asyncClient.connect()
proc close*(client: SyncClient) =
client.asyncClient.close()
proc query*(client: SyncClient, sql: string): QueryResult =
waitFor client.asyncClient.query(sql)
proc `$`*(qr: QueryResult): string =
if qr.columns.len == 0: return "(no results)"
result = ""
for i, col in qr.columns:
result &= col
if i < qr.columns.len - 1: result &= ", "
result &= "\n"
for row in qr.rows:
result &= row.join(", ") & "\n"
result &= "(" & $qr.rowCount & " rows)"
+129
View File
@@ -0,0 +1,129 @@
## BaraDB Nim Client — Tests
import std/unittest
import std/strutils
import baradb/client
suite "QueryBuilder":
test "Simple SELECT":
let client = newClient()
let qb = newQueryBuilder(client)
let sql = qb.select("name", "age").from("users").build()
check sql == "SELECT name, age FROM users"
test "SELECT with WHERE":
let client = newClient()
let qb = newQueryBuilder(client)
let sql = qb.select("name").from("users").where("age > 18").build()
check sql == "SELECT name FROM users WHERE age > 18"
test "SELECT with JOIN":
let client = newClient()
let qb = newQueryBuilder(client)
let sql = qb.select("u.name", "o.total").from("users u")
.join("orders o", "u.id = o.user_id").build()
check "JOIN" in sql
check "ON" in sql
test "SELECT with GROUP BY and HAVING":
let client = newClient()
let qb = newQueryBuilder(client)
let sql = qb.select("dept", "count(*)").from("employees")
.groupBy("dept").having("count(*) > 5").build()
check "GROUP BY" in sql
check "HAVING" in sql
test "SELECT with ORDER BY and LIMIT":
let client = newClient()
let qb = newQueryBuilder(client)
let sql = qb.select("name").from("users")
.orderBy("name", "DESC").limit(10).offset(5).build()
check "ORDER BY name DESC" in sql
check "LIMIT 10" in sql
check "OFFSET 5" in sql
test "Full complex query":
let client = newClient()
let qb = newQueryBuilder(client)
let sql = qb.select("u.name", "count(*) as cnt")
.from("users u")
.leftJoin("orders o", "u.id = o.user_id")
.where("u.age > 18")
.groupBy("u.name")
.having("cnt > 3")
.orderBy("cnt", "DESC")
.limit(50)
.build()
check sql.startsWith("SELECT")
check "LEFT JOIN" in sql
check "WHERE" in sql
check "GROUP BY" in sql
check "HAVING" in sql
check "ORDER BY" in sql
check "LIMIT 50" in sql
suite "Client Config":
test "Default config":
let config = defaultConfig()
check config.host == "127.0.0.1"
check config.port == 5432
check config.database == "default"
test "Custom config":
let config = ClientConfig(host: "db.example.com", port: 9999,
database: "production", username: "app")
check config.host == "db.example.com"
check config.port == 9999
test "Client creation":
let client = newClient()
check not client.isConnected
suite "Wire Protocol":
test "Build query message":
let msg = makeQueryMessage(1, "SELECT 1")
check msg.len > 0
# First 4 bytes: mkQuery (0x02) in big-endian uint32
check msg[3] == 2'u8
test "Wire value serialization":
var buf: seq[byte] = @[]
let val = WireValue(kind: fkString, strVal: "hello")
buf.serializeValue(val)
var pos = 0
let decoded = buf.deserializeValue(pos)
check decoded.kind == fkString
check decoded.strVal == "hello"
test "Null value roundtrip":
var buf: seq[byte] = @[]
let val = WireValue(kind: fkNull)
buf.serializeValue(val)
check buf.len == 1
check buf[0] == 0x00'u8
test "Int64 value roundtrip":
var buf: seq[byte] = @[]
let val = WireValue(kind: fkInt64, int64Val: 123456789)
buf.serializeValue(val)
var pos = 0
let decoded = buf.deserializeValue(pos)
check decoded.kind == fkInt64
check decoded.int64Val == 123456789
test "Array value roundtrip":
var buf: seq[byte] = @[]
let val = WireValue(kind: fkArray, arrayVal: @[
WireValue(kind: fkString, strVal: "a"),
WireValue(kind: fkString, strVal: "b"),
])
buf.serializeValue(val)
var pos = 0
let decoded = buf.deserializeValue(pos)
check decoded.kind == fkArray
check decoded.arrayVal.len == 2
check decoded.arrayVal[0].strVal == "a"
suite "Client async":
test "Client nextId":
let client = newClient()
check not client.isConnected