Files
Baradb/src/barabadb/client/client.nim
T
dimgigov 9d71edafd4 Сесия 9: Седмици 1+2 — Stabilization + Type Safety
- Поправени deficiencies #5-#8 (GROUP BY bare columns, aggregate names,
  sync client с blocking socket, thread-safe Lock в SyncClient)
- Добавен BlockingClient (net.Socket + Lock) в clients/nim/src/baradb/client.nim
- Обновен src/barabadb/client/client.nim със sync клиент без waitFor
- Regression тестове за всички 10 deficiencies

- Добавен valueKind в IRExpr за типова информация
- evalExprValue връща Value discriminated union
- Премахнати parseFloat евристики от irAdd/Sub/Mul/Div/Mod/Pow/Neg
- INT+INT→INT, INT+FLOAT→FLOAT, FLOAT/INT→FLOAT
- 12 нови теста за type safety

Тестове: 311 — 0 failures
Build: 0 warnings
2026-05-17 10:21:15 +03:00

255 lines
6.9 KiB
Nim

## BaraDB Client — Nim client library
import std/asyncdispatch
import std/asyncnet
import std/net as netmod
import std/locks
import std/strutils
import ../protocol/wire
type
ClientConfig* = object
host*: string
port*: int
database*: string
username*: string
password*: string
timeout*: int # milliseconds
maxRetries*: int
QueryResult* = object
columns*: seq[string]
rows*: seq[seq[string]]
rowCount*: int
affectedRows*: int
executionTime*: float64 # seconds
BaraClient* = ref object
config: ClientConfig
socket: AsyncSocket
connected: bool
requestId: uint32
proc defaultClientConfig*(): ClientConfig =
ClientConfig(
host: "127.0.0.1",
port: 9472,
database: "default",
username: "admin",
password: "",
timeout: 30000,
maxRetries: 3,
)
proc newBaraClient*(config: ClientConfig = defaultClientConfig()): 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 disconnect*(client: BaraClient) {.async.} =
if client.connected:
client.socket.close()
client.connected = false
proc nextRequestId(client: BaraClient): uint32 =
inc client.requestId
return client.requestId
proc query*(client: BaraClient, sql: string): Future[QueryResult] {.async.} =
if not client.connected:
raise newException(IOError, "Not connected")
let reqId = client.nextRequestId()
let msg = makeQueryMessage(reqId, sql)
await client.socket.send(cast[string](msg))
# Read response
let response = await client.socket.recv(8192)
if response.len == 0:
raise newException(IOError, "Connection closed by server")
result = QueryResult(columns: @[], rows: @[], rowCount: 0, affectedRows: 0)
# Parse response (simplified)
if response.len > 0:
result.rowCount = 0
proc execute*(client: BaraClient, sql: string): Future[int] {.async.} =
let qr = await client.query(sql)
return qr.affectedRows
proc isConnected*(client: BaraClient): bool = client.connected
# Synchronous wrapper (blocking socket, thread-safe)
type
SyncClient* = ref object
config: ClientConfig
socket: netmod.Socket
connected: bool
requestId: uint32
lock: Lock
proc newSyncClient*(config: ClientConfig = defaultClientConfig()): SyncClient =
result = SyncClient(config: config, connected: false, requestId: 0)
result.socket = netmod.newSocket()
initLock(result.lock)
proc connect*(client: SyncClient) =
netmod.connect(client.socket, client.config.host, Port(client.config.port))
client.connected = true
proc disconnect*(client: SyncClient) =
if client.connected:
try:
let msg = makeQueryMessage(0, "DISCONNECT")
netmod.send(client.socket, cast[string](msg))
except: discard
netmod.close(client.socket)
client.connected = false
deinitLock(client.lock)
proc query*(client: SyncClient, sql: string): QueryResult =
acquire(client.lock)
try:
if not client.connected:
raise newException(IOError, "Not connected")
let reqId = client.requestId + 1
client.requestId = reqId
let msg = makeQueryMessage(reqId, sql)
netmod.send(client.socket, cast[string](msg))
# Simplified response read — read until we get something
var response = ""
while response.len == 0:
response = netmod.recv(client.socket, 8192)
if response.len == 0:
raise newException(IOError, "Connection closed by server")
result = QueryResult(columns: @[], rows: @[], rowCount: 0, affectedRows: 0)
finally:
release(client.lock)
proc execute*(client: SyncClient, sql: string): int =
let qr = client.query(sql)
return qr.affectedRows
proc isConnected*(client: SyncClient): bool =
client.connected
# Connection string parser
proc parseConnectionString*(connStr: string): ClientConfig =
result = defaultClientConfig()
let parts = connStr.split(" ")
for part in parts:
let kv = part.split("=", 1)
if kv.len == 2:
case kv[0].toLower()
of "host": result.host = kv[1]
of "port": result.port = parseInt(kv[1])
of "database", "dbname": result.database = kv[1]
of "user", "username": result.username = kv[1]
of "password", "pass": result.password = kv[1]
of "connect_timeout", "timeout": result.timeout = parseInt(kv[1])
else: discard
# Fluent query builder
type
QueryBuilder* = ref object
client: BaraClient
selectCols: seq[string]
fromTable: string
whereClauses: seq[string]
orderByCols: seq[string]
orderDirs: seq[string]
limitVal: int
offsetVal: int
groupByCols: seq[string]
havingClause: string
joinClauses: seq[string]
proc newQueryBuilder*(client: BaraClient): QueryBuilder =
QueryBuilder(
client: client,
selectCols: @[],
fromTable: "",
whereClauses: @[],
orderByCols: @[],
orderDirs: @[],
limitVal: 0,
offsetVal: 0,
groupByCols: @[],
havingClause: "",
joinClauses: @[],
)
proc select*(qb: QueryBuilder, cols: varargs[string]): QueryBuilder =
for col in cols:
qb.selectCols.add(col)
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 orderBy*(qb: QueryBuilder, col: string, dir: string = "ASC"): QueryBuilder =
qb.orderByCols.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 join*(qb: QueryBuilder, table: string, on: string): QueryBuilder =
qb.joinClauses.add("JOIN " & table & " ON " & on)
return qb
proc groupBy*(qb: QueryBuilder, cols: varargs[string]): QueryBuilder =
for col in cols:
qb.groupByCols.add(col)
return qb
proc having*(qb: QueryBuilder, clause: string): QueryBuilder =
qb.havingClause = clause
return qb
proc build*(qb: QueryBuilder): string =
result = "SELECT "
if qb.selectCols.len == 0:
result &= "*"
else:
result &= qb.selectCols.join(", ")
result &= " FROM " & qb.fromTable
for joinClause in qb.joinClauses:
result &= " " & joinClause
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.orderByCols.len > 0:
result &= " ORDER BY "
for i, col in qb.orderByCols:
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())