9d71edafd4
- Поправени 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
255 lines
6.9 KiB
Nim
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())
|