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:
@@ -6,6 +6,9 @@ type
|
||||
maxConnections*: int
|
||||
walEnabled*: bool
|
||||
compactionStrategy*: CompactionStrategy
|
||||
tlsEnabled*: bool
|
||||
certFile*: string
|
||||
keyFile*: string
|
||||
|
||||
CompactionStrategy* = enum
|
||||
csSizeTiered = "size_tiered"
|
||||
@@ -19,6 +22,9 @@ proc defaultConfig*(): BaraConfig =
|
||||
maxConnections: 1000,
|
||||
walEnabled: true,
|
||||
compactionStrategy: csLeveled,
|
||||
tlsEnabled: false,
|
||||
certFile: "",
|
||||
keyFile: "",
|
||||
)
|
||||
|
||||
proc loadConfig*(): BaraConfig =
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -3,6 +3,7 @@ import std/tables
|
||||
import std/locks
|
||||
import std/monotimes
|
||||
import std/sets
|
||||
import deadlock
|
||||
|
||||
type
|
||||
TxnId* = distinct uint64
|
||||
@@ -44,6 +45,7 @@ type
|
||||
committedTxns: seq[TxnId]
|
||||
globalVersions*: Table[string, seq[VersionedRecord]]
|
||||
txnTimeoutMs: int64
|
||||
deadlockDetector*: DeadlockDetector
|
||||
|
||||
proc `==`*(a, b: TxnId): bool {.borrow.}
|
||||
proc `==`*(a, b: Lsn): bool {.borrow.}
|
||||
@@ -59,6 +61,7 @@ proc newTxnManager*(txnTimeoutMs: int64 = 30000): TxnManager =
|
||||
result.committedTxns = @[]
|
||||
result.globalVersions = initTable[string, seq[VersionedRecord]]()
|
||||
result.txnTimeoutMs = txnTimeoutMs
|
||||
result.deadlockDetector = newDeadlockDetector()
|
||||
|
||||
proc allocTxnId(tm: TxnManager): TxnId =
|
||||
result = TxnId(tm.nextTxnId)
|
||||
@@ -167,7 +170,22 @@ proc write*(tm: TxnManager, txn: Transaction, key: string, value: seq[byte]): bo
|
||||
otherTxn.state = tsAborted
|
||||
tm.activeTxns.del(otherId)
|
||||
|
||||
# Check for write-write conflict
|
||||
# Check for write-write conflict against other active transactions' write sets
|
||||
for otherId, otherTxn in tm.activeTxns:
|
||||
if otherId != txn.id and otherTxn.state == tsActive:
|
||||
if key in otherTxn.writeSet:
|
||||
# Wait-for graph: txn is waiting for otherId to finish
|
||||
tm.deadlockDetector.addWait(uint64(txn.id), uint64(otherId))
|
||||
if tm.deadlockDetector.hasDeadlock():
|
||||
let victimId = TxnId(tm.deadlockDetector.findDeadlockVictim())
|
||||
tm.deadlockDetector.removeTxn(uint64(victimId))
|
||||
if victimId in tm.activeTxns:
|
||||
tm.activeTxns[victimId].state = tsAborted
|
||||
tm.activeTxns.del(victimId)
|
||||
release(tm.lock)
|
||||
return false # write-write conflict with uncommitted txn
|
||||
|
||||
# Check for write-write conflict with committed versions
|
||||
if key notin txn.writeSet:
|
||||
if key in tm.globalVersions:
|
||||
for version in tm.globalVersions[key]:
|
||||
@@ -229,6 +247,7 @@ proc commit*(tm: TxnManager, txn: Transaction): bool =
|
||||
txn.state = tsCommitted
|
||||
tm.committedTxns.add(txn.id)
|
||||
tm.activeTxns.del(txn.id)
|
||||
tm.deadlockDetector.removeTxn(uint64(txn.id))
|
||||
release(tm.lock)
|
||||
return true
|
||||
|
||||
@@ -239,6 +258,7 @@ proc abortTxn*(tm: TxnManager, txn: Transaction): bool =
|
||||
return false
|
||||
txn.state = tsAborted
|
||||
tm.activeTxns.del(txn.id)
|
||||
tm.deadlockDetector.removeTxn(uint64(txn.id))
|
||||
release(tm.lock)
|
||||
return true
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import std/os
|
||||
import std/endians
|
||||
import config
|
||||
import ../protocol/wire
|
||||
import ../protocol/ssl
|
||||
import ../query/lexer
|
||||
import ../query/parser
|
||||
import ../query/ast
|
||||
@@ -16,11 +17,12 @@ import ../core/mvcc
|
||||
|
||||
type
|
||||
Server* = ref object
|
||||
config: BaraConfig
|
||||
running: bool
|
||||
db: LSMTree
|
||||
ctx: ExecutionContext
|
||||
txnManager: TxnManager
|
||||
config*: BaraConfig
|
||||
running*: bool
|
||||
db*: LSMTree
|
||||
ctx*: ExecutionContext
|
||||
txnManager*: TxnManager
|
||||
tls*: TLSContext
|
||||
|
||||
ClientConnection = ref object
|
||||
socket: AsyncSocket
|
||||
@@ -32,8 +34,12 @@ proc newServer*(config: BaraConfig): Server =
|
||||
let db = newLSMTree(dataDir)
|
||||
let ctx = newExecutionContext(db)
|
||||
ctx.txnManager = newTxnManager()
|
||||
var tls: TLSContext = nil
|
||||
if config.tlsEnabled and config.certFile.len > 0 and config.keyFile.len > 0:
|
||||
let tlsConfig = newTLSConfig(config.certFile, config.keyFile)
|
||||
tls = newTLSContext(tlsConfig)
|
||||
Server(config: config, running: false, db: db, ctx: ctx,
|
||||
txnManager: ctx.txnManager)
|
||||
txnManager: ctx.txnManager, tls: tls)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Wire Protocol Helpers
|
||||
@@ -63,7 +69,7 @@ proc parseHeader(data: string): (bool, MessageHeader) =
|
||||
# Query Execution (pipeline-based)
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string): (bool, QueryResult, string) =
|
||||
proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string, params: seq[WireValue] = @[]): (bool, QueryResult, string) =
|
||||
try:
|
||||
let tokens = tokenize(query)
|
||||
let astNode = parse(tokens)
|
||||
@@ -71,7 +77,7 @@ proc executeQuery(db: LSMTree, ctx: ExecutionContext, query: string): (bool, Que
|
||||
if astNode.stmts.len == 0:
|
||||
return (true, QueryResult(), "")
|
||||
|
||||
let result = executor.executeQuery(ctx, astNode)
|
||||
let result = executor.executeQuery(ctx, astNode, params)
|
||||
if result.success:
|
||||
var qr = QueryResult(affectedRows: result.affectedRows, rowCount: result.rows.len)
|
||||
qr.columns = result.columns
|
||||
@@ -186,6 +192,21 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
|
||||
let errorMsg = serializeError(1, errorMsg, header.requestId)
|
||||
await client.send(cast[string](errorMsg))
|
||||
|
||||
of mkQueryParams:
|
||||
let (queryStr, params) = readQueryParamsMessage(cast[seq[byte]](payload))
|
||||
echo "[", clientId, "] QueryParams: ", queryStr, " (", params.len, " params)"
|
||||
|
||||
let (success, result, errorMsg) = executeQuery(server.db, connCtx, queryStr, params)
|
||||
if success:
|
||||
if result.rows.len > 0:
|
||||
let dataMsg = serializeResult(result, header.requestId)
|
||||
await client.send(cast[string](dataMsg))
|
||||
let completeMsg = serializeComplete(result.affectedRows, header.requestId)
|
||||
await client.send(cast[string](completeMsg))
|
||||
else:
|
||||
let errorMsg = serializeError(1, errorMsg, header.requestId)
|
||||
await client.send(cast[string](errorMsg))
|
||||
|
||||
of mkPing:
|
||||
var pongPayload: seq[byte] = @[]
|
||||
var pongMsg = WireMessage(
|
||||
@@ -214,9 +235,19 @@ proc run*(server: Server) {.async.} =
|
||||
sock.setSockOpt(OptReuseAddr, true)
|
||||
sock.bindAddr(Port(server.config.port), server.config.address)
|
||||
sock.listen()
|
||||
echo "BaraDB listening on ", server.config.address, ":", server.config.port
|
||||
if server.config.tlsEnabled:
|
||||
echo "BaraDB TLS listening on ", server.config.address, ":", server.config.port
|
||||
else:
|
||||
echo "BaraDB listening on ", server.config.address, ":", server.config.port
|
||||
while server.running:
|
||||
let client = await sock.accept()
|
||||
if server.tls != nil:
|
||||
try:
|
||||
server.tls.wrapServer(client)
|
||||
except Exception as e:
|
||||
echo "TLS handshake failed: ", e.msg
|
||||
client.close()
|
||||
continue
|
||||
inc clientId
|
||||
asyncCheck server.handleClient(client, clientId)
|
||||
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
## TLS/SSL Wrapper — encrypted socket using OpenSSL
|
||||
## TLS/SSL Wrapper — encrypted sockets using OpenSSL (Nim stdlib)
|
||||
when not defined(ssl):
|
||||
{.error: "BaraDB requires SSL support. Compile with -d:ssl".}
|
||||
|
||||
import std/os
|
||||
import std/osproc
|
||||
import std/strutils
|
||||
import std/net
|
||||
import std/asyncnet
|
||||
|
||||
type
|
||||
TLSSocket* = ref object
|
||||
ctx: pointer # SSL_CTX*
|
||||
ssl: pointer # SSL*
|
||||
fd: int
|
||||
connected: bool
|
||||
config: TLSConfig
|
||||
|
||||
TLSConfig* = object
|
||||
certFile*: string
|
||||
keyFile*: string
|
||||
caFile*: string
|
||||
verifyPeer*: bool
|
||||
|
||||
TLSContext* = ref object
|
||||
sslCtx*: SslContext
|
||||
config*: TLSConfig
|
||||
|
||||
proc newTLSConfig*(certFile: string, keyFile: string, caFile: string = "",
|
||||
verifyPeer: bool = false): TLSConfig =
|
||||
TLSConfig(
|
||||
@@ -23,44 +26,28 @@ proc newTLSConfig*(certFile: string, keyFile: string, caFile: string = "",
|
||||
caFile: caFile, verifyPeer: verifyPeer,
|
||||
)
|
||||
|
||||
proc newTLSSocket*(config: TLSConfig): TLSSocket =
|
||||
TLSSocket(config: config, connected: false)
|
||||
proc newTLSContext*(config: TLSConfig): TLSContext =
|
||||
result = TLSContext(config: config)
|
||||
if fileExists(config.certFile) and fileExists(config.keyFile):
|
||||
result.sslCtx = newContext(
|
||||
certFile = config.certFile,
|
||||
keyFile = config.keyFile,
|
||||
)
|
||||
else:
|
||||
raise newException(IOError, "TLS certificate or key file not found: " &
|
||||
config.certFile & ", " & config.keyFile)
|
||||
|
||||
proc connect*(sock: TLSSocket, host: string, port: int): bool =
|
||||
# In production, would use OpenSSL SSL_connect() via FFI
|
||||
# For now, validate config and return mock connection
|
||||
if not fileExists(sock.config.certFile):
|
||||
return false
|
||||
sock.connected = true
|
||||
return true
|
||||
proc wrapClient*(tls: TLSContext, socket: AsyncSocket) {.inline.} =
|
||||
if tls.sslCtx != nil:
|
||||
tls.sslCtx.wrapSocket(socket)
|
||||
|
||||
proc accept*(sock: TLSSocket, clientFd: int): bool =
|
||||
if not fileExists(sock.config.certFile):
|
||||
return false
|
||||
if not fileExists(sock.config.keyFile):
|
||||
return false
|
||||
sock.fd = clientFd
|
||||
sock.connected = true
|
||||
return true
|
||||
proc wrapServer*(tls: TLSContext, socket: AsyncSocket) {.inline.} =
|
||||
if tls.sslCtx != nil:
|
||||
tls.sslCtx.wrapConnectedSocket(socket, handshakeAsServer)
|
||||
|
||||
proc `send`*(sock: TLSSocket, data: seq[byte]): int =
|
||||
if not sock.connected:
|
||||
return -1
|
||||
# In production: SSL_write(sock.ssl, data[0].addr, data.len.cint)
|
||||
return data.len
|
||||
|
||||
proc `recv`*(sock: TLSSocket, buf: var seq[byte], size: int): int =
|
||||
if not sock.connected:
|
||||
return -1
|
||||
# In production: SSL_read(sock.ssl, buf[0].addr, size.cint)
|
||||
buf.setLen(min(buf.len, size))
|
||||
return size
|
||||
|
||||
proc close*(sock: TLSSocket) =
|
||||
sock.connected = false
|
||||
# In production: SSL_shutdown(sock.ssl); SSL_free(sock.ssl); SSL_CTX_free(sock.ctx)
|
||||
|
||||
proc isConnected*(sock: TLSSocket): bool = sock.connected
|
||||
proc close*(tls: TLSContext) =
|
||||
if tls.sslCtx != nil:
|
||||
tls.sslCtx.destroyContext()
|
||||
|
||||
# TLS Certificate management
|
||||
type
|
||||
@@ -77,28 +64,21 @@ proc parseCertInfo*(certPath: string): CertInfo =
|
||||
result = CertInfo()
|
||||
if not fileExists(certPath):
|
||||
return
|
||||
|
||||
# Read PEM certificate and extract basic info
|
||||
let content = readFile(certPath)
|
||||
result.subject = "Unknown"
|
||||
result.issuer = "Unknown"
|
||||
result.fingerprint = ""
|
||||
|
||||
# In production: use OpenSSL X509 parsing
|
||||
for line in content.splitLines():
|
||||
if line.startsWith("Subject:"):
|
||||
result.subject = line[8..^1].strip()
|
||||
elif line.startsWith("Issuer:"):
|
||||
result.issuer = line[7..^1].strip()
|
||||
|
||||
result.isSelfSigned = result.subject == result.issuer
|
||||
|
||||
proc generateSelfSignedCert*(outputDir: string, commonName: string = "localhost"): (string, string) =
|
||||
let certPath = outputDir / (commonName & ".crt")
|
||||
let keyPath = outputDir / (commonName & ".key")
|
||||
createDir(outputDir)
|
||||
|
||||
# Use openssl CLI if available
|
||||
let cmd = "openssl req -x509 -newkey rsa:2048 -keyout " & keyPath &
|
||||
" -out " & certPath & " -days 365 -nodes -subj '/CN=" & commonName & "' 2>/dev/null"
|
||||
if execShellCmd(cmd) == 0 and fileExists(certPath):
|
||||
@@ -109,9 +89,13 @@ proc certificateFingerprint*(certPath: string): string =
|
||||
if not fileExists(certPath):
|
||||
return ""
|
||||
let cmd = "openssl x509 -in " & certPath & " -fingerprint -noout 2>/dev/null"
|
||||
result = ""
|
||||
# In production, use popen() to read command output
|
||||
discard execShellCmd(cmd)
|
||||
let (output, _) = execCmdEx(cmd)
|
||||
for line in output.splitLines():
|
||||
if "Fingerprint=" in line:
|
||||
let parts = line.split("Fingerprint=")
|
||||
if parts.len > 1:
|
||||
return parts[^1].strip()
|
||||
return ""
|
||||
|
||||
proc isExpired*(certPath: string): bool =
|
||||
if not fileExists(certPath):
|
||||
@@ -122,12 +106,15 @@ proc isExpired*(certPath: string): bool =
|
||||
proc daysUntilExpiry*(certPath: string): int =
|
||||
if not fileExists(certPath):
|
||||
return -1
|
||||
let cmd = "openssl x509 -in " & certPath & " -checkend 86400 2>/dev/null"
|
||||
if execShellCmd(cmd) == 0:
|
||||
# Expires in more than 1 day
|
||||
# In production, parse the actual enddate
|
||||
return 365
|
||||
return 0
|
||||
# Check if expires within 1 day
|
||||
let cmd1 = "openssl x509 -in " & certPath & " -checkend 86400 2>/dev/null"
|
||||
if execShellCmd(cmd1) == 0:
|
||||
# Check if expires within 30 days
|
||||
let cmd30 = "openssl x509 -in " & certPath & " -checkend 2592000 2>/dev/null"
|
||||
if execShellCmd(cmd30) == 0:
|
||||
return 365
|
||||
return 30
|
||||
return 1
|
||||
|
||||
proc validateCert*(certPath: string): seq[string] =
|
||||
result = @[]
|
||||
|
||||
@@ -260,6 +260,31 @@ proc makeQueryMessage*(requestId: uint32, query: string): seq[byte] =
|
||||
)
|
||||
return serializeMessage(msg)
|
||||
|
||||
proc makeQueryParamsMessage*(requestId: uint32, query: string, params: seq[WireValue]): seq[byte] =
|
||||
var payload: seq[byte] = @[]
|
||||
payload.writeString(query)
|
||||
payload.add(byte(rfBinary))
|
||||
payload.writeUint32(uint32(params.len))
|
||||
for p in params:
|
||||
payload.serializeValue(p)
|
||||
|
||||
var msg = WireMessage(
|
||||
header: MessageHeader(kind: mkQueryParams, length: uint32(payload.len), requestId: requestId),
|
||||
payload: payload,
|
||||
)
|
||||
return serializeMessage(msg)
|
||||
|
||||
proc readQueryParamsMessage*(payload: openArray[byte]): (string, seq[WireValue]) =
|
||||
var pos = 0
|
||||
let queryStr = readString(payload, pos)
|
||||
discard payload[pos] # format byte
|
||||
pos += 1
|
||||
let paramCount = int(readUint32(payload, pos))
|
||||
var params: seq[WireValue] = @[]
|
||||
for i in 0..<paramCount:
|
||||
params.add(deserializeValue(payload, pos))
|
||||
return (queryStr, params)
|
||||
|
||||
proc makeReadyMessage*(requestId: uint32): seq[byte] =
|
||||
var payload: seq[byte] = @[]
|
||||
payload.add(0'u8) # idle state
|
||||
|
||||
@@ -22,8 +22,22 @@ type
|
||||
nkExplainStmt
|
||||
nkCreateView
|
||||
nkDropView
|
||||
nkCreateTrigger
|
||||
nkDropTrigger
|
||||
nkCreateMigration
|
||||
nkApplyMigration
|
||||
nkMigrationStatus
|
||||
nkMigrationUp
|
||||
nkMigrationDown
|
||||
nkMigrationDryRun
|
||||
nkCreateUser
|
||||
nkDropUser
|
||||
nkCreatePolicy
|
||||
nkDropPolicy
|
||||
nkEnableRLS
|
||||
nkDisableRLS
|
||||
nkGrant
|
||||
nkRevoke
|
||||
|
||||
# Clauses
|
||||
nkFrom
|
||||
@@ -59,6 +73,8 @@ type
|
||||
nkBetweenExpr
|
||||
nkLikeExpr
|
||||
nkIsExpr
|
||||
nkStar
|
||||
nkPlaceholder
|
||||
|
||||
# Graph-specific
|
||||
nkGraphTraversal
|
||||
@@ -129,6 +145,7 @@ type
|
||||
Node* = ref object
|
||||
line*: int
|
||||
col*: int
|
||||
exprAlias*: string
|
||||
case kind*: NodeKind
|
||||
of nkSelect:
|
||||
selDistinct*: bool
|
||||
@@ -211,11 +228,61 @@ type
|
||||
of nkDropView:
|
||||
dvName*: string
|
||||
dvIfExists*: bool
|
||||
of nkCreateTrigger:
|
||||
trigName*: string
|
||||
trigTable*: string
|
||||
trigTiming*: string # BEFORE, AFTER, INSTEAD OF
|
||||
trigEvent*: string # INSERT, UPDATE, DELETE
|
||||
trigAction*: Node # SQL statement to execute
|
||||
of nkDropTrigger:
|
||||
trigDropName*: string
|
||||
trigDropIfExists*: bool
|
||||
of nkCreateMigration:
|
||||
cmName*: string
|
||||
cmBody*: string
|
||||
cmBody*: string # UP migration body (SQL string)
|
||||
cmDownBody*: string # DOWN migration body (SQL string)
|
||||
cmChecksum*: string # SHA256 of cmBody
|
||||
cmAuthor*: string
|
||||
cmTimestamp*: int64
|
||||
of nkCreateUser:
|
||||
cuName*: string
|
||||
cuPassword*: string
|
||||
cuSuperuser*: bool
|
||||
of nkDropUser:
|
||||
duName*: string
|
||||
duIfExists*: bool
|
||||
of nkCreatePolicy:
|
||||
cpName*: string
|
||||
cpTable*: string
|
||||
cpCommand*: string # ALL, SELECT, INSERT, UPDATE, DELETE
|
||||
cpUsing*: Node # USING expression
|
||||
cpWithCheck*: Node # WITH CHECK expression
|
||||
of nkDropPolicy:
|
||||
dpName*: string
|
||||
dpTable*: string
|
||||
dpIfExists*: bool
|
||||
of nkEnableRLS:
|
||||
erlsTable*: string
|
||||
of nkDisableRLS:
|
||||
drlsTable*: string
|
||||
of nkGrant:
|
||||
grPrivilege*: string # SELECT, INSERT, UPDATE, DELETE, ALL
|
||||
grTable*: string
|
||||
grGrantee*: string # user, role, or PUBLIC
|
||||
of nkRevoke:
|
||||
rvPrivilege*: string
|
||||
rvTable*: string
|
||||
rvGrantee*: string
|
||||
of nkApplyMigration:
|
||||
amName*: string
|
||||
of nkMigrationStatus:
|
||||
discard # no fields needed
|
||||
of nkMigrationUp:
|
||||
muCount*: int # 0 = all, N = apply N pending migrations
|
||||
of nkMigrationDown:
|
||||
mdCount*: int # 0 = 1, N = rollback N migrations
|
||||
of nkMigrationDryRun:
|
||||
mdrName*: string # migration name to dry-run
|
||||
of nkCreateIndex:
|
||||
ciTarget*: string
|
||||
ciName*: string
|
||||
@@ -344,6 +411,10 @@ type
|
||||
joinTarget*: Node
|
||||
joinOn*: Node
|
||||
joinAlias*: string
|
||||
of nkStar:
|
||||
discard
|
||||
of nkPlaceholder:
|
||||
discard
|
||||
of nkPropertyDef:
|
||||
pdName*: string
|
||||
pdType*: string
|
||||
|
||||
+842
-33
File diff suppressed because it is too large
Load Diff
@@ -49,6 +49,7 @@ type
|
||||
irekCast
|
||||
irekConditional
|
||||
irekExists
|
||||
irekStar
|
||||
|
||||
IRJoinKind* = enum
|
||||
irjkInner
|
||||
@@ -163,6 +164,8 @@ type
|
||||
elseExpr*: IRExpr
|
||||
of irekExists:
|
||||
existsSubquery*: IRPlan
|
||||
of irekStar:
|
||||
discard
|
||||
|
||||
type
|
||||
TypeChecker* = ref object
|
||||
@@ -240,3 +243,5 @@ proc inferExpr*(tc: TypeChecker, expr: IRExpr, context: Table[string, IRType]):
|
||||
return thenType
|
||||
of irekExists:
|
||||
return IRType(name: "bool", kind: itkScalar)
|
||||
of irekStar:
|
||||
return IRType(name: "star", kind: itkScalar)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
## BaraQL Lexer — tokenization
|
||||
import std/tables
|
||||
import std/strutils
|
||||
import std/unicode
|
||||
|
||||
type
|
||||
TokenKind* = enum
|
||||
@@ -91,8 +92,25 @@ type
|
||||
tkRollback
|
||||
tkExplain
|
||||
tkView
|
||||
tkTrigger
|
||||
tkBefore
|
||||
tkAfter
|
||||
tkInstead
|
||||
tkOf
|
||||
tkMigration
|
||||
tkApply
|
||||
tkStatus
|
||||
tkUp
|
||||
tkDown
|
||||
tkDryRun
|
||||
tkUser
|
||||
tkPolicy
|
||||
tkEnable
|
||||
tkDisable
|
||||
tkFor
|
||||
tkUsing
|
||||
tkGrant
|
||||
tkRevoke
|
||||
tkCount
|
||||
tkSum
|
||||
tkAvg
|
||||
@@ -141,6 +159,7 @@ type
|
||||
tkConcat
|
||||
tkCoalesce
|
||||
tkFloorDiv
|
||||
tkPlaceholder
|
||||
|
||||
# Special
|
||||
tkEof
|
||||
@@ -237,8 +256,25 @@ const keywords*: Table[string, TokenKind] = {
|
||||
"rollback": tkRollback,
|
||||
"explain": tkExplain,
|
||||
"view": tkView,
|
||||
"trigger": tkTrigger,
|
||||
"before": tkBefore,
|
||||
"after": tkAfter,
|
||||
"instead": tkInstead,
|
||||
"of": tkOf,
|
||||
"migration": tkMigration,
|
||||
"apply": tkApply,
|
||||
"status": tkStatus,
|
||||
"up": tkUp,
|
||||
"down": tkDown,
|
||||
"dryrun": tkDryRun,
|
||||
"user": tkUser,
|
||||
"policy": tkPolicy,
|
||||
"enable": tkEnable,
|
||||
"disable": tkDisable,
|
||||
"for": tkFor,
|
||||
"using": tkUsing,
|
||||
"grant": tkGrant,
|
||||
"revoke": tkRevoke,
|
||||
"count": tkCount,
|
||||
"sum": tkSum,
|
||||
"avg": tkAvg,
|
||||
@@ -273,6 +309,32 @@ proc advance(l: var Lexer): char =
|
||||
else:
|
||||
inc l.col
|
||||
|
||||
proc peekRune(l: Lexer): Rune =
|
||||
if l.pos < l.input.len:
|
||||
var p = l.pos
|
||||
var r: Rune
|
||||
fastRuneAt(l.input, p, r, true)
|
||||
return r
|
||||
return Rune(0)
|
||||
|
||||
proc advanceRune(l: var Lexer): Rune =
|
||||
if l.pos < l.input.len:
|
||||
var r: Rune
|
||||
fastRuneAt(l.input, l.pos, r, true)
|
||||
if r == Rune('\n'):
|
||||
inc l.line
|
||||
l.col = 1
|
||||
else:
|
||||
inc l.col
|
||||
return r
|
||||
return Rune(0)
|
||||
|
||||
proc isIdentStartRune(r: Rune): bool =
|
||||
return isAlpha(r) or r == Rune('_')
|
||||
|
||||
proc isIdentPartRune(r: Rune): bool =
|
||||
return isAlpha(r) or (r.int >= ord('0') and r.int <= ord('9')) or r == Rune('_')
|
||||
|
||||
proc skipWhitespace(l: var Lexer) =
|
||||
while l.pos < l.input.len and l.input[l.pos] in {' ', '\t', '\r', '\n'}:
|
||||
discard l.advance()
|
||||
@@ -326,9 +388,15 @@ proc readNumber(l: var Lexer, startLine, startCol: int): Token =
|
||||
|
||||
proc readIdent(l: var Lexer, startLine, startCol: int): Token =
|
||||
var ident = ""
|
||||
while l.pos < l.input.len and (l.input[l.pos] in IdentChars or l.input[l.pos] in Digits):
|
||||
ident.add(l.input[l.pos])
|
||||
discard l.advance()
|
||||
while l.pos < l.input.len:
|
||||
let r = l.peekRune()
|
||||
if isIdentPartRune(r):
|
||||
var run: Rune
|
||||
fastRuneAt(l.input, l.pos, run, true)
|
||||
ident.add($run)
|
||||
inc l.col
|
||||
else:
|
||||
break
|
||||
let lowerIdent = ident.toLower()
|
||||
if lowerIdent in keywords:
|
||||
Token(kind: keywords[lowerIdent], value: ident, line: startLine, col: startCol)
|
||||
@@ -428,7 +496,7 @@ proc nextToken*(l: var Lexer): Token =
|
||||
discard l.advance()
|
||||
return Token(kind: tkCoalesce, value: "??", line: startLine, col: startCol)
|
||||
discard l.advance()
|
||||
return Token(kind: tkInvalid, value: "?", line: startLine, col: startCol)
|
||||
return Token(kind: tkPlaceholder, value: "?", line: startLine, col: startCol)
|
||||
of '.':
|
||||
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '<':
|
||||
discard l.advance()
|
||||
@@ -479,7 +547,7 @@ proc nextToken*(l: var Lexer): Token =
|
||||
else:
|
||||
if ch in Digits:
|
||||
return l.readNumber(startLine, startCol)
|
||||
elif ch in IdentStartChars:
|
||||
elif ch in IdentStartChars or isIdentStartRune(l.peekRune()):
|
||||
return l.readIdent(startLine, startCol)
|
||||
else:
|
||||
discard l.advance()
|
||||
|
||||
@@ -105,6 +105,12 @@ proc parsePrimary(p: var Parser): Node =
|
||||
let sub = p.parseSelect()
|
||||
discard p.expect(tkRParen)
|
||||
Node(kind: nkExists, existsExpr: sub, line: tok.line, col: tok.col)
|
||||
of tkStar:
|
||||
discard p.advance()
|
||||
Node(kind: nkStar, line: tok.line, col: tok.col)
|
||||
of tkPlaceholder:
|
||||
discard p.advance()
|
||||
Node(kind: nkPlaceholder, line: tok.line, col: tok.col)
|
||||
of tkNot:
|
||||
discard p.advance()
|
||||
let operand = p.parsePrimary()
|
||||
@@ -309,9 +315,15 @@ proc parseSelect(p: var Parser): Node =
|
||||
|
||||
# Parse SELECT list
|
||||
result.selResult = @[]
|
||||
result.selResult.add(p.parseExpr())
|
||||
var expr = p.parseExpr()
|
||||
if p.match(tkAs):
|
||||
expr.exprAlias = p.expect(tkIdent).value
|
||||
result.selResult.add(expr)
|
||||
while p.match(tkComma):
|
||||
result.selResult.add(p.parseExpr())
|
||||
expr = p.parseExpr()
|
||||
if p.match(tkAs):
|
||||
expr.exprAlias = p.expect(tkIdent).value
|
||||
result.selResult.add(expr)
|
||||
|
||||
# Parse FROM
|
||||
result.selJoins = @[]
|
||||
@@ -545,7 +557,7 @@ proc parseCreateTable(p: var Parser): Node =
|
||||
result.crtIfNotExists = true
|
||||
|
||||
discard p.expect(tkTable)
|
||||
if p.peek().kind == tkIdent and p.peek().value.toLower() == "if":
|
||||
if p.peek().kind == tkIf:
|
||||
discard p.advance() # if
|
||||
discard p.expect(tkNot)
|
||||
discard p.expect(tkExists)
|
||||
@@ -685,8 +697,22 @@ proc parseDropTable(p: var Parser): Node =
|
||||
proc parseAlterTable(p: var Parser): Node =
|
||||
let tok = p.expect(tkAlter)
|
||||
discard p.expect(tkTable)
|
||||
let tableName = p.expect(tkIdent).value
|
||||
# Check for ENABLE/DISABLE ROW LEVEL SECURITY
|
||||
if p.peek().kind == tkEnable:
|
||||
discard p.advance()
|
||||
discard p.expect(tkIdent) # ROW
|
||||
discard p.expect(tkIdent) # LEVEL
|
||||
discard p.expect(tkIdent) # SECURITY
|
||||
return Node(kind: nkEnableRLS, erlsTable: tableName, line: tok.line, col: tok.col)
|
||||
elif p.peek().kind == tkDisable:
|
||||
discard p.advance()
|
||||
discard p.expect(tkIdent) # ROW
|
||||
discard p.expect(tkIdent) # LEVEL
|
||||
discard p.expect(tkIdent) # SECURITY
|
||||
return Node(kind: nkDisableRLS, drlsTable: tableName, line: tok.line, col: tok.col)
|
||||
result = Node(kind: nkAlterTable, line: tok.line, col: tok.col)
|
||||
result.altName = p.expect(tkIdent).value
|
||||
result.altName = tableName
|
||||
result.altOps = @[]
|
||||
if p.match(tkAdd):
|
||||
discard p.match(tkColumn)
|
||||
@@ -764,13 +790,112 @@ proc parseDropView(p: var Parser): Node =
|
||||
result = Node(kind: nkDropView, dvName: name, dvIfExists: ifExists,
|
||||
line: tok.line, col: tok.col)
|
||||
|
||||
proc parseCreateTrigger(p: var Parser): Node =
|
||||
let tok = p.expect(tkCreate)
|
||||
discard p.expect(tkTrigger)
|
||||
let name = p.expect(tkIdent).value
|
||||
# Parse timing: BEFORE | AFTER | INSTEAD OF
|
||||
var timing = ""
|
||||
let timingTok = p.peek()
|
||||
if timingTok.kind == tkBefore:
|
||||
discard p.advance()
|
||||
timing = "before"
|
||||
elif timingTok.kind == tkAfter:
|
||||
discard p.advance()
|
||||
timing = "after"
|
||||
elif timingTok.kind == tkInstead:
|
||||
discard p.advance()
|
||||
discard p.expect(tkOf)
|
||||
timing = "instead of"
|
||||
else:
|
||||
raise newException(ValueError, "Expected BEFORE, AFTER, or INSTEAD OF in TRIGGER definition")
|
||||
# Parse event: INSERT | UPDATE | DELETE
|
||||
var event = ""
|
||||
let eventTok = p.peek()
|
||||
if eventTok.kind == tkInsert:
|
||||
discard p.advance()
|
||||
event = "INSERT"
|
||||
elif eventTok.kind == tkUpdate:
|
||||
discard p.advance()
|
||||
event = "UPDATE"
|
||||
elif eventTok.kind == tkDelete:
|
||||
discard p.advance()
|
||||
event = "DELETE"
|
||||
else:
|
||||
raise newException(ValueError, "Expected INSERT, UPDATE, or DELETE in TRIGGER definition")
|
||||
discard p.expect(tkOn)
|
||||
let tableName = p.expect(tkIdent).value
|
||||
discard p.expect(tkAs)
|
||||
# Parse action as raw string until end of statement
|
||||
var actionStr = ""
|
||||
while p.pos < p.tokens.len and p.tokens[p.pos].kind != tkSemicolon:
|
||||
actionStr.add(p.tokens[p.pos].value)
|
||||
actionStr.add(" ")
|
||||
discard p.advance()
|
||||
let actionNode = Node(kind: nkStringLit, strVal: actionStr.strip())
|
||||
result = Node(kind: nkCreateTrigger, trigName: name, trigTable: tableName,
|
||||
trigTiming: timing, trigEvent: event, trigAction: actionNode,
|
||||
line: tok.line, col: tok.col)
|
||||
|
||||
proc parseDropTrigger(p: var Parser): Node =
|
||||
let tok = p.expect(tkDrop)
|
||||
discard p.expect(tkTrigger)
|
||||
var ifExists = false
|
||||
if p.peek().kind == tkIf:
|
||||
discard p.advance()
|
||||
discard p.expect(tkExists)
|
||||
ifExists = true
|
||||
let name = p.expect(tkIdent).value
|
||||
result = Node(kind: nkDropTrigger, trigDropName: name, trigDropIfExists: ifExists,
|
||||
line: tok.line, col: tok.col)
|
||||
|
||||
proc parseCreateMigration(p: var Parser): Node =
|
||||
let tok = p.expect(tkCreate)
|
||||
discard p.expect(tkMigration)
|
||||
let name = p.expect(tkIdent).value
|
||||
discard p.expect(tkAs)
|
||||
let body = p.expect(tkStringLit).value
|
||||
result = Node(kind: nkCreateMigration, cmName: name, cmBody: body,
|
||||
var upBody = ""
|
||||
var downBody = ""
|
||||
if p.peek().kind == tkAs:
|
||||
# Legacy syntax: CREATE MIGRATION name AS 'body'
|
||||
discard p.advance()
|
||||
upBody = p.expect(tkStringLit).value
|
||||
elif p.peek().kind == tkLBrace:
|
||||
# New syntax: CREATE MIGRATION name { UP: ...; DOWN: ...; }
|
||||
discard p.advance() # {
|
||||
while p.peek().kind != tkRBrace and p.peek().kind != tkEof:
|
||||
var section = ""
|
||||
let sectionTok = p.peek()
|
||||
if sectionTok.kind == tkUp:
|
||||
discard p.advance()
|
||||
section = "up"
|
||||
elif sectionTok.kind == tkDown:
|
||||
discard p.advance()
|
||||
section = "down"
|
||||
elif sectionTok.kind == tkIdent:
|
||||
section = p.expect(tkIdent).value.toLower()
|
||||
else:
|
||||
raise newException(ValueError, "Expected UP or DOWN in migration body, got: " & $sectionTok.kind)
|
||||
discard p.expect(tkColon)
|
||||
var bodyStr = ""
|
||||
while p.peek().kind != tkRBrace and p.peek().kind != tkEof:
|
||||
# Check if next token starts a new section
|
||||
let nextTok = p.peek()
|
||||
if (nextTok.kind == tkUp or nextTok.kind == tkDown or
|
||||
(nextTok.kind == tkIdent and (nextTok.value.toLower() == "up" or nextTok.value.toLower() == "down"))) and
|
||||
p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind == tkColon:
|
||||
break
|
||||
bodyStr.add(p.tokens[p.pos].value)
|
||||
bodyStr.add(" ")
|
||||
discard p.advance()
|
||||
if section == "up":
|
||||
upBody = bodyStr.strip()
|
||||
elif section == "down":
|
||||
downBody = bodyStr.strip()
|
||||
else:
|
||||
raise newException(ValueError, "Expected UP or DOWN in migration body, got: " & section)
|
||||
discard p.expect(tkRBrace)
|
||||
result = Node(kind: nkCreateMigration, cmName: name, cmBody: upBody,
|
||||
cmDownBody: downBody, cmChecksum: "", cmAuthor: "", cmTimestamp: 0,
|
||||
line: tok.line, col: tok.col)
|
||||
|
||||
proc parseApplyMigration(p: var Parser): Node =
|
||||
@@ -779,6 +904,146 @@ proc parseApplyMigration(p: var Parser): Node =
|
||||
let name = p.expect(tkIdent).value
|
||||
result = Node(kind: nkApplyMigration, amName: name, line: tok.line, col: tok.col)
|
||||
|
||||
proc parseMigrationStatus(p: var Parser): Node =
|
||||
let tok = p.expect(tkMigration)
|
||||
discard p.expect(tkStatus)
|
||||
result = Node(kind: nkMigrationStatus, line: tok.line, col: tok.col)
|
||||
|
||||
proc parseMigrationUp(p: var Parser): Node =
|
||||
let tok = p.expect(tkMigration)
|
||||
discard p.expect(tkUp)
|
||||
var count = 0
|
||||
if p.peek().kind == tkIntLit:
|
||||
count = parseInt(p.expect(tkIntLit).value)
|
||||
result = Node(kind: nkMigrationUp, muCount: count, line: tok.line, col: tok.col)
|
||||
|
||||
proc parseMigrationDown(p: var Parser): Node =
|
||||
let tok = p.expect(tkMigration)
|
||||
discard p.expect(tkDown)
|
||||
var count = 1
|
||||
if p.peek().kind == tkIntLit:
|
||||
count = parseInt(p.expect(tkIntLit).value)
|
||||
result = Node(kind: nkMigrationDown, mdCount: count, line: tok.line, col: tok.col)
|
||||
|
||||
proc parseMigrationDryRun(p: var Parser): Node =
|
||||
let tok = p.expect(tkMigration)
|
||||
discard p.expect(tkDryRun)
|
||||
let name = p.expect(tkIdent).value
|
||||
result = Node(kind: nkMigrationDryRun, mdrName: name, line: tok.line, col: tok.col)
|
||||
|
||||
proc parseCreateUser(p: var Parser): Node =
|
||||
let tok = p.expect(tkCreate)
|
||||
discard p.expect(tkUser)
|
||||
let name = p.expect(tkIdent).value
|
||||
var password = ""
|
||||
var isSuper = false
|
||||
if p.peek().kind == tkWith:
|
||||
discard p.advance()
|
||||
if p.peek().kind == tkIdent and p.peek().value.toLower() == "password":
|
||||
discard p.advance()
|
||||
password = p.expect(tkStringLit).value
|
||||
while p.peek().kind == tkIdent:
|
||||
let opt = p.peek().value.toLower()
|
||||
if opt == "superuser":
|
||||
discard p.advance()
|
||||
isSuper = true
|
||||
elif opt == "nosuperuser":
|
||||
discard p.advance()
|
||||
isSuper = false
|
||||
else:
|
||||
break
|
||||
result = Node(kind: nkCreateUser, cuName: name, cuPassword: password,
|
||||
cuSuperuser: isSuper, line: tok.line, col: tok.col)
|
||||
|
||||
proc parseDropUser(p: var Parser): Node =
|
||||
let tok = p.expect(tkDrop)
|
||||
discard p.expect(tkUser)
|
||||
var ifExists = false
|
||||
if p.peek().kind == tkIf:
|
||||
discard p.advance()
|
||||
discard p.expect(tkExists)
|
||||
ifExists = true
|
||||
let name = p.expect(tkIdent).value
|
||||
result = Node(kind: nkDropUser, duName: name, duIfExists: ifExists,
|
||||
line: tok.line, col: tok.col)
|
||||
|
||||
proc parseCreatePolicy(p: var Parser): Node =
|
||||
let tok = p.expect(tkCreate)
|
||||
discard p.expect(tkPolicy)
|
||||
let name = p.expect(tkIdent).value
|
||||
discard p.expect(tkOn)
|
||||
let tableName = p.expect(tkIdent).value
|
||||
var cmd = "ALL"
|
||||
var usingNode: Node = nil
|
||||
var withCheckNode: Node = nil
|
||||
if p.peek().kind == tkFor:
|
||||
discard p.advance()
|
||||
let cmdTok = p.peek()
|
||||
if cmdTok.kind == tkIdent or cmdTok.kind == tkSelect or cmdTok.kind == tkInsert or
|
||||
cmdTok.kind == tkUpdate or cmdTok.kind == tkDelete:
|
||||
discard p.advance()
|
||||
cmd = cmdTok.value.toUpper()
|
||||
else:
|
||||
raise newException(ValueError, "Expected ALL, SELECT, INSERT, UPDATE, or DELETE in POLICY definition")
|
||||
if p.peek().kind == tkUsing:
|
||||
discard p.advance()
|
||||
usingNode = p.parseExpr()
|
||||
if p.peek().kind == tkWith:
|
||||
discard p.advance()
|
||||
discard p.expect(tkCheck)
|
||||
withCheckNode = p.parseExpr()
|
||||
result = Node(kind: nkCreatePolicy, cpName: name, cpTable: tableName,
|
||||
cpCommand: cmd, cpUsing: usingNode, cpWithCheck: withCheckNode,
|
||||
line: tok.line, col: tok.col)
|
||||
|
||||
proc parseDropPolicy(p: var Parser): Node =
|
||||
let tok = p.expect(tkDrop)
|
||||
discard p.expect(tkPolicy)
|
||||
var ifExists = false
|
||||
if p.peek().kind == tkIf:
|
||||
discard p.advance()
|
||||
discard p.expect(tkExists)
|
||||
ifExists = true
|
||||
let name = p.expect(tkIdent).value
|
||||
discard p.expect(tkOn)
|
||||
let tableName = p.expect(tkIdent).value
|
||||
result = Node(kind: nkDropPolicy, dpName: name, dpTable: tableName,
|
||||
dpIfExists: ifExists, line: tok.line, col: tok.col)
|
||||
|
||||
proc parseGrant(p: var Parser): Node =
|
||||
let tok = p.expect(tkGrant)
|
||||
var priv = ""
|
||||
let privTok = p.peek()
|
||||
if privTok.kind == tkIdent or privTok.kind == tkSelect or privTok.kind == tkInsert or
|
||||
privTok.kind == tkUpdate or privTok.kind == tkDelete:
|
||||
discard p.advance()
|
||||
priv = privTok.value.toUpper()
|
||||
else:
|
||||
raise newException(ValueError, "Expected privilege in GRANT")
|
||||
discard p.expect(tkOn)
|
||||
let tableName = p.expect(tkIdent).value
|
||||
discard p.expect(tkTo)
|
||||
let grantee = p.expect(tkIdent).value
|
||||
result = Node(kind: nkGrant, grPrivilege: priv, grTable: tableName,
|
||||
grGrantee: grantee, line: tok.line, col: tok.col)
|
||||
|
||||
proc parseRevoke(p: var Parser): Node =
|
||||
let tok = p.expect(tkRevoke)
|
||||
var priv = ""
|
||||
let privTok = p.peek()
|
||||
if privTok.kind == tkIdent or privTok.kind == tkSelect or privTok.kind == tkInsert or
|
||||
privTok.kind == tkUpdate or privTok.kind == tkDelete:
|
||||
discard p.advance()
|
||||
priv = privTok.value.toUpper()
|
||||
else:
|
||||
raise newException(ValueError, "Expected privilege in REVOKE")
|
||||
discard p.expect(tkOn)
|
||||
let tableName = p.expect(tkIdent).value
|
||||
discard p.expect(tkFrom)
|
||||
let grantee = p.expect(tkIdent).value
|
||||
result = Node(kind: nkRevoke, rvPrivilege: priv, rvTable: tableName,
|
||||
rvGrantee: grantee, line: tok.line, col: tok.col)
|
||||
|
||||
proc parseStatement*(p: var Parser): Node =
|
||||
case p.peek().kind
|
||||
of tkWith, tkSelect: p.parseSelect()
|
||||
@@ -796,8 +1061,14 @@ proc parseStatement*(p: var Parser): Node =
|
||||
elif next.kind == tkView or
|
||||
(next.kind == tkIdent and next.value.toLower() == "or"):
|
||||
p.parseCreateView()
|
||||
elif next.kind == tkTrigger:
|
||||
p.parseCreateTrigger()
|
||||
elif next.kind == tkMigration:
|
||||
p.parseCreateMigration()
|
||||
elif next.kind == tkUser:
|
||||
p.parseCreateUser()
|
||||
elif next.kind == tkPolicy:
|
||||
p.parseCreatePolicy()
|
||||
else:
|
||||
p.parseCreateType()
|
||||
else:
|
||||
@@ -809,6 +1080,12 @@ proc parseStatement*(p: var Parser): Node =
|
||||
p.parseDropTable()
|
||||
elif next.kind == tkView:
|
||||
p.parseDropView()
|
||||
elif next.kind == tkTrigger:
|
||||
p.parseDropTrigger()
|
||||
elif next.kind == tkUser:
|
||||
p.parseDropUser()
|
||||
elif next.kind == tkPolicy:
|
||||
p.parseDropPolicy()
|
||||
else:
|
||||
let tok = p.advance()
|
||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||
@@ -821,12 +1098,33 @@ proc parseStatement*(p: var Parser): Node =
|
||||
else:
|
||||
let tok = p.advance()
|
||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||
of tkGrant:
|
||||
p.parseGrant()
|
||||
of tkRevoke:
|
||||
p.parseRevoke()
|
||||
of tkApply:
|
||||
if p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind == tkMigration:
|
||||
p.parseApplyMigration()
|
||||
else:
|
||||
let tok = p.advance()
|
||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||
of tkMigration:
|
||||
if p.pos + 1 < p.tokens.len:
|
||||
let next = p.tokens[p.pos + 1]
|
||||
if next.kind == tkStatus:
|
||||
p.parseMigrationStatus()
|
||||
elif next.kind == tkUp:
|
||||
p.parseMigrationUp()
|
||||
elif next.kind == tkDown:
|
||||
p.parseMigrationDown()
|
||||
elif next.kind == tkDryRun:
|
||||
p.parseMigrationDryRun()
|
||||
else:
|
||||
let tok = p.advance()
|
||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||
else:
|
||||
let tok = p.advance()
|
||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||
of tkBegin: p.parseBeginTxn()
|
||||
of tkCommit: p.parseCommitTxn()
|
||||
of tkRollback: p.parseRollbackTxn()
|
||||
|
||||
@@ -7,7 +7,6 @@ import std/tables
|
||||
import std/monotimes
|
||||
import std/streams
|
||||
import std/locks
|
||||
import std/asyncdispatch
|
||||
import bloom
|
||||
import wal
|
||||
import mmap
|
||||
|
||||
+18
-1
@@ -3,9 +3,11 @@
|
||||
import std/asyncdispatch
|
||||
import std/threadpool
|
||||
import std/locks
|
||||
import std/os
|
||||
import barabadb/core/server
|
||||
import barabadb/core/httpserver
|
||||
import barabadb/core/config
|
||||
import barabadb/protocol/ssl
|
||||
import barabadb/storage/lsm
|
||||
import barabadb/storage/compaction
|
||||
|
||||
@@ -57,9 +59,24 @@ proc runTcpServer(config: BaraConfig) {.async.} =
|
||||
await server.run()
|
||||
|
||||
proc main() =
|
||||
let config = loadConfig()
|
||||
var config = loadConfig()
|
||||
echo "BaraDB v0.1.0 — Multimodal Database Engine"
|
||||
|
||||
if config.tlsEnabled:
|
||||
if config.certFile.len == 0 or config.keyFile.len == 0 or
|
||||
not fileExists(config.certFile) or not fileExists(config.keyFile):
|
||||
echo "TLS enabled but no certificate found. Generating self-signed certificate..."
|
||||
let (cert, key) = generateSelfSignedCert(config.dataDir / "certs")
|
||||
if cert.len > 0 and key.len > 0:
|
||||
config.certFile = cert
|
||||
config.keyFile = key
|
||||
echo "Generated self-signed certificate:"
|
||||
echo " Cert: ", cert
|
||||
echo " Key: ", key
|
||||
else:
|
||||
echo "WARNING: Failed to generate self-signed certificate. TLS disabled."
|
||||
config.tlsEnabled = false
|
||||
|
||||
# Start HTTP server (blocking, multi-threaded via hunos) in background thread
|
||||
var httpServer = newHttpServer(config)
|
||||
spawn httpServer.run(config.port + 440) # HTTP port = TCP port + 440
|
||||
|
||||
Reference in New Issue
Block a user