## 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/net as netmod import std/locks 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 fkJson = 0x0D 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 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] of fkJson: jsonVal*: string 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 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.." of fkArray: return "" of fkObject: return "" of fkVector: return "" of fkJson: return wv.jsonVal proc readQueryResponse(client: BaraClient): Future[QueryResult] {.async.} = let headerData = await client.socket.recv(12) if headerData.len < 12: raise newException(IOError, "Connection closed") var pos = 0 let hdrData = toBytes(headerData) let kind = MsgKind(readUint32(hdrData, pos)) let payloadLen = int(readUint32(hdrData, pos)) discard readUint32(hdrData, pos) let payloadStr = await client.socket.recv(payloadLen) var payload = toBytes(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 == mkData: var dpos = 0 let colCount = int(readUint32(payload, dpos)) var cols: seq[string] = @[] for i in 0..= 12: var chPos = 0 let chData = toBytes(compHeader) let compKind = MsgKind(readUint32(chData, chPos)) let compLen = int(readUint32(chData, chPos)) discard readUint32(chData, chPos) let compPayloadStr = await client.socket.recv(compLen) if compKind == mkComplete: var cpPos = 0 result.affectedRows = int(readUint32(toBytes(compPayloadStr), cpPos)) return if kind == mkComplete: var rpos = 0 result.affectedRows = int(readUint32(payload, rpos)) return proc query*(client: BaraClient, sql: string): Future[QueryResult] {.async.} = if not client.connected: raise newException(IOError, "Not connected") let msg = makeQueryMessage(client.nextId(), sql) let msgStr = toString(msg) await client.socket.send(msgStr) return await client.readQueryResponse() proc query*(client: BaraClient, sql: string, params: seq[WireValue]): Future[QueryResult] {.async.} = if not client.connected: raise newException(IOError, "Not connected") let msg = makeQueryParamsMessage(client.nextId(), sql, params) let msgStr = toString(msg) await client.socket.send(msgStr) return await client.readQueryResponse() proc exec*(client: BaraClient, sql: string): Future[int] {.async.} = let qr = await client.query(sql) return qr.affectedRows proc auth*(client: BaraClient, token: string) {.async.} = if not client.connected: raise newException(IOError, "Not connected") let msg = makeAuthMessage(client.nextId(), token) let msgStr = toString(msg) await client.socket.send(msgStr) let headerData = await client.socket.recv(12) if headerData.len < 12: raise newException(IOError, "Connection closed") var pos = 0 let hdrData = toBytes(headerData) let kind = MsgKind(readUint32(hdrData, pos)) let payloadLen = int(readUint32(hdrData, pos)) discard readUint32(hdrData, pos) if kind == mkAuthOk: return elif kind == mkError: let payloadStr = await client.socket.recv(payloadLen) var epos = 0 let emsg = readString(toBytes(payloadStr), epos) raise newException(IOError, "Auth failed: " & emsg) else: raise newException(IOError, "Unexpected auth response: 0x" & toHex(uint32(kind), 2)) proc ping*(client: BaraClient): Future[bool] {.async.} = if not client.connected: return false let msg = buildMessage(mkPing, client.nextId(), @[]) let msgStr = toString(msg) await client.socket.send(msgStr) let headerData = await client.socket.recv(12) if headerData.len < 12: return false var pos = 0 let hdrData = toBytes(headerData) let kind = MsgKind(readUint32(hdrData, pos)) return kind == mkPong # === 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()) # === Blocking Sync Client (production-grade, no waitFor) === type SyncClient* = ref object config: ClientConfig socket: netmod.Socket connected: bool requestId: uint32 lock: Lock proc newSyncClient*(config: ClientConfig = defaultConfig()): SyncClient = result = SyncClient(config: config, connected: false, requestId: 0) result.socket = netmod.newSocket() initLock(result.lock) proc recvExact(sock: netmod.Socket, size: int): string = result = "" while result.len < size: let chunk = sock.recv(size - result.len) if chunk.len == 0: raise newException(IOError, "Connection closed") result.add(chunk) proc readQueryResponseBlocking(client: SyncClient): QueryResult = let headerData = client.socket.recvExact(12) var pos = 0 let hdrData = toBytes(headerData) let kind = MsgKind(readUint32(hdrData, pos)) let payloadLen = int(readUint32(hdrData, pos)) discard readUint32(hdrData, pos) let payloadStr = client.socket.recvExact(payloadLen) var payload = toBytes(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 == mkData: var dpos = 0 let colCount = int(readUint32(payload, dpos)) var cols: seq[string] = @[] for i in 0..