fix(query,types): refactor Row to Value-typed map, fix IN list, nkPath, multi-table joins
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

- Refactored Row from Table[string, string] to Table[string, Value]
- Added Value operators: ==, !=, $, in for string interop
- Fixed valueToString for vkNull to return '\\N' instead of ''
- Fixed evalExpr irekField to return vkNull when field not found
- Added IN (val1, val2, ...) parser support
- Fixed nkPath column names in multi-table joins
- Fixed LATERAL JOIN null padding when no matching rows
- Added CREATE/DROP/USE/SHOW DATABASE parser support
- Adapted all tests for new Value type
This commit is contained in:
2026-05-20 23:22:14 +03:00
parent 57d2908066
commit 372e5cf627
14 changed files with 1084 additions and 346 deletions
+28 -9
View File
@@ -6,13 +6,13 @@ import hunos/context
import json
import tables
import strutils
import os
import times
import std/asyncdispatch
import config
import ../query/lexer
import ../query/parser
import ../query/executor
import ../core/types
import ../storage/lsm
import ../core/mvcc
import ../protocol/wire
@@ -20,6 +20,7 @@ import ../core/websocket
import jwt as jwtlib
import ../protocol/auth
import ../protocol/ratelimit
import ../core/registry
type
HttpServer* = ref object
@@ -27,6 +28,7 @@ type
running: bool
db*: LSMTree
ctx: ExecutionContext
registry*: DatabaseRegistry
metrics*: Metrics
secretKey*: string
authManager*: AuthManager
@@ -40,8 +42,10 @@ type
selectCount*: int
activeConnections*: int
proc newHttpServerWithDb*(config: BaraConfig, db: LSMTree): HttpServer =
let ctx = newExecutionContext(db)
proc newHttpServerWithRegistry*(config: BaraConfig, registry: DatabaseRegistry): HttpServer =
let dbInfo = getOrCreateDatabase(registry, "default")
let db = dbInfo.db
let ctx = cast[ExecutionContext](cast[pointer](dbInfo.ctx))
ctx.txnManager = newTxnManager()
let secret = config.getEffectiveJwtSecret()
let ws = newWsServer(config, secret)
@@ -51,15 +55,27 @@ proc newHttpServerWithDb*(config: BaraConfig, db: LSMTree): HttpServer =
asyncCheck ws.broadcastToTable(ev.table, msg)
let am = newAuthManager(secret)
HttpServer(config: config, running: false, db: db, ctx: ctx,
registry: registry,
secretKey: secret,
authManager: am,
rateLimiter: rl,
metrics: Metrics(), ws: ws)
proc newHttpServerWithDb*(config: BaraConfig, db: LSMTree): HttpServer =
let registry = newDatabaseRegistry(config)
registry.setContextFactory(proc(d: LSMTree, r: DatabaseRegistry): ContextRef {.closure.} =
cast[ContextRef](cast[pointer](newExecutionContext(d, r))))
let ctx = newExecutionContext(db, registry)
registry.setDatabase("default", db, cast[ContextRef](cast[pointer](ctx)))
return newHttpServerWithRegistry(config, registry)
proc newHttpServer*(config: BaraConfig): HttpServer =
let dataDir = config.dataDir / "server"
let db = newLSMTree(dataDir)
return newHttpServerWithDb(config, db)
let registry = newDatabaseRegistry(config)
registry.setContextFactory(proc(d: LSMTree, r: DatabaseRegistry): ContextRef {.closure.} =
cast[ContextRef](cast[pointer](newExecutionContext(d, r))))
registry.loadExistingDatabases()
registry.ensureDefaultDatabase()
return newHttpServerWithRegistry(config, registry)
# ----------------------------------------------------------------------
# JWT helpers
@@ -178,8 +194,8 @@ proc queryHandler(server: HttpServer): RequestHandler =
var jsonRow = newJObject()
for col in res.columns:
let key = col
if key in row and not isNull(row[key]):
jsonRow[key] = %row[key]
if key in row and row[key].kind != vkNull:
jsonRow[key] = %valueToString(row[key])
else:
jsonRow[key] = newJNull()
jsonRows.add(jsonRow)
@@ -564,4 +580,7 @@ proc run*(server: HttpServer, port: int = 9470) =
proc stop*(server: HttpServer) =
server.running = false
server.ws.stop()
server.db.close()
if server.registry != nil:
server.registry.closeAll()
else:
server.db.close()
+192
View File
@@ -0,0 +1,192 @@
## BaraDB Database Registry — manages per-database LSMTree instances
import std/tables
import std/os
import std/strutils
import std/locks
import std/algorithm
import logging
import config
import ../storage/lsm
type
ContextRef* = ref RootObj
ContextFactory* = proc(db: LSMTree, reg: DatabaseRegistry): ContextRef {.closure.}
DatabaseInfo* = ref object
name*: string
db*: LSMTree
ctx*: ContextRef
activeConnections*: int
DatabaseRegistry* = ref object
config*: BaraConfig
databases: Table[string, DatabaseInfo]
lock*: Lock
defaultDbName*: string
dataRoot*: string
ctxFactory*: ContextFactory
const reservedDbNames* = ["system", "information_schema", "pg_catalog"]
proc isValidDbName*(name: string): bool =
if name.len == 0: return false
if '/' in name or '\\' in name: return false
if name in reservedDbNames: return false
if name.startsWith("_"): return false
if name == ".." or name == ".": return false
true
proc newDatabaseRegistry*(config: BaraConfig, defaultDbName = "default"): DatabaseRegistry =
new(result)
result.config = config
result.databases = initTable[string, DatabaseInfo]()
result.defaultDbName = defaultDbName
result.dataRoot = config.dataDir / "databases"
initLock(result.lock)
# Create root directory
if not dirExists(result.dataRoot):
createDir(result.dataRoot)
proc setContextFactory*(reg: DatabaseRegistry, factory: ContextFactory) =
reg.ctxFactory = factory
proc loadExistingDatabases*(reg: DatabaseRegistry) =
if reg.ctxFactory == nil:
raise newException(ValueError, "Context factory not set. Call setContextFactory first.")
# Scan for existing databases
for kind, path in walkDir(reg.dataRoot):
if kind == pcDir:
let dbName = path.splitPath().tail
if dbName.len > 0 and isValidDbName(dbName):
let dbDir = reg.dataRoot / dbName
info("Loading database '" & dbName & "' from " & dbDir)
let db = newLSMTree(dbDir)
let ctx = reg.ctxFactory(db, reg)
reg.databases[dbName] = DatabaseInfo(
name: dbName, db: db, ctx: ctx, activeConnections: 0
)
proc setDatabase*(reg: DatabaseRegistry, name: string, db: LSMTree, ctx: ContextRef) =
acquire(reg.lock)
defer: release(reg.lock)
reg.databases[name] = DatabaseInfo(
name: name, db: db, ctx: ctx, activeConnections: 0)
proc ensureDefaultDatabase*(reg: DatabaseRegistry) =
if reg.ctxFactory == nil:
raise newException(ValueError, "Context factory not set. Call setContextFactory first.")
let defaultDbName = reg.defaultDbName
if defaultDbName notin reg.databases:
let dbDir = reg.dataRoot / defaultDbName
info("Creating default database at " & dbDir)
let db = newLSMTree(dbDir)
let ctx = reg.ctxFactory(db, reg)
reg.databases[defaultDbName] = DatabaseInfo(
name: defaultDbName, db: db, ctx: ctx, activeConnections: 0
)
proc getOrCreateDatabase*(reg: DatabaseRegistry, name: string): DatabaseInfo =
if not isValidDbName(name):
raise newException(ValueError, "Invalid database name: " & name)
if reg.ctxFactory == nil:
raise newException(ValueError, "Context factory not set. Call setContextFactory first.")
acquire(reg.lock)
defer: release(reg.lock)
if name in reg.databases:
return reg.databases[name]
# Create new database
let dbDir = reg.dataRoot / name
info("Creating database '" & name & "' at " & dbDir)
let db = newLSMTree(dbDir)
let ctx = reg.ctxFactory(db, reg)
let info = DatabaseInfo(name: name, db: db, ctx: ctx, activeConnections: 0)
reg.databases[name] = info
info
proc getConnectionCount*(reg: DatabaseRegistry, name: string): int =
acquire(reg.lock)
defer: release(reg.lock)
if name in reg.databases:
return reg.databases[name].activeConnections
return 0
proc incrementConnections*(reg: DatabaseRegistry, name: string) =
acquire(reg.lock)
defer: release(reg.lock)
if name in reg.databases:
inc reg.databases[name].activeConnections
proc decrementConnections*(reg: DatabaseRegistry, name: string) =
acquire(reg.lock)
defer: release(reg.lock)
if name in reg.databases and reg.databases[name].activeConnections > 0:
dec reg.databases[name].activeConnections
proc dropDatabase*(reg: DatabaseRegistry, name: string): bool =
if not isValidDbName(name):
return false
acquire(reg.lock)
defer: release(reg.lock)
if name notin reg.databases:
return false
if name == reg.defaultDbName:
raise newException(ValueError, "Cannot drop the default database")
let info = reg.databases[name]
if info.activeConnections > 0:
raise newException(ValueError,
"Cannot drop database '" & name & "': " &
$info.activeConnections & " active connections")
# Close LSMTree
info.db.close()
# Remove data directory
let dbDir = reg.dataRoot / name
if dirExists(dbDir):
removeDir(dbDir)
reg.databases.del(name)
true
proc listDatabases*(reg: DatabaseRegistry): seq[string] =
acquire(reg.lock)
defer: release(reg.lock)
result = @[]
for name in reg.databases.keys:
result.add(name)
result.sort()
proc databaseExists*(reg: DatabaseRegistry, name: string): bool =
acquire(reg.lock)
defer: release(reg.lock)
name in reg.databases
proc getDatabaseInfo*(reg: DatabaseRegistry, name: string): DatabaseInfo =
acquire(reg.lock)
defer: release(reg.lock)
if name in reg.databases:
return reg.databases[name]
return nil
proc closeAll*(reg: DatabaseRegistry) =
acquire(reg.lock)
defer: release(reg.lock)
for name, info in reg.databases.pairs:
try:
info.db.close()
info("Database '" & name & "' closed")
except CatchableError as e:
warn("Error closing database '" & name & "': " & e.msg)
reg.databases.clear()
+57 -16
View File
@@ -4,7 +4,6 @@ import std/asyncnet
import std/strutils
import std/sequtils
import std/tables
import std/os
import std/endians
import std/monotimes
import std/locks
@@ -28,6 +27,7 @@ import ../core/replication
import ../core/sharding
import ../core/gossip
import ../protocol/ratelimit
import ../core/registry
import jwt as jwtlib
type
@@ -36,6 +36,7 @@ type
running*: bool
db*: LSMTree
ctx*: ExecutionContext
registry*: DatabaseRegistry
txnManager*: TxnManager
distTxnManager*: DistTxnManager
replicationManager*: ReplicationManager
@@ -47,8 +48,10 @@ type
activeConnections*: int
activeConnectionsLock*: Lock
proc newServerWithDb*(config: BaraConfig, db: LSMTree): Server =
let ctx = newExecutionContext(db)
proc newServerWithRegistry*(config: BaraConfig, registry: DatabaseRegistry): Server =
let dbInfo = getOrCreateDatabase(registry, "default")
let db = dbInfo.db
let ctx = cast[ExecutionContext](cast[pointer](dbInfo.ctx))
ctx.txnManager = newTxnManager()
var tls: TLSContext = nil
if config.tlsEnabled and config.certFile.len > 0 and config.keyFile.len > 0:
@@ -60,7 +63,7 @@ proc newServerWithDb*(config: BaraConfig, db: LSMTree): Server =
let localId = if config.raftNodeId.len > 0: config.raftNodeId else: "node-" & $config.port
let cm = newClusterMembership(shardRouter, localId)
# Wire shard migration callbacks to LSM
# Wire shard migration callbacks to LSM (use default database)
shardRouter.iterateKeys = proc(shardId: int): seq[(string, seq[byte])] {.gcsafe.} =
var entries: seq[(string, seq[byte])] = @[]
for (key, value) in db.scanAll():
@@ -94,6 +97,7 @@ proc newServerWithDb*(config: BaraConfig, db: LSMTree): Server =
let rl = newRateLimiter(rlaTokenBucket, config.rateLimitGlobal, config.rateLimitPerClient)
result = Server(config: config, running: false, db: db, ctx: ctx,
registry: registry,
txnManager: ctx.txnManager, distTxnManager: newDistTxnManager(),
replicationManager: newReplicationManager(),
shardRouter: shardRouter,
@@ -103,10 +107,22 @@ proc newServerWithDb*(config: BaraConfig, db: LSMTree): Server =
rateLimiter: rl)
initLock(result.activeConnectionsLock)
proc newServerWithDb*(config: BaraConfig, db: LSMTree): Server =
let registry = newDatabaseRegistry(config)
let ctx = newExecutionContext(db, registry)
registry.setContextFactory(proc(d: LSMTree, r: DatabaseRegistry): ContextRef {.closure.} =
cast[ContextRef](cast[pointer](newExecutionContext(d, r))))
# Use the existing db for default
registry.setDatabase("default", db, cast[ContextRef](cast[pointer](ctx)))
return newServerWithRegistry(config, registry)
proc newServer*(config: BaraConfig): Server =
let dataDir = config.dataDir / "server"
let db = newLSMTree(dataDir)
return newServerWithDb(config, db)
let registry = newDatabaseRegistry(config)
registry.setContextFactory(proc(d: LSMTree, r: DatabaseRegistry): ContextRef {.closure.} =
cast[ContextRef](cast[pointer](newExecutionContext(d, r))))
registry.loadExistingDatabases()
registry.ensureDefaultDatabase()
return newServerWithRegistry(config, registry)
# ----------------------------------------------------------------------
# Wire Protocol Helpers
@@ -230,7 +246,7 @@ proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string, params: seq
for row in res.rows:
var wireRow: seq[WireValue] = @[]
for i, col in res.columns:
let val = if col in row: row[col] else: "\\N"
let val = if col in row: valueToString(row[col]) else: "\\N"
let cType = if i < colTypes.len: colTypes[i] else: ""
wireRow.add(valueToWire(val, cType))
qr.rows.add(wireRow)
@@ -312,16 +328,17 @@ proc slowQueryLog(logPath: string, query: string, durationMs: int, clientId: int
f.write(line)
except IOError: discard
proc verifyToken(secret, tokenStr: string): (bool, string, string) =
proc verifyToken(secret, tokenStr: string): (bool, string, string, string) =
try:
let token = tokenStr.toJWT()
if not token.verify(secret, HS256):
return (false, "", "")
return (false, "", "", "")
let userId = token.claims["sub"].node.str
let role = if "role" in token.claims: token.claims["role"].node.str else: "user"
return (true, userId, role)
let database = if "database" in token.claims: token.claims["database"].node.str else: ""
return (true, userId, role, database)
except ValueError, KeyError:
return (false, "", "")
return (false, "", "", "")
proc recvWithTimeout(client: AsyncSocket, size: int, timeoutMs: int): Future[string] {.async.} =
if timeoutMs <= 0:
@@ -475,7 +492,7 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
case header.kind
of mkAuth:
let tokenStr = parseAuthMessage(stringToBytes(payload))
let (valid, userId, role) = verifyToken(secret, tokenStr)
let (valid, userId, role, jwtDatabase) = verifyToken(secret, tokenStr)
if valid:
authenticated = true
connCtx.currentUser = userId
@@ -483,6 +500,24 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
let okMsg = makeAuthOkMessage(header.requestId)
await client.send(bytesToString(okMsg))
info("Client " & $clientId & " authenticated as " & userId)
# Switch to database from JWT claim if provided
if jwtDatabase.len > 0 and server.registry != nil and isValidDbName(jwtDatabase):
let info = getDatabaseInfo(server.registry, jwtDatabase)
if info != nil:
let targetCtx = cast[ExecutionContext](cast[pointer](info.ctx))
connCtx.db = info.db
connCtx.tables = targetCtx.tables
connCtx.btrees = targetCtx.btrees
connCtx.views = targetCtx.views
connCtx.ftsIndexes = targetCtx.ftsIndexes
connCtx.vectorIndexes = targetCtx.vectorIndexes
connCtx.users = targetCtx.users
connCtx.policies = targetCtx.policies
connCtx.graphs = targetCtx.graphs
connCtx.autoIncCounters = targetCtx.autoIncCounters
connCtx.sequences = targetCtx.sequences
connCtx.currentDatabase = jwtDatabase
incrementConnections(server.registry, jwtDatabase)
else:
let err = makeErrorMessage(header.requestId, 403, "Invalid token")
await client.send(bytesToString(err))
@@ -510,7 +545,7 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
if shardCheck:
let startTicks = getMonoTime().ticks()
let (success, result, errorMsg) = executeQuery(server.db, connCtx, queryStr, replication=server.replicationManager)
let (success, result, errorMsg) = executeQuery(connCtx.db, connCtx, queryStr, replication=server.replicationManager)
let durationMs = int((getMonoTime().ticks() - startTicks) div 1_000_000)
if durationMs >= slowThreshold:
@@ -530,7 +565,7 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
info("[" & $clientId & "] QueryParams: " & queryStr & " (" & $params.len & " params)")
let startTicks = getMonoTime().ticks()
let (success, result, errorMsg) = executeQuery(server.db, connCtx, queryStr, params, replication=server.replicationManager)
let (success, result, errorMsg) = executeQuery(connCtx.db, connCtx, queryStr, params, replication=server.replicationManager)
let durationMs = int((getMonoTime().ticks() - startTicks) div 1_000_000)
if durationMs >= slowThreshold:
@@ -562,6 +597,9 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
except Exception as e:
errorMsg("Client " & $clientId & " error: " & e.msg)
finally:
# Decrement database connection counter
if server.registry != nil and connCtx.currentDatabase.len > 0:
decrementConnections(server.registry, connCtx.currentDatabase)
acquire(server.activeConnectionsLock)
try:
if server.activeConnections > 0:
@@ -629,4 +667,7 @@ proc stop*(server: Server) =
server.running = false
if server.gossipProtocol != nil:
server.gossipProtocol.stop()
server.db.close()
if server.registry != nil:
server.registry.closeAll()
else:
server.db.close()
+58
View File
@@ -1,6 +1,7 @@
import std/times
import std/oids
import std/monotimes
import std/strutils
type
ValueKind* = enum
@@ -124,3 +125,60 @@ proc newRecordId*(): RecordId =
proc `==`*(a, b: RecordId): bool {.borrow.}
proc `$`*(r: RecordId): string = $uint64(r)
proc `==`*(a, b: Value): bool {.noSideEffect.} =
if a.kind != b.kind: return false
case a.kind
of vkNull: return true
of vkBool: return a.boolVal == b.boolVal
of vkInt8: return a.int8Val == b.int8Val
of vkInt16: return a.int16Val == b.int16Val
of vkInt32: return a.int32Val == b.int32Val
of vkInt64: return a.int64Val == b.int64Val
of vkFloat32: return a.float32Val == b.float32Val
of vkFloat64: return a.float64Val == b.float64Val
of vkString: return a.strVal == b.strVal
of vkBytes: return a.bytesVal == b.bytesVal
of vkUuid: return a.uuidVal == b.uuidVal
of vkDateTime: return false # DateTime comparison not supported without side effects
of vkJson: return a.jsonVal == b.jsonVal
of vkArray: return false # Recursive comparison not supported
of vkObject: return false # Recursive comparison not supported
of vkVector: return a.vecVal == b.vecVal
proc `!=`*(a, b: Value): bool {.noSideEffect.} =
return not (a == b)
proc `$`*(v: Value): string =
case v.kind
of vkNull: return "\\N"
of vkBool: return $v.boolVal
of vkInt8: return $v.int8Val
of vkInt16: return $v.int16Val
of vkInt32: return $v.int32Val
of vkInt64: return $v.int64Val
of vkFloat32: return $v.float32Val
of vkFloat64: return $v.float64Val
of vkString: return v.strVal
of vkBytes: return "<bytes>"
of vkUuid: return $v.uuidVal
of vkDateTime: return $v.dtVal
of vkJson: return v.jsonVal
of vkArray: return $v.arrayVal
of vkObject: return $v.objVal
of vkVector: return $v.vecVal
proc `==`*(a: Value, b: string): bool =
if a.kind == vkString: return a.strVal == b
if a.kind == vkNull: return b == "\\N"
return $a == b
proc `==`*(a: string, b: Value): bool =
return b == a
proc `in`*(v: Value, s: seq[string]): bool =
return $v in s
proc `in`*(s: string, v: Value): bool =
if v.kind == vkString: return v.strVal.contains(s)
return ($v).contains(s)