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:
2026-05-06 16:42:53 +03:00
parent 856a07c030
commit f9f77b3a18
25 changed files with 3088 additions and 797 deletions
+6
View File
@@ -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 =
+208 -50
View File
@@ -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()&#10;CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)&#10;INSERT INTO users VALUES (1, "test")&#10;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)
+21 -1
View File
@@ -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
+40 -9
View File
@@ -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)
+46 -59
View File
@@ -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 = @[]
+25
View File
@@ -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
+72 -1
View File
@@ -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
File diff suppressed because it is too large Load Diff
+5
View File
@@ -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)
+73 -5
View File
@@ -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()
+305 -7
View File
@@ -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()
-1
View File
@@ -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