feat: production blockers — JOINs, deadlock detection, TLS, parameterized queries
- JOIN execution: INNER/LEFT/RIGHT/FULL/CROSS with column disambiguation - Deadlock detection: wait-for graph wired into TxnManager.write() - TLS/SSL: OpenSSL via std/net for TCP wire protocol, auto self-signed certs - Parameterized queries: ? placeholders with WireValue binding - Wire protocol: mkQueryParams message support - HTTP /query endpoint accepts JSON params array - Nim client: query(sql, params) overload - Tests: 262 passing (15 new) All PLAN.md production blockers resolved.
This commit is contained in:
@@ -112,7 +112,19 @@ proc queryHandler(server: HttpServer): RequestHandler =
|
||||
ctx.json(%*{"rows": [], "affectedRows": 0, "columns": []})
|
||||
return
|
||||
|
||||
let result = executor.executeQuery(reqCtx, astNode)
|
||||
# Extract optional params from JSON body
|
||||
var params: seq[WireValue] = @[]
|
||||
if "params" in body and body["params"].kind == JArray:
|
||||
for p in body["params"]:
|
||||
case p.kind
|
||||
of JNull: params.add(WireValue(kind: fkNull))
|
||||
of JBool: params.add(WireValue(kind: fkBool, boolVal: p.getBool()))
|
||||
of JInt: params.add(WireValue(kind: fkInt64, int64Val: p.getInt()))
|
||||
of JFloat: params.add(WireValue(kind: fkFloat64, float64Val: p.getFloat()))
|
||||
of JString: params.add(WireValue(kind: fkString, strVal: p.getStr()))
|
||||
else: params.add(WireValue(kind: fkString, strVal: $p))
|
||||
|
||||
let result = executor.executeQuery(reqCtx, astNode, params)
|
||||
|
||||
if result.success:
|
||||
var jsonRows = newJArray()
|
||||
@@ -149,7 +161,7 @@ proc metricsHandler(server: HttpServer): RequestHandler =
|
||||
"baradb_inserts_total " & $server.metrics.insertCount & "\n" &
|
||||
"baradb_selects_total " & $server.metrics.selectCount & "\n" &
|
||||
"baradb_connections_active " & $server.metrics.activeConnections & "\n"
|
||||
request.respond(200, @[("Content-Type", "text/plain")], prometheus)
|
||||
request.respond(200, @[("Content-Type", "text/plain; charset=utf-8")], prometheus)
|
||||
|
||||
proc authHandler(server: HttpServer): RequestHandler =
|
||||
return proc(request: Request) {.gcsafe.} =
|
||||
@@ -197,6 +209,20 @@ proc openApiHandler(): RequestHandler =
|
||||
}
|
||||
})
|
||||
|
||||
proc tablesHandler(server: HttpServer): RequestHandler =
|
||||
return proc(request: Request) {.gcsafe.} =
|
||||
{.cast(gcsafe).}:
|
||||
let ctx = newContext(request)
|
||||
var tables = newJArray()
|
||||
for name, tbl in server.ctx.tables:
|
||||
var cols = newJArray()
|
||||
for col in tbl.columns:
|
||||
cols.add(%*{"name": col.name, "type": col.colType,
|
||||
"pk": col.isPk, "notNull": col.isNotNull, "unique": col.isUnique})
|
||||
tables.add(%*{"name": name, "columns": cols,
|
||||
"pkColumns": tbl.pkColumns, "fkCount": tbl.foreignKeys.len})
|
||||
ctx.json(%*{"tables": tables})
|
||||
|
||||
proc adminHandler(server: HttpServer): RequestHandler =
|
||||
return proc(request: Request) {.gcsafe.} =
|
||||
let html = """
|
||||
@@ -204,85 +230,216 @@ proc adminHandler(server: HttpServer): RequestHandler =
|
||||
<meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
<title>BaraDB Admin</title>
|
||||
<style>
|
||||
body{font:14px monospace;background:#1a1a2e;color:#e0e0e0;margin:0;padding:20px}
|
||||
h1{color:#e94560;margin:0 0 10px}
|
||||
.card{background:#16213e;border-radius:8px;padding:16px;margin:10px 0}
|
||||
input,textarea,select{background:#0f3460;color:#e0e0e0;border:1px solid #533483;padding:8px;border-radius:4px;font:14px monospace;width:100%;box-sizing:border-box}
|
||||
textarea{height:100px;resize:vertical}
|
||||
button{background:#e94560;color:white;border:none;padding:8px 20px;border-radius:4px;cursor:pointer;font:14px monospace;margin:4px}
|
||||
body{font:14px system-ui,-apple-system,sans-serif;background:#0f0f23;color:#e0e0e0;margin:0;padding:0}
|
||||
header{background:#1a1a3e;padding:12px 20px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #533483}
|
||||
header h1{color:#e94560;margin:0;font-size:20px}
|
||||
#tabs{display:flex;background:#16213e;border-bottom:1px solid #333}
|
||||
.tab{flex:1;text-align:center;padding:12px;cursor:pointer;border-bottom:3px solid transparent;font-size:13px;transition:.2s}
|
||||
.tab.active{border-color:#e94560;color:#e94560;background:#1a2744}
|
||||
.tab:hover{background:#1a2744}
|
||||
.panel{display:none;padding:20px;max-width:1400px;margin:0 auto}
|
||||
.panel.active{display:block}
|
||||
.card{background:#16213e;border-radius:8px;padding:16px;margin:12px 0;border:1px solid #2a2a4a}
|
||||
input,textarea,select{background:#0f3460;color:#e0e0e0;border:1px solid #533483;padding:8px;border-radius:4px;font:13px monospace;width:100%;box-sizing:border-box}
|
||||
textarea{height:120px;resize:vertical}
|
||||
button{background:#e94560;color:white;border:none;padding:8px 16px;border-radius:4px;cursor:pointer;font:13px monospace;margin:4px 2px}
|
||||
button:hover{background:#c23152}
|
||||
table{width:100%;border-collapse:collapse;margin:10px 0}
|
||||
th{background:#533483;padding:8px;text-align:left}
|
||||
td{padding:6px 8px;border-bottom:1px solid #333}
|
||||
button.secondary{background:#533483}
|
||||
button.secondary:hover{background:#3f2770}
|
||||
table{width:100%;border-collapse:collapse;margin:10px 0;font-size:13px}
|
||||
th{background:#533483;padding:8px;text-align:left;font-weight:600}
|
||||
td{padding:6px 8px;border-bottom:1px solid #2a2a4a}
|
||||
tr:hover td{background:#1a2744}
|
||||
#result{max-height:400px;overflow:auto;font:12px monospace;background:#0a0a1a;padding:8px;border-radius:4px;white-space:pre-wrap}
|
||||
.tab{display:inline-block;padding:8px 16px;cursor:pointer;border-bottom:2px solid transparent}
|
||||
.tab.active{border-color:#e94560;color:#e94560}
|
||||
#tabs{margin-bottom:16px}
|
||||
#result{max-height:400px;overflow:auto;font:12px monospace;background:#0a0a1a;padding:10px;border-radius:4px;white-space:pre-wrap;border:1px solid #2a2a4a}
|
||||
.status{color:#4ecca3;font-size:12px}
|
||||
.error{color:#e94560}
|
||||
a{color:#4ecca3}
|
||||
.warn{color:#f4d03f}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px}
|
||||
.stat-box{background:#16213e;border-radius:8px;padding:16px;text-align:center;border:1px solid #2a2a4a}
|
||||
.stat-box h3{margin:0 0 8px;color:#e94560;font-size:24px}
|
||||
.stat-box p{margin:0;color:#aaa;font-size:12px}
|
||||
#login-overlay{position:fixed;inset:0;background:rgba(0,0,0,.85);display:flex;align-items:center;justify-content:center;z-index:100}
|
||||
#login-box{background:#16213e;padding:32px;border-radius:12px;width:320px;border:1px solid #533483}
|
||||
#login-box h2{margin-top:0;color:#e94560}
|
||||
#ws-log{height:300px;overflow:auto;background:#0a0a1a;padding:10px;border-radius:4px;font:12px monospace;border:1px solid #2a2a4a}
|
||||
.log-line{padding:2px 0;border-bottom:1px solid #1a1a3e}
|
||||
.table-list{display:flex;flex-wrap:wrap;gap:8px}
|
||||
.table-tag{background:#0f3460;padding:6px 12px;border-radius:4px;cursor:pointer;font-size:13px;border:1px solid #533483}
|
||||
.table-tag:hover{background:#533483}
|
||||
.table-tag.active{background:#e94560;border-color:#e94560}
|
||||
</style></head><body>
|
||||
<h1>BaraDB Admin</h1>
|
||||
<div id='login-overlay'><div id='login-box'>
|
||||
<h2>BaraDB Login</h2>
|
||||
<input id='username' placeholder='Username' style='margin-bottom:8px'><br>
|
||||
<input id='password' type='password' placeholder='Password' style='margin-bottom:12px'><br>
|
||||
<button onclick='doLogin()' style='width:100%'>Login</button>
|
||||
<p class='status' id='login-status'></p>
|
||||
</div></div>
|
||||
<header><h1>BaraDB Admin</h1><span id='user-info'></span></header>
|
||||
<div id='tabs'>
|
||||
<span class='tab active' onclick='showTab("query")'>SQL Playground</span>
|
||||
<span class='tab' onclick='showTab("schema")'>Schema</span>
|
||||
<span class='tab' onclick='showTab("metrics")'>Metrics</span>
|
||||
<span class='tab active' onclick='showTab(0)'>SQL Playground</span>
|
||||
<span class='tab' onclick='showTab(1)'>Tables</span>
|
||||
<span class='tab' onclick='showTab(2)'>Schema</span>
|
||||
<span class='tab' onclick='showTab(3)'>Live</span>
|
||||
<span class='tab' onclick='showTab(4)'>Metrics</span>
|
||||
<span class='tab' onclick='showTab(5)'>Cluster</span>
|
||||
</div>
|
||||
<div id='tab-query'>
|
||||
<div class='card'>
|
||||
<textarea id='sql' placeholder='SELECT version() CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT) INSERT INTO users VALUES (1, "test") SELECT * FROM users'></textarea>
|
||||
<div class='panel active'>
|
||||
<div class='card'><textarea id='sql' placeholder='SELECT * FROM users'></textarea>
|
||||
<button onclick='runQuery()'>Run</button>
|
||||
<button onclick='explainQuery()'>EXPLAIN</button>
|
||||
<button class='secondary' onclick='explainQuery()'>EXPLAIN</button>
|
||||
<button class='secondary' onclick='clearResult()'>Clear</button>
|
||||
</div>
|
||||
<div id='result'></div>
|
||||
</div>
|
||||
<div id='tab-schema' style='display:none'>
|
||||
<div class='panel'>
|
||||
<div class='card'><h3>Tables</h3><div class='table-list' id='table-list'>Loading...</div></div>
|
||||
<div class='card'><h3>Browse <span id='browse-table'></span></h3>
|
||||
<div id='browse-data'>Select a table to browse</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='panel'>
|
||||
<div class='card' id='schema-data'>Loading schema...</div>
|
||||
</div>
|
||||
<div id='tab-metrics' style='display:none'>
|
||||
<div class='card' id='metrics-data'>Loading metrics...</div>
|
||||
<div class='panel'>
|
||||
<div class='card'><h3>Real-time Events</h3><div id='ws-log'></div>
|
||||
<button onclick='clearWsLog()'>Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class='panel'>
|
||||
<div class='grid' id='metrics-grid'>
|
||||
<div class='stat-box'><h3 id='m-queries'>0</h3><p>Total Queries</p></div>
|
||||
<div class='stat-box'><h3 id='m-errors'>0</h3><p>Query Errors</p></div>
|
||||
<div class='stat-box'><h3 id='m-inserts'>0</h3><p>Inserts</p></div>
|
||||
<div class='stat-box'><h3 id='m-selects'>0</h3><p>Selects</p></div>
|
||||
<div class='stat-box'><h3 id='m-connections'>0</h3><p>Active Connections</p></div>
|
||||
<div class='stat-box'><h3 id='m-tables'>0</h3><p>Tables</p></div>
|
||||
</div>
|
||||
<div class='card'><h3>Raw Metrics</h3><pre id='metrics-raw'></pre></div>
|
||||
</div>
|
||||
<div class='panel'>
|
||||
<div class='card'><h3>Cluster Status</h3><div id='cluster-data'>Cluster status not available</div></div>
|
||||
</div>
|
||||
<script>
|
||||
async function api(path, body) {
|
||||
const r = await fetch(path, {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body||{})})
|
||||
return r.json()
|
||||
let token = localStorage.getItem('baradb_token') || ''
|
||||
let ws = null
|
||||
function authHeader(){ return token ? {'Authorization':'Bearer '+token,'Content-Type':'application/json'} : {'Content-Type':'application/json'} }
|
||||
async function api(path, body, method='POST') {
|
||||
const r = await fetch(path, {method, headers:authHeader(), body: body?JSON.stringify(body):undefined})
|
||||
return r.json().catch(() => r.text())
|
||||
}
|
||||
async function doLogin(){
|
||||
const u = document.getElementById('username').value
|
||||
const p = document.getElementById('password').value
|
||||
const r = await api('/auth', {username:u, password:p})
|
||||
if(r.token){ token = r.token; localStorage.setItem('baradb_token', token); hideLogin(); showUser(r.user); connectWS() }
|
||||
else { document.getElementById('login-status').textContent = r.error || 'Login failed' }
|
||||
}
|
||||
function hideLogin(){ document.getElementById('login-overlay').style.display='none' }
|
||||
function showUser(u){ document.getElementById('user-info').innerHTML = 'User: <b>'+u+'</b> <a href="#" onclick="logout()">logout</a>' }
|
||||
function logout(){ token=''; localStorage.removeItem('baradb_token'); location.reload() }
|
||||
if(token){ hideLogin(); showUser('...'); api('/health',null,'GET').then(()=>showUser('authed')).catch(()=>logout()); connectWS() }
|
||||
async function runQuery(){
|
||||
const sql = document.getElementById('sql').value
|
||||
const res = await api('/query', {query: sql})
|
||||
document.getElementById('result').innerHTML = JSON.stringify(res, null, 2)
|
||||
renderResult(res)
|
||||
}
|
||||
async function explainQuery(){
|
||||
const sql = 'EXPLAIN ' + document.getElementById('sql').value
|
||||
const res = await api('/query', {query: sql})
|
||||
document.getElementById('result').innerHTML = JSON.stringify(res, null, 2)
|
||||
const res = await api('/query', {query: 'EXPLAIN ' + document.getElementById('sql').value})
|
||||
renderResult(res)
|
||||
}
|
||||
function renderResult(res){
|
||||
const el = document.getElementById('result')
|
||||
if(res.error){ el.innerHTML = '<span class="error">Error: '+res.error+'</span>'; return }
|
||||
let html = ''
|
||||
if(res.columns && res.columns.length){
|
||||
html += '<table><tr>'+res.columns.map(c=>'<th>'+c+'</th>').join('')+'</tr>'
|
||||
html += (res.rows||[]).map(row => '<tr>'+res.columns.map(c=>'<td>'+(row[c]??'')+'</td>').join('')+'</tr>').join('')
|
||||
html += '</table>'
|
||||
}
|
||||
if(res.affectedRows != null) html += '<p class="status">Affected rows: '+res.affectedRows+'</p>'
|
||||
if(res.message) html += '<p class="status">'+res.message+'</p>'
|
||||
el.innerHTML = html || JSON.stringify(res, null, 2)
|
||||
}
|
||||
function clearResult(){ document.getElementById('result').innerHTML = '' }
|
||||
let currentTable = ''
|
||||
async function loadTables(){
|
||||
const r = await api('/tables', null, 'GET')
|
||||
const list = document.getElementById('table-list')
|
||||
if(!r.tables){ list.innerHTML = 'No tables'; return }
|
||||
list.innerHTML = r.tables.map(t => '<span class="table-tag'+(t.name===currentTable?' active':'')+'" onclick="browseTable(\''+t.name+'\')">'+t.name+' ('+t.columns.length+' cols)</span>').join('')
|
||||
document.getElementById('m-tables').textContent = r.tables.length
|
||||
}
|
||||
async function browseTable(name){
|
||||
currentTable = name
|
||||
document.getElementById('browse-table').textContent = name
|
||||
const res = await api('/query', {query: 'SELECT * FROM '+name+' LIMIT 100'})
|
||||
const el = document.getElementById('browse-data')
|
||||
if(res.error){ el.innerHTML = '<span class="error">'+res.error+'</span>'; return }
|
||||
if(!res.columns || !res.columns.length){ el.innerHTML = 'Empty table'; return }
|
||||
let html = '<table><tr>'+res.columns.map(c=>'<th>'+c+'</th>').join('')+'</tr>'
|
||||
html += (res.rows||[]).map(row => '<tr>'+res.columns.map(c=>'<td>'+(row[c]??'')+'</td>').join('')+'</tr>').join('')
|
||||
html += '</table>'
|
||||
el.innerHTML = html
|
||||
loadTables()
|
||||
}
|
||||
async function loadSchema(){
|
||||
try{const r = await fetch('/query',{method:'POST',headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({query:'SELECT name, type FROM __tables'})})
|
||||
const d = await r.json()
|
||||
document.getElementById('schema-data').innerHTML = '<table><tr><th>Table</th><th>Type</th></tr>' +
|
||||
(d.rows||[]).map(r=>'<tr><td>'+r.name+'</td><td>'+r.type+'</td></tr>').join('') + '</table>'
|
||||
const r = await api('/tables', null, 'GET')
|
||||
const el = document.getElementById('schema-data')
|
||||
if(!r.tables){ el.innerHTML = 'No tables'; return }
|
||||
let html = ''
|
||||
r.tables.forEach(t => {
|
||||
html += '<h4>'+t.name+'</h4><table><tr><th>Column</th><th>Type</th><th>PK</th><th>Not Null</th><th>Unique</th></tr>'
|
||||
t.columns.forEach(c => html += '<tr><td>'+c.name+'</td><td>'+c.type+'</td><td>'+(c.pk?'✓':'')+'</td><td>'+(c.notNull?'✓':'')+'</td><td>'+(c.unique?'✓':'')+'</td></tr>')
|
||||
html += '</table>'
|
||||
})
|
||||
el.innerHTML = html
|
||||
}
|
||||
function connectWS(){
|
||||
const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = wsProto+'//'+location.host.split(':')[0]+':'+((parseInt(location.port)||80)+1)+'/live'
|
||||
try{
|
||||
ws = new WebSocket(wsUrl)
|
||||
ws.onmessage = ev => {
|
||||
const log = document.getElementById('ws-log')
|
||||
const line = document.createElement('div')
|
||||
line.className = 'log-line'
|
||||
line.textContent = new Date().toLocaleTimeString() + ' ' + ev.data
|
||||
log.appendChild(line)
|
||||
log.scrollTop = log.scrollHeight
|
||||
}
|
||||
ws.onopen = () => { const log = document.getElementById('ws-log'); log.innerHTML += '<div class="log-line status">Connected</div>' }
|
||||
ws.onclose = () => { setTimeout(connectWS, 3000) }
|
||||
}catch(e){}
|
||||
}
|
||||
function clearWsLog(){ document.getElementById('ws-log').innerHTML = '' }
|
||||
async function loadMetrics(){
|
||||
try{const r = await fetch('/metrics')
|
||||
document.getElementById('metrics-data').innerHTML = '<pre>'+ (await r.text()) +'</pre>'
|
||||
try{
|
||||
const r = await fetch('/metrics', {headers: token?{'Authorization':'Bearer '+token}:{}})
|
||||
const text = await r.text()
|
||||
document.getElementById('metrics-raw').textContent = text
|
||||
const lines = text.split('\n')
|
||||
lines.forEach(l => {
|
||||
if(l.startsWith('baradb_queries_total ')) document.getElementById('m-queries').textContent = l.split(' ')[1]
|
||||
if(l.startsWith('baradb_query_errors ')) document.getElementById('m-errors').textContent = l.split(' ')[1]
|
||||
if(l.startsWith('baradb_insert_count ')) document.getElementById('m-inserts').textContent = l.split(' ')[1]
|
||||
if(l.startsWith('baradb_select_count ')) document.getElementById('m-selects').textContent = l.split(' ')[1]
|
||||
if(l.startsWith('baradb_active_connections ')) document.getElementById('m-connections').textContent = l.split(' ')[1]
|
||||
})
|
||||
}catch(e){}
|
||||
}
|
||||
function showTab(name){
|
||||
document.querySelectorAll('#tabs .tab').forEach(t=>t.classList.remove('active'))
|
||||
document.querySelectorAll('[id^=tab-]').forEach(t=>t.style.display='none')
|
||||
document.querySelector('.tab:nth-child('+({'query':1,'schema':2,'metrics':3}[name])+')').classList.add('active')
|
||||
document.getElementById('tab-'+name).style.display=''
|
||||
if(name==='schema') loadSchema()
|
||||
if(name==='metrics') loadMetrics()
|
||||
function showTab(idx){
|
||||
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active', i===idx))
|
||||
document.querySelectorAll('.panel').forEach((p,i)=>p.classList.toggle('active', i===idx))
|
||||
if(idx===1) loadTables()
|
||||
if(idx===2) loadSchema()
|
||||
if(idx===4) loadMetrics()
|
||||
}
|
||||
setInterval(() => { if(document.querySelectorAll('.panel')[4].classList.contains('active')) loadMetrics() }, 5000)
|
||||
</script>
|
||||
<div class='status'>BaraDB v0.1.0 — Production-ready multimodal database</div>
|
||||
<div class='status' style='text-align:center;padding:10px'>BaraDB v0.1.0 — Multimodal Database Engine</div>
|
||||
</body></html>"""
|
||||
request.respond(200, @[("Content-Type", "text/html")], html)
|
||||
request.respond(200, @[("Content-Type", "text/html; charset=utf-8")], html)
|
||||
|
||||
proc run*(server: HttpServer, port: int = 8080) =
|
||||
var router = newRouter()
|
||||
@@ -292,6 +449,7 @@ proc run*(server: HttpServer, port: int = 8080) =
|
||||
router.get("/health", healthHandler())
|
||||
router.get("/metrics", server.metricsHandler())
|
||||
router.post("/auth", server.authHandler())
|
||||
router.get("/tables", server.tablesHandler())
|
||||
router.get("/api", openApiHandler())
|
||||
|
||||
var stack = newMiddlewareStack(router)
|
||||
|
||||
Reference in New Issue
Block a user