fix(adapter,executor,tests): harden nimforum adapter and fix transaction/aggregate bugs
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
Adapter (baradb_sqlite.nim):
- getRow now returns @[] for missing rows (db_sqlite compat)
- insertID/nextId handle NULL (\N) values correctly
- preprocessQuery replaces SQLite-specific syntax:
TIMESTAMP->DATETIME, DATETIME('now')->current timestamp, INET->STRING
- add dbBegin/dbCommit/dbRollback for transaction support
Executor:
- fix COMMIT treating empty values as DELETE (rows with only PK were lost)
- fix MIN/MAX aggregates including NULL (\N) which skewed results
- add numeric comparison for irNeq
Tests:
- stress_test: shared DB, 16 workers, correctness scan after run
- nimforum_smoke_test: 10 tests including schema, transactions, iterators
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
## NimForum Adapter Smoke Test
|
||||
## NimForum Adapter Smoke Test — comprehensive validation
|
||||
import std/unittest
|
||||
import std/osproc
|
||||
import std/os
|
||||
import std/strutils
|
||||
import std/strtabs
|
||||
import std/times
|
||||
|
||||
@@ -24,7 +23,23 @@ suite "NimForum Adapter Smoke Test":
|
||||
env["BARADB_DATA_DIR"] = dataDir
|
||||
env["BARADB_LOG_LEVEL"] = "error"
|
||||
serverProcess = startProcess("./build/baradadb", env=env, options={poStdErrToStdOut, poDaemon})
|
||||
sleep(800)
|
||||
# Wait for server to be ready (with timeout)
|
||||
var pingDb: DbConn
|
||||
var connected = false
|
||||
for i in 0 ..< 50:
|
||||
sleep(100)
|
||||
try:
|
||||
pingDb = open("127.0.0.1:" & $port, "", "", "default")
|
||||
connected = true
|
||||
break
|
||||
except:
|
||||
discard
|
||||
if not connected:
|
||||
serverProcess.terminate()
|
||||
discard serverProcess.waitForExit()
|
||||
removeDir(dataDir)
|
||||
raise newException(IOError, "Server failed to start")
|
||||
pingDb.close()
|
||||
|
||||
teardown:
|
||||
if serverProcess != nil:
|
||||
@@ -67,3 +82,170 @@ suite "NimForum Adapter Smoke Test":
|
||||
check row[row.len - 1] == "ten"
|
||||
|
||||
db.close()
|
||||
|
||||
test "getRow returns @[] for missing rows":
|
||||
var db = open("127.0.0.1:" & $port, "", "", "default")
|
||||
db.exec(sql"CREATE TABLE nf_missing (id INT PRIMARY KEY)")
|
||||
let row = db.getRow(sql"SELECT * FROM nf_missing WHERE id = 999")
|
||||
check row.len == 0
|
||||
db.close()
|
||||
|
||||
test "insertID and nextId":
|
||||
var db = open("127.0.0.1:" & $port, "", "", "default")
|
||||
db.exec(sql"CREATE TABLE nf_ids (id INT PRIMARY KEY, name STRING)")
|
||||
|
||||
let id1 = db.insertID(sql"INSERT INTO nf_ids (id, name) VALUES (5, 'five')")
|
||||
check id1 == 5
|
||||
|
||||
let next = db.nextId("nf_ids")
|
||||
check next == 6
|
||||
|
||||
db.close()
|
||||
|
||||
test "Transactions (BEGIN/COMMIT/ROLLBACK)":
|
||||
var db = open("127.0.0.1:" & $port, "", "", "default")
|
||||
db.exec(sql"CREATE TABLE nf_txn (id INT PRIMARY KEY)")
|
||||
|
||||
db.dbBegin()
|
||||
db.exec(sql"INSERT INTO nf_txn (id) VALUES (1)")
|
||||
db.dbCommit()
|
||||
check db.getValue(sql"SELECT count(*) FROM nf_txn") == "1"
|
||||
|
||||
db.dbBegin()
|
||||
db.exec(sql"INSERT INTO nf_txn (id) VALUES (2)")
|
||||
db.dbRollback()
|
||||
check db.getValue(sql"SELECT count(*) FROM nf_txn") == "1"
|
||||
|
||||
db.close()
|
||||
|
||||
test "fastRows iterator":
|
||||
var db = open("127.0.0.1:" & $port, "", "", "default")
|
||||
db.exec(sql"CREATE TABLE nf_iter (id INT PRIMARY KEY)")
|
||||
db.exec(sql"INSERT INTO nf_iter (id) VALUES (1)")
|
||||
db.exec(sql"INSERT INTO nf_iter (id) VALUES (2)")
|
||||
db.exec(sql"INSERT INTO nf_iter (id) VALUES (3)")
|
||||
|
||||
var count = 0
|
||||
for r in db.fastRows(sql"SELECT id FROM nf_iter ORDER BY id"):
|
||||
count.inc
|
||||
check count == 3
|
||||
|
||||
db.close()
|
||||
|
||||
test "execAffectedRows":
|
||||
var db = open("127.0.0.1:" & $port, "", "", "default")
|
||||
db.exec(sql"CREATE TABLE nf_aff (id INT PRIMARY KEY, name STRING)")
|
||||
db.exec(sql"INSERT INTO nf_aff (id, name) VALUES (1, 'a')")
|
||||
db.exec(sql"INSERT INTO nf_aff (id, name) VALUES (2, 'b')")
|
||||
|
||||
let affected = db.execAffectedRows(sql"DELETE FROM nf_aff WHERE id = 1")
|
||||
check affected == 1
|
||||
|
||||
db.close()
|
||||
|
||||
test "tryExec success and failure":
|
||||
var db = open("127.0.0.1:" & $port, "", "", "default")
|
||||
db.exec(sql"CREATE TABLE nf_try (id INT PRIMARY KEY)")
|
||||
|
||||
check db.tryExec(sql"INSERT INTO nf_try (id) VALUES (1)") == true
|
||||
check db.tryExec(sql"INSERT INTO nf_try (id) VALUES (1)") == false
|
||||
|
||||
db.close()
|
||||
|
||||
test "NimForum schema creation":
|
||||
var db = open("127.0.0.1:" & $port, "", "", "default")
|
||||
|
||||
# Create nimforum-like schema (adapter preprocesses DATETIME/TIMESTAMP/INET)
|
||||
db.exec(sql"""
|
||||
CREATE TABLE thread (
|
||||
id INT PRIMARY KEY,
|
||||
name STRING NOT NULL,
|
||||
views INT NOT NULL,
|
||||
modified DATETIME NOT NULL DEFAULT DATETIME('now')
|
||||
)
|
||||
""")
|
||||
db.exec(sql"CREATE UNIQUE INDEX ThreadNameIx ON thread (name)")
|
||||
|
||||
db.exec(sql"""
|
||||
CREATE TABLE person (
|
||||
id INT PRIMARY KEY,
|
||||
name STRING NOT NULL,
|
||||
password STRING NOT NULL,
|
||||
email STRING NOT NULL,
|
||||
creation DATETIME NOT NULL DEFAULT DATETIME('now'),
|
||||
salt STRING NOT NULL,
|
||||
user_status STRING NOT NULL,
|
||||
lastOnline DATETIME NOT NULL DEFAULT DATETIME('now'),
|
||||
ban STRING NOT NULL DEFAULT ''
|
||||
)
|
||||
""")
|
||||
db.exec(sql"CREATE UNIQUE INDEX UserNameIx ON person (name)")
|
||||
|
||||
db.exec(sql"""
|
||||
CREATE TABLE post (
|
||||
id INT PRIMARY KEY,
|
||||
author INT NOT NULL,
|
||||
ip STRING NOT NULL,
|
||||
header STRING NOT NULL,
|
||||
content STRING NOT NULL,
|
||||
thread INT NOT NULL,
|
||||
creation DATETIME NOT NULL DEFAULT DATETIME('now')
|
||||
)
|
||||
""")
|
||||
|
||||
db.exec(sql"""
|
||||
CREATE TABLE session (
|
||||
id INT PRIMARY KEY,
|
||||
ip STRING NOT NULL,
|
||||
password STRING NOT NULL,
|
||||
userid INT NOT NULL,
|
||||
lastModified DATETIME NOT NULL DEFAULT DATETIME('now')
|
||||
)
|
||||
""")
|
||||
|
||||
db.exec(sql"""
|
||||
CREATE TABLE antibot (
|
||||
id INT PRIMARY KEY,
|
||||
ip STRING NOT NULL,
|
||||
answer STRING NOT NULL,
|
||||
created DATETIME NOT NULL DEFAULT DATETIME('now')
|
||||
)
|
||||
""")
|
||||
|
||||
db.exec(sql"CREATE INDEX PersonStatusIdx ON person(user_status)")
|
||||
db.exec(sql"CREATE INDEX PostByAuthorIdx ON post(thread, author)")
|
||||
|
||||
# Insert and query forum data
|
||||
db.exec(sql"INSERT INTO thread (id, name, views) VALUES (1, 'First Thread', 0)")
|
||||
db.exec(sql"INSERT INTO person (id, name, password, email, salt, user_status, ban) VALUES (1, 'admin', 'pw', 'a@b.c', 'salt', 'active', '')")
|
||||
db.exec(sql"INSERT INTO post (id, author, ip, header, content, thread) VALUES (1, 1, '127.0.0.1', 'Hello', 'World', 1)")
|
||||
|
||||
let threads = db.getAllRows(sql"SELECT * FROM thread")
|
||||
check threads.len == 1
|
||||
|
||||
let posts = db.getAllRows(sql"SELECT * FROM post WHERE thread = 1")
|
||||
check posts.len == 1
|
||||
|
||||
db.close()
|
||||
|
||||
test "NimForum-like operations":
|
||||
var db = open("127.0.0.1:" & $port, "", "", "default")
|
||||
|
||||
db.exec(sql"CREATE TABLE nf_ops (id INT PRIMARY KEY, views INT NOT NULL)")
|
||||
db.exec(sql"INSERT INTO nf_ops (id, views) VALUES (1, 0)")
|
||||
|
||||
# Update views (like nimforum does)
|
||||
let affected = db.execAffectedRows(sql"UPDATE nf_ops SET views = views + 1 WHERE id = 1")
|
||||
check affected == 1
|
||||
|
||||
let views = db.getValue(sql"SELECT views FROM nf_ops WHERE id = 1")
|
||||
check views == "1"
|
||||
|
||||
# Try update with DATETIME('now') (preprocessed by adapter)
|
||||
db.exec(sql"CREATE TABLE nf_dt (id INT PRIMARY KEY, modified DATETIME)")
|
||||
db.exec(sql"INSERT INTO nf_dt (id, modified) VALUES (1, DATETIME('now'))")
|
||||
let dtRow = db.getRow(sql"SELECT modified FROM nf_dt WHERE id = 1")
|
||||
check dtRow.len == 1
|
||||
check dtRow[0].len > 0
|
||||
|
||||
db.close()
|
||||
|
||||
+73
-29
@@ -1,73 +1,104 @@
|
||||
## Stress Test — parallel workloads against LSM-Tree
|
||||
## Each worker gets its own LSMTree instance (thread-safe via internal locks).
|
||||
## Stress Test — parallel workloads against shared LSM-Tree
|
||||
## Validates correctness under concurrent access.
|
||||
import std/os
|
||||
import std/random
|
||||
import std/strutils
|
||||
import std/times
|
||||
import std/monotimes
|
||||
import std/locks
|
||||
import barabadb/storage/lsm
|
||||
|
||||
const
|
||||
NumWorkers = 10
|
||||
OpsPerWorker = 1000
|
||||
KeySpace = 500
|
||||
NumWorkers = 16
|
||||
OpsPerWorker = 2000
|
||||
KeySpace = 1000
|
||||
|
||||
type
|
||||
WorkerArgs = tuple
|
||||
workerId: int
|
||||
dataDir: string
|
||||
db: ptr LSMTree
|
||||
errors: ptr int
|
||||
lock: ptr Lock
|
||||
|
||||
randomize()
|
||||
|
||||
proc runWorker(args: WorkerArgs) {.thread.} =
|
||||
var db: LSMTree
|
||||
var localErrors = 0
|
||||
{.cast(gcsafe).}:
|
||||
db = newLSMTree(args.dataDir)
|
||||
var rng = initRand(args.workerId)
|
||||
|
||||
for i in 0 ..< OpsPerWorker:
|
||||
let op = rand(0 .. 2)
|
||||
let key = "key_" & $args.workerId & "_" & $(rand(0 ..< KeySpace))
|
||||
let op = rng.rand(0 .. 3)
|
||||
let key = "key_" & $(rng.rand(0 ..< KeySpace))
|
||||
let value = cast[seq[byte]]("val_" & $i & "_" & $args.workerId)
|
||||
|
||||
case op
|
||||
of 0:
|
||||
db.put(key, value)
|
||||
# Put
|
||||
acquire(args.lock[])
|
||||
try:
|
||||
args.db[].put(key, value)
|
||||
except:
|
||||
localErrors.inc
|
||||
finally:
|
||||
release(args.lock[])
|
||||
of 1:
|
||||
let (found, val) = db.get(key)
|
||||
# Get and verify (if found, value should be from some worker)
|
||||
acquire(args.lock[])
|
||||
var found = false
|
||||
var val: seq[byte]
|
||||
try:
|
||||
(found, val) = args.db[].get(key)
|
||||
except:
|
||||
localErrors.inc
|
||||
finally:
|
||||
release(args.lock[])
|
||||
if found:
|
||||
if val != value and val.len > 0:
|
||||
discard
|
||||
let valStr = cast[string](val)
|
||||
if not valStr.startsWith("val_"):
|
||||
localErrors.inc
|
||||
of 2:
|
||||
db.delete(key)
|
||||
# Delete
|
||||
acquire(args.lock[])
|
||||
try:
|
||||
args.db[].delete(key)
|
||||
except:
|
||||
localErrors.inc
|
||||
finally:
|
||||
release(args.lock[])
|
||||
of 3:
|
||||
# Overwrite
|
||||
acquire(args.lock[])
|
||||
try:
|
||||
args.db[].put(key, value)
|
||||
except:
|
||||
localErrors.inc
|
||||
finally:
|
||||
release(args.lock[])
|
||||
else:
|
||||
discard
|
||||
|
||||
for k in 0 ..< KeySpace:
|
||||
let key = "key_" & $args.workerId & "_" & $k
|
||||
let (found, _) = db.get(key)
|
||||
discard found
|
||||
|
||||
db.close()
|
||||
args.errors[] = localErrors
|
||||
|
||||
proc main() =
|
||||
let baseDir = "/tmp/baradb_stress_test"
|
||||
let baseDir = "/tmp/baradb_stress_test_shared"
|
||||
removeDir(baseDir)
|
||||
createDir(baseDir)
|
||||
|
||||
let start = getMonoTime()
|
||||
|
||||
var workerDirs: seq[string] = @[]
|
||||
for i in 0 ..< NumWorkers:
|
||||
workerDirs.add(baseDir / "worker_" & $i)
|
||||
# Single shared database
|
||||
var db = newLSMTree(baseDir)
|
||||
var dbPtr = addr db
|
||||
var dbLock: Lock
|
||||
initLock(dbLock)
|
||||
var lockPtr = addr dbLock
|
||||
|
||||
# Run workers in parallel using std/threads
|
||||
var threadArr: array[NumWorkers, Thread[WorkerArgs]]
|
||||
var errorCounts: array[NumWorkers, int]
|
||||
|
||||
for i in 0 ..< NumWorkers:
|
||||
let args: WorkerArgs = (workerId: i, dataDir: workerDirs[i], errors: addr errorCounts[i])
|
||||
let args: WorkerArgs = (workerId: i, db: dbPtr, errors: addr errorCounts[i], lock: lockPtr)
|
||||
createThread(threadArr[i], runWorker, args)
|
||||
|
||||
var totalErrors = 0
|
||||
@@ -75,15 +106,28 @@ proc main() =
|
||||
joinThread(threadArr[i])
|
||||
totalErrors += errorCounts[i]
|
||||
|
||||
# Verify: scan all keys and ensure no corruption
|
||||
var scanErrors = 0
|
||||
for k in 0 ..< KeySpace:
|
||||
let key = "key_" & $k
|
||||
let (found, val) = db.get(key)
|
||||
if found:
|
||||
let valStr = cast[string](val)
|
||||
if not valStr.startsWith("val_"):
|
||||
scanErrors.inc
|
||||
|
||||
db.close()
|
||||
|
||||
let elapsed = (getMonoTime() - start).inMilliseconds
|
||||
let totalOps = NumWorkers * OpsPerWorker
|
||||
|
||||
echo "Stress test completed: ", totalOps, " ops across ", NumWorkers, " workers"
|
||||
echo "Time: ", elapsed, " ms"
|
||||
echo "Throughput: ", float(totalOps) / (float(elapsed) / 1000.0), " ops/sec"
|
||||
echo "Errors: ", totalErrors
|
||||
echo "Runtime errors: ", totalErrors
|
||||
echo "Scan errors: ", scanErrors
|
||||
|
||||
if totalErrors > 0:
|
||||
if totalErrors > 0 or scanErrors > 0:
|
||||
quit(1)
|
||||
|
||||
removeDir(baseDir)
|
||||
|
||||
Reference in New Issue
Block a user