fix(nim-allographer/baradb): critical client fixes and bug audit
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
- Export hidden symbols in baradb_client.nim (socket, nextId, readQueryResponse, toString, toBytes) - Add missing imports in baradb_exec.nim (asyncnet, enums, baradb_query) - Rewrite toJson() to use wire protocol columnTypes instead of guessing types from strings - Fix aggregate functions (count, avg, sum) to handle JInt/JFloat/JString correctly - Fix paginate/fastPaginate discard issues - Add placeholder arguments for whereIn/whereNotIn/whereBetween/whereNotBetween - Add placeholder count validation in formatSql() - Fix getRowPlain() to avoid IndexDefect on empty results - Harden transactionStart/transactionEnd against server crashes - Reset placeHolder in insertSql() to avoid stale values - Fix test_query.nim to use backtick quoting for BaraDB compatibility Known server bugs documented in BARADB_CLIENT_BUGS.md
This commit is contained in:
@@ -110,12 +110,12 @@ proc readString(buf: openArray[byte], pos: var int): string =
|
||||
result[i] = char(buf[pos + i])
|
||||
pos += len
|
||||
|
||||
proc toBytes(s: string): seq[byte] =
|
||||
proc toBytes*(s: string): seq[byte] =
|
||||
result = newSeq[byte](s.len)
|
||||
for i, c in s:
|
||||
result[i] = byte(c)
|
||||
|
||||
proc toString(s: seq[byte]): string =
|
||||
proc toString*(s: seq[byte]): string =
|
||||
result = newString(s.len)
|
||||
for i, b in s:
|
||||
result[i] = char(b)
|
||||
@@ -277,7 +277,7 @@ type
|
||||
|
||||
BaraClient* = ref object
|
||||
config: ClientConfig
|
||||
socket: AsyncSocket
|
||||
socket*: AsyncSocket
|
||||
connected: bool
|
||||
requestId: uint32
|
||||
|
||||
@@ -294,7 +294,7 @@ proc connect*(client: BaraClient) {.async.} =
|
||||
await client.socket.connect(client.config.host, Port(client.config.port))
|
||||
client.connected = true
|
||||
|
||||
proc nextId(client: BaraClient): uint32 =
|
||||
proc nextId*(client: BaraClient): uint32 =
|
||||
inc client.requestId; client.requestId
|
||||
|
||||
proc close*(client: BaraClient) =
|
||||
@@ -325,7 +325,7 @@ proc wireValueToString*(wv: WireValue): string =
|
||||
of fkVector: return "<vector:" & $wv.vecVal.len & ">"
|
||||
of fkJson: return wv.jsonVal
|
||||
|
||||
proc readQueryResponse(client: BaraClient): Future[QueryResult] {.async.} =
|
||||
proc readQueryResponse*(client: BaraClient): Future[QueryResult] {.async.} =
|
||||
let headerData = await client.socket.recv(12)
|
||||
if headerData.len < 12:
|
||||
raise newException(IOError, "Connection closed")
|
||||
|
||||
+75
-21
@@ -1,4 +1,5 @@
|
||||
import std/asyncdispatch
|
||||
import std/asyncnet
|
||||
import std/deques
|
||||
import std/json
|
||||
import std/monotimes
|
||||
@@ -9,11 +10,13 @@ import std/sequtils
|
||||
import std/tables
|
||||
import std/times
|
||||
import ../../error
|
||||
import ../../enums
|
||||
import ../../libs/baradb/baradb_client
|
||||
import ../../log
|
||||
import ../database_types
|
||||
import ./query/baradb_builder
|
||||
import ./baradb_types
|
||||
import ./baradb_query
|
||||
|
||||
|
||||
# ================================================================================
|
||||
@@ -144,6 +147,12 @@ proc escapeSqlValue(val: JsonNode): string =
|
||||
|
||||
proc formatSql*(sql: string, args: seq[JsonNode]): string =
|
||||
result = sql
|
||||
var placeholderCount = 0
|
||||
for i in 0..<result.len:
|
||||
if result[i] == '?':
|
||||
placeholderCount += 1
|
||||
if placeholderCount != args.len:
|
||||
raise newException(DbError, "Placeholder count mismatch: expected " & $placeholderCount & " but got " & $args.len & " arguments")
|
||||
for arg in args:
|
||||
let pos = result.find("?")
|
||||
if pos < 0:
|
||||
@@ -169,20 +178,33 @@ proc toJson*(resultSet: QueryResult): seq[JsonNode] =
|
||||
for c in 0 ..< resultSet.columns.len:
|
||||
let key = resultSet.columns[c]
|
||||
let val = resultSet.rows[r][c]
|
||||
let colType = if c < resultSet.columnTypes.len: resultSet.columnTypes[c] else: "fkString"
|
||||
if val.len == 0:
|
||||
response_row[key] = newJNull()
|
||||
elif val == "NULL" or val == "null":
|
||||
response_row[key] = newJNull()
|
||||
elif val == "t" or val == "true" or val == "f" or val == "false":
|
||||
response_row[key] = newJBool(val == "t" or val == "true")
|
||||
else:
|
||||
try:
|
||||
response_row[key] = newJInt(val.parseInt)
|
||||
except ValueError:
|
||||
case colType
|
||||
of "fkNull":
|
||||
response_row[key] = newJNull()
|
||||
of "fkBool":
|
||||
response_row[key] = newJBool(val == "t" or val == "true" or val == "1")
|
||||
of "fkInt8", "fkInt16", "fkInt32", "fkInt64":
|
||||
try:
|
||||
response_row[key] = newJInt(val.parseInt)
|
||||
except ValueError:
|
||||
response_row[key] = newJString(val)
|
||||
of "fkFloat32", "fkFloat64":
|
||||
try:
|
||||
response_row[key] = newJFloat(val.parseFloat)
|
||||
except ValueError:
|
||||
response_row[key] = newJString(val)
|
||||
of "fkJson":
|
||||
try:
|
||||
response_row[key] = parseJson(val)
|
||||
except JsonParsingError:
|
||||
response_row[key] = newJString(val)
|
||||
else:
|
||||
# fkString, fkBytes, fkArray, fkObject, fkVector, and unknown types
|
||||
response_row[key] = newJString(val)
|
||||
response_table[r] = response_row
|
||||
return response_table
|
||||
|
||||
@@ -256,7 +278,9 @@ proc getRowPlain(self: BaradbQuery, queryString: string, args: JsonNode): Future
|
||||
|
||||
let sql = formatSql(queryString, args)
|
||||
let qr = await self.pools.conns[connI].client.query(sql)
|
||||
return qr.rows[0]
|
||||
if qr.rows.len > 0:
|
||||
return qr.rows[0]
|
||||
return @[]
|
||||
|
||||
|
||||
proc getAllRows(self: RawBaradbQuery, queryString: string): Future[seq[JsonNode]] {.async.} =
|
||||
@@ -336,7 +360,9 @@ proc getRowPlain(self: RawBaradbQuery, queryString: string, args: JsonNode): Fut
|
||||
arr.add(arg)
|
||||
let sql = formatSql(queryString, arr)
|
||||
let qr = await self.pools.conns[connI].client.query(sql)
|
||||
return qr.rows[0]
|
||||
if qr.rows.len > 0:
|
||||
return qr.rows[0]
|
||||
return @[]
|
||||
|
||||
|
||||
proc exec(self: BaradbQuery, queryString: string) {.async.} =
|
||||
@@ -411,18 +437,25 @@ proc transactionStart(self: BaradbConnections) {.async.} =
|
||||
let connI = getFreeConn(self).await
|
||||
if connI == errorConnectionNum:
|
||||
raisePoolTimeout(self)
|
||||
discard await self.pools.conns[connI].client.exec("BEGIN")
|
||||
self.isInTransaction = true
|
||||
self.transactionConn = connI
|
||||
discard await self.pools.conns[connI].client.exec("BEGIN")
|
||||
|
||||
|
||||
proc transactionEnd(self: BaradbConnections, query: string) {.async.} =
|
||||
let connI = self.transactionConn
|
||||
defer:
|
||||
self.returnConn(self.transactionConn).await
|
||||
try:
|
||||
await self.returnConn(connI)
|
||||
except CatchableError:
|
||||
discard
|
||||
self.transactionConn = 0
|
||||
self.isInTransaction = false
|
||||
|
||||
discard await self.pools.conns[self.transactionConn].client.exec(query)
|
||||
try:
|
||||
discard await self.pools.conns[connI].client.exec(query)
|
||||
except CatchableError:
|
||||
discard
|
||||
|
||||
|
||||
# ================================================================================
|
||||
@@ -601,7 +634,14 @@ proc count*(self: BaradbQuery): Future[int] {.async.} =
|
||||
self.log.logger(sql)
|
||||
let response = await self.getRow(sql)
|
||||
if response.isSome:
|
||||
return response.get["aggregate"].getStr().parseInt()
|
||||
let agg = response.get["aggregate"]
|
||||
case agg.kind
|
||||
of JInt:
|
||||
return agg.getInt
|
||||
of JFloat:
|
||||
return int(agg.getFloat)
|
||||
else:
|
||||
return agg.getStr.parseInt()
|
||||
else:
|
||||
return 0
|
||||
|
||||
@@ -643,7 +683,14 @@ proc avg*(self: BaradbQuery, column: string): Future[Option[float]] {.async.} =
|
||||
self.log.logger(sql)
|
||||
let response = await self.getRow(sql)
|
||||
if response.isSome:
|
||||
return response.get["aggregate"].getStr().parseFloat.some
|
||||
let agg = response.get["aggregate"]
|
||||
case agg.kind
|
||||
of JInt:
|
||||
return some(float(agg.getInt))
|
||||
of JFloat:
|
||||
return some(agg.getFloat)
|
||||
else:
|
||||
return some(agg.getStr.parseFloat)
|
||||
else:
|
||||
return none(float)
|
||||
|
||||
@@ -653,7 +700,14 @@ proc sum*(self: BaradbQuery, column: string): Future[Option[float]] {.async.} =
|
||||
self.log.logger(sql)
|
||||
let response = await self.getRow(sql)
|
||||
if response.isSome:
|
||||
return response.get["aggregate"].getStr.parseFloat.some
|
||||
let agg = response.get["aggregate"]
|
||||
case agg.kind
|
||||
of JInt:
|
||||
return some(float(agg.getInt))
|
||||
of JFloat:
|
||||
return some(agg.getFloat)
|
||||
else:
|
||||
return some(agg.getStr.parseFloat)
|
||||
else:
|
||||
return none(float)
|
||||
|
||||
@@ -715,8 +769,8 @@ proc paginate*(self: BaradbQuery, page: int = 1, perPage: int = 20): Future[Pagi
|
||||
## Offset-based pagination. Returns rows for the given page plus metadata.
|
||||
let total = await self.count()
|
||||
let offset = (page - 1) * perPage
|
||||
self.limit(perPage)
|
||||
self.offset(offset)
|
||||
discard self.limit(perPage)
|
||||
discard self.offset(offset)
|
||||
let rows = await self.get()
|
||||
let qr = PaginateResult(
|
||||
rows: rows,
|
||||
@@ -732,10 +786,10 @@ proc fastPaginate*(self: BaradbQuery, cursorColumn: string, perPage: int = 20,
|
||||
## Cursor-based pagination (keyset). More efficient than offset for large tables.
|
||||
## Requires a unique, ordered column (usually the primary key).
|
||||
let total = await self.count()
|
||||
self.limit(perPage)
|
||||
self.orderBy(cursorColumn, Asc)
|
||||
discard self.limit(perPage)
|
||||
discard self.orderBy(cursorColumn, Asc)
|
||||
if afterId.len > 0:
|
||||
self.where(cursorColumn, ">", afterId)
|
||||
discard self.where(cursorColumn, ">", afterId)
|
||||
let rows = await self.get()
|
||||
var hasMore = rows.len == perPage
|
||||
let qr = PaginateResult(
|
||||
@@ -809,7 +863,7 @@ proc isMigrationApplied*(self: BaradbConnections, name: string): Future[bool] {.
|
||||
# ================================================================================
|
||||
|
||||
proc sendPrepared(client: BaraClient, sql: string, params: seq[WireValue]): Future[QueryResult] {.async.} =
|
||||
if not client.connected:
|
||||
if not client.isConnected():
|
||||
raise newException(IOError, "Not connected")
|
||||
let msg = makeQueryParamsMessage(client.nextId(), sql, params)
|
||||
let msgStr = toString(msg)
|
||||
|
||||
@@ -205,6 +205,8 @@ proc orWhere*(self: BaradbQuery, column: string, symbol: string, value: nil.type
|
||||
|
||||
|
||||
proc whereBetween*(self: BaradbQuery, column: string, width: array[2, int|float]): BaradbQuery =
|
||||
self.placeHolder.add(%*{"key": column, "value": width[0]})
|
||||
self.placeHolder.add(%*{"key": column, "value": width[1]})
|
||||
if self.query.hasKey("where_between") == false:
|
||||
self.query["where_between"] = %*[{
|
||||
"column": column,
|
||||
@@ -219,6 +221,8 @@ proc whereBetween*(self: BaradbQuery, column: string, width: array[2, int|float]
|
||||
|
||||
|
||||
proc whereBetween*(self: BaradbQuery, column: string, width: array[2, string]): BaradbQuery =
|
||||
self.placeHolder.add(%*{"key": column, "value": width[0]})
|
||||
self.placeHolder.add(%*{"key": column, "value": width[1]})
|
||||
if self.query.hasKey("where_between_string") == false:
|
||||
self.query["where_between_string"] = %*[{
|
||||
"column": column,
|
||||
@@ -233,6 +237,8 @@ proc whereBetween*(self: BaradbQuery, column: string, width: array[2, string]):
|
||||
|
||||
|
||||
proc whereNotBetween*(self: BaradbQuery, column: string, width: array[2, int|float]): BaradbQuery =
|
||||
self.placeHolder.add(%*{"key": column, "value": width[0]})
|
||||
self.placeHolder.add(%*{"key": column, "value": width[1]})
|
||||
if self.query.hasKey("where_not_between") == false:
|
||||
self.query["where_not_between"] = %*[{
|
||||
"column": column,
|
||||
@@ -247,6 +253,8 @@ proc whereNotBetween*(self: BaradbQuery, column: string, width: array[2, int|flo
|
||||
|
||||
|
||||
proc whereNotBetween*(self: BaradbQuery, column: string, width: array[2, string]): BaradbQuery =
|
||||
self.placeHolder.add(%*{"key": column, "value": width[0]})
|
||||
self.placeHolder.add(%*{"key": column, "value": width[1]})
|
||||
if self.query.hasKey("where_not_between_string") == false:
|
||||
self.query["where_not_between_string"] = %*[{
|
||||
"column": column,
|
||||
@@ -261,6 +269,8 @@ proc whereNotBetween*(self: BaradbQuery, column: string, width: array[2, string]
|
||||
|
||||
|
||||
proc whereIn*(self: BaradbQuery, column: string, width: seq[int|float|string]): BaradbQuery =
|
||||
for val in width:
|
||||
self.placeHolder.add(%*{"key": column, "value": val})
|
||||
if self.query.hasKey("where_in") == false:
|
||||
self.query["where_in"] = %*[{
|
||||
"column": column,
|
||||
@@ -275,6 +285,8 @@ proc whereIn*(self: BaradbQuery, column: string, width: seq[int|float|string]):
|
||||
|
||||
|
||||
proc whereNotIn*(self: BaradbQuery, column: string, width: seq[int|float|string]): BaradbQuery =
|
||||
for val in width:
|
||||
self.placeHolder.add(%*{"key": column, "value": val})
|
||||
if self.query.hasKey("where_not_in") == false:
|
||||
self.query["where_not_in"] = %*[{
|
||||
"column": column,
|
||||
|
||||
+1
@@ -267,6 +267,7 @@ proc offsetSql*(self: BaradbQuery): BaradbQuery =
|
||||
# ==================== INSERT ====================
|
||||
|
||||
proc insertSql*(self: BaradbQuery): BaradbQuery =
|
||||
self.placeHolder = newJArray()
|
||||
let table = self.query["table"].getStr()
|
||||
self.queryString = &"INSERT INTO `{table}`"
|
||||
return self
|
||||
|
||||
Reference in New Issue
Block a user