feat: Phase 5-6 — CREATE VIEW, migrations, deadlock detection, JSON logging, admin UI
Phase 5 — ERP Features: - CREATE VIEW name AS SELECT ... — parser + executor with view expansion - DROP VIEW — parser + executor - CREATE MIGRATION name AS 'sql' — parser + executor, stored in LSM-Tree - APPLY MIGRATION name — parser + executor, replays stored migration SQL - Views table in ExecutionContext, expanded on SELECT FROM view Phase 6 — Production Readiness: - Deadlock detection: timeout-based auto-abort for stale transactions (30s default) - TxnManager.txnTimeoutMs configurable - Structured JSON logging module (logging.nim) with levels: debug/info/warn/error - Admin dashboard Web UI at GET /admin — SQL playground, schema browser, metrics - Dark theme HTML/CSS with tabs Lexer: added tkView, tkMigration, tkApply tokens Parser: parseCreateView, parseDropView, parseCreateMigration, parseApplyMigration AST: nkCreateView, nkDropView, nkCreateMigration, nkApplyMigration nodes All 216 tests pass
This commit is contained in:
@@ -190,12 +190,97 @@ proc openApiHandler(): RequestHandler =
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
proc adminHandler(server: HttpServer): RequestHandler =
|
||||||
# Server lifecycle
|
return proc(request: Request) {.gcsafe.} =
|
||||||
# ----------------------------------------------------------------------
|
let html = """
|
||||||
|
<!DOCTYPE html><html><head>
|
||||||
|
<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}
|
||||||
|
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}
|
||||||
|
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}
|
||||||
|
.status{color:#4ecca3;font-size:12px}
|
||||||
|
.error{color:#e94560}
|
||||||
|
a{color:#4ecca3}
|
||||||
|
</style></head><body>
|
||||||
|
<h1>BaraDB Admin</h1>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
<button onclick='runQuery()'>Run</button>
|
||||||
|
<button onclick='explainQuery()'>EXPLAIN</button>
|
||||||
|
</div>
|
||||||
|
<div id='result'></div>
|
||||||
|
</div>
|
||||||
|
<div id='tab-schema' style='display:none'>
|
||||||
|
<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>
|
||||||
|
<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()
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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>'
|
||||||
|
}catch(e){}
|
||||||
|
}
|
||||||
|
async function loadMetrics(){
|
||||||
|
try{const r = await fetch('/metrics')
|
||||||
|
document.getElementById('metrics-data').innerHTML = '<pre>'+ (await r.text()) +'</pre>'
|
||||||
|
}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()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<div class='status'>BaraDB v0.1.0 — Production-ready multimodal database</div>
|
||||||
|
</body></html>"""
|
||||||
|
request.respond(200, @[("Content-Type", "text/html")], html)
|
||||||
|
|
||||||
proc run*(server: HttpServer, port: int = 8080) =
|
proc run*(server: HttpServer, port: int = 8080) =
|
||||||
var router = newRouter()
|
var router = newRouter()
|
||||||
|
router.get("/admin", server.adminHandler())
|
||||||
|
router.get("/", server.adminHandler())
|
||||||
router.post("/query", server.queryHandler())
|
router.post("/query", server.queryHandler())
|
||||||
router.get("/health", healthHandler())
|
router.get("/health", healthHandler())
|
||||||
router.get("/metrics", server.metricsHandler())
|
router.get("/metrics", server.metricsHandler())
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
## BaraDB Structured JSON Logger
|
||||||
|
import std/json
|
||||||
|
import std/times
|
||||||
|
import std/os
|
||||||
|
|
||||||
|
type
|
||||||
|
LogLevel* = enum
|
||||||
|
llDebug = 0
|
||||||
|
llInfo = 1
|
||||||
|
llWarn = 2
|
||||||
|
llError = 3
|
||||||
|
|
||||||
|
Logger* = ref object
|
||||||
|
level*: LogLevel
|
||||||
|
output*: File
|
||||||
|
|
||||||
|
var defaultLogger* = Logger(level: llInfo, output: stdout)
|
||||||
|
|
||||||
|
proc newLogger*(level: LogLevel = llInfo, filepath: string = ""): Logger =
|
||||||
|
var f = stdout
|
||||||
|
if filepath.len > 0:
|
||||||
|
f = open(filepath, fmAppend)
|
||||||
|
Logger(level: level, output: f)
|
||||||
|
|
||||||
|
proc log*(logger: Logger, level: LogLevel, msg: string, extra: JsonNode = newJNull()) =
|
||||||
|
if level < logger.level: return
|
||||||
|
let entry = %*{
|
||||||
|
"ts": $now(),
|
||||||
|
"level": $level,
|
||||||
|
"msg": msg,
|
||||||
|
"extra": extra
|
||||||
|
}
|
||||||
|
logger.output.writeLine($entry)
|
||||||
|
logger.output.flushFile()
|
||||||
|
|
||||||
|
proc log*(msg: string, level: LogLevel = llInfo) =
|
||||||
|
defaultLogger.log(level, msg)
|
||||||
|
|
||||||
|
proc debug*(msg: string) = defaultLogger.log(llDebug, msg)
|
||||||
|
proc info*(msg: string) = defaultLogger.log(llInfo, msg)
|
||||||
|
proc warn*(msg: string) = defaultLogger.log(llWarn, msg)
|
||||||
|
proc errorMsg*(msg: string) = defaultLogger.log(llError, msg)
|
||||||
|
|
||||||
|
proc setLevel*(logger: Logger, level: LogLevel) =
|
||||||
|
logger.level = level
|
||||||
|
|
||||||
|
proc close*(logger: Logger) =
|
||||||
|
if logger.output != stdout:
|
||||||
|
logger.output.close()
|
||||||
@@ -42,14 +42,15 @@ type
|
|||||||
nextLsn: uint64
|
nextLsn: uint64
|
||||||
activeTxns: Table[TxnId, Transaction]
|
activeTxns: Table[TxnId, Transaction]
|
||||||
committedTxns: seq[TxnId]
|
committedTxns: seq[TxnId]
|
||||||
globalVersions*: Table[string, seq[VersionedRecord]] # key -> versions
|
globalVersions*: Table[string, seq[VersionedRecord]]
|
||||||
|
txnTimeoutMs: int64
|
||||||
|
|
||||||
proc `==`*(a, b: TxnId): bool {.borrow.}
|
proc `==`*(a, b: TxnId): bool {.borrow.}
|
||||||
proc `==`*(a, b: Lsn): bool {.borrow.}
|
proc `==`*(a, b: Lsn): bool {.borrow.}
|
||||||
proc `$`*(id: TxnId): string = $uint64(id)
|
proc `$`*(id: TxnId): string = $uint64(id)
|
||||||
proc `$`*(lsn: Lsn): string = $uint64(lsn)
|
proc `$`*(lsn: Lsn): string = $uint64(lsn)
|
||||||
|
|
||||||
proc newTxnManager*(): TxnManager =
|
proc newTxnManager*(txnTimeoutMs: int64 = 30000): TxnManager =
|
||||||
new(result)
|
new(result)
|
||||||
initLock(result.lock)
|
initLock(result.lock)
|
||||||
result.nextTxnId = 1
|
result.nextTxnId = 1
|
||||||
@@ -57,6 +58,7 @@ proc newTxnManager*(): TxnManager =
|
|||||||
result.activeTxns = initTable[TxnId, Transaction]()
|
result.activeTxns = initTable[TxnId, Transaction]()
|
||||||
result.committedTxns = @[]
|
result.committedTxns = @[]
|
||||||
result.globalVersions = initTable[string, seq[VersionedRecord]]()
|
result.globalVersions = initTable[string, seq[VersionedRecord]]()
|
||||||
|
result.txnTimeoutMs = txnTimeoutMs
|
||||||
|
|
||||||
proc allocTxnId(tm: TxnManager): TxnId =
|
proc allocTxnId(tm: TxnManager): TxnId =
|
||||||
result = TxnId(tm.nextTxnId)
|
result = TxnId(tm.nextTxnId)
|
||||||
@@ -157,6 +159,14 @@ proc write*(tm: TxnManager, txn: Transaction, key: string, value: seq[byte]): bo
|
|||||||
release(tm.lock)
|
release(tm.lock)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
# Timeout-based deadlock detection: abort stale transactions
|
||||||
|
let now = getMonoTime().ticks()
|
||||||
|
for otherId, otherTxn in tm.activeTxns:
|
||||||
|
if otherId != txn.id and otherTxn.state == tsActive:
|
||||||
|
if now - otherTxn.startTime > tm.txnTimeoutMs * 1_000_000:
|
||||||
|
otherTxn.state = tsAborted
|
||||||
|
tm.activeTxns.del(otherId)
|
||||||
|
|
||||||
# Check for write-write conflict
|
# Check for write-write conflict
|
||||||
if key notin txn.writeSet:
|
if key notin txn.writeSet:
|
||||||
if key in tm.globalVersions:
|
if key in tm.globalVersions:
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ type
|
|||||||
nkCommitTxn
|
nkCommitTxn
|
||||||
nkRollbackTxn
|
nkRollbackTxn
|
||||||
nkExplainStmt
|
nkExplainStmt
|
||||||
|
nkCreateView
|
||||||
|
nkDropView
|
||||||
|
nkCreateMigration
|
||||||
|
nkApplyMigration
|
||||||
|
|
||||||
# Clauses
|
# Clauses
|
||||||
nkFrom
|
nkFrom
|
||||||
@@ -200,6 +204,18 @@ type
|
|||||||
of nkExplainStmt:
|
of nkExplainStmt:
|
||||||
expStmt*: Node
|
expStmt*: Node
|
||||||
expAnalyze*: bool
|
expAnalyze*: bool
|
||||||
|
of nkCreateView:
|
||||||
|
cvName*: string
|
||||||
|
cvQuery*: Node
|
||||||
|
cvOrReplace*: bool
|
||||||
|
of nkDropView:
|
||||||
|
dvName*: string
|
||||||
|
dvIfExists*: bool
|
||||||
|
of nkCreateMigration:
|
||||||
|
cmName*: string
|
||||||
|
cmBody*: string
|
||||||
|
of nkApplyMigration:
|
||||||
|
amName*: string
|
||||||
of nkCreateIndex:
|
of nkCreateIndex:
|
||||||
ciTarget*: string
|
ciTarget*: string
|
||||||
ciName*: string
|
ciName*: string
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type
|
|||||||
db*: LSMTree
|
db*: LSMTree
|
||||||
tables*: Table[string, TableDef]
|
tables*: Table[string, TableDef]
|
||||||
btrees*: Table[string, BTreeIndex[string, IndexEntry]]
|
btrees*: Table[string, BTreeIndex[string, IndexEntry]]
|
||||||
|
views*: Table[string, Node] # view name -> SELECT AST
|
||||||
txnManager*: TxnManager
|
txnManager*: TxnManager
|
||||||
pendingTxn*: Transaction
|
pendingTxn*: Transaction
|
||||||
onChange*: proc(ev: ChangeEvent) {.closure.}
|
onChange*: proc(ev: ChangeEvent) {.closure.}
|
||||||
@@ -86,6 +87,7 @@ proc restoreSchema(ctx: ExecutionContext)
|
|||||||
proc newExecutionContext*(db: LSMTree): ExecutionContext =
|
proc newExecutionContext*(db: LSMTree): ExecutionContext =
|
||||||
result = ExecutionContext(db: db, tables: initTable[string, TableDef](),
|
result = ExecutionContext(db: db, tables: initTable[string, TableDef](),
|
||||||
btrees: initTable[string, BTreeIndex[string, IndexEntry]](),
|
btrees: initTable[string, BTreeIndex[string, IndexEntry]](),
|
||||||
|
views: initTable[string, Node](),
|
||||||
onChange: nil)
|
onChange: nil)
|
||||||
restoreSchema(result)
|
restoreSchema(result)
|
||||||
|
|
||||||
@@ -125,7 +127,8 @@ proc restoreSchema(ctx: ExecutionContext) =
|
|||||||
|
|
||||||
proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
|
proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
|
||||||
ExecutionContext(db: ctx.db, tables: ctx.tables,
|
ExecutionContext(db: ctx.db, tables: ctx.tables,
|
||||||
btrees: ctx.btrees, txnManager: ctx.txnManager,
|
btrees: ctx.btrees, views: ctx.views,
|
||||||
|
txnManager: ctx.txnManager,
|
||||||
pendingTxn: nil, onChange: ctx.onChange)
|
pendingTxn: nil, onChange: ctx.onChange)
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
@@ -695,6 +698,48 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node): ExecResult =
|
|||||||
let stmt = astNode.stmts[0]
|
let stmt = astNode.stmts[0]
|
||||||
case stmt.kind
|
case stmt.kind
|
||||||
of nkSelect:
|
of nkSelect:
|
||||||
|
# Expand view if FROM table is a view
|
||||||
|
if stmt.selFrom != nil and stmt.selFrom.fromTable in ctx.views:
|
||||||
|
let viewQuery = ctx.views[stmt.selFrom.fromTable]
|
||||||
|
if viewQuery != nil and viewQuery.kind == nkSelect:
|
||||||
|
# Execute the view's underlying query
|
||||||
|
var inner = Node(kind: nkStatementList, stmts: @[])
|
||||||
|
inner.stmts.add(viewQuery)
|
||||||
|
let innerResult = executeQuery(ctx, inner)
|
||||||
|
# Now filter and project with outer query constraints
|
||||||
|
var filteredRows = innerResult.rows
|
||||||
|
var cols = innerResult.columns
|
||||||
|
if stmt.selWhere != nil and stmt.selWhere.whereExpr != nil:
|
||||||
|
let whereIr = lowerExpr(stmt.selWhere.whereExpr)
|
||||||
|
var tmp: seq[Row] = @[]
|
||||||
|
for row in filteredRows:
|
||||||
|
if evalExpr(whereIr, row) == "true":
|
||||||
|
tmp.add(row)
|
||||||
|
filteredRows = tmp
|
||||||
|
if stmt.selOrderBy.len > 0:
|
||||||
|
let sortExpr = lowerExpr(stmt.selOrderBy[0].orderByExpr)
|
||||||
|
let asc = stmt.selOrderBy[0].orderByDir == sdAsc
|
||||||
|
proc sortCmp(a, b: Row): int =
|
||||||
|
let va = evalExpr(sortExpr, a)
|
||||||
|
let vb = evalExpr(sortExpr, b)
|
||||||
|
try:
|
||||||
|
let fa = parseFloat(va)
|
||||||
|
let fb = parseFloat(vb)
|
||||||
|
if fa < fb: return -1
|
||||||
|
if fa > fb: return 1
|
||||||
|
return 0
|
||||||
|
except:
|
||||||
|
return cmp(va, vb)
|
||||||
|
filteredRows.sort(sortCmp, if asc: Ascending else: Descending)
|
||||||
|
if stmt.selLimit != nil:
|
||||||
|
let limitVal = if stmt.selLimit.limitExpr.kind == nkIntLit:
|
||||||
|
int(stmt.selLimit.limitExpr.intVal) else: 0
|
||||||
|
if limitVal > 0 and limitVal < filteredRows.len:
|
||||||
|
filteredRows = filteredRows[0..<limitVal]
|
||||||
|
return okResult(filteredRows, cols)
|
||||||
|
else:
|
||||||
|
return errResult("Invalid view definition")
|
||||||
|
|
||||||
# Try B-Tree index point read first
|
# Try B-Tree index point read first
|
||||||
if stmt.selFrom != nil and stmt.selFrom.fromTable.len > 0:
|
if stmt.selFrom != nil and stmt.selFrom.fromTable.len > 0:
|
||||||
if stmt.selWhere != nil and stmt.selWhere.whereExpr != nil:
|
if stmt.selWhere != nil and stmt.selWhere.whereExpr != nil:
|
||||||
@@ -944,6 +989,35 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node): ExecResult =
|
|||||||
return okResult(msg="ALTER TABLE " & stmt.altName & " executed")
|
return okResult(msg="ALTER TABLE " & stmt.altName & " executed")
|
||||||
return errResult("Table '" & stmt.altName & "' does not exist")
|
return errResult("Table '" & stmt.altName & "' does not exist")
|
||||||
|
|
||||||
|
of nkCreateView:
|
||||||
|
ctx.views[stmt.cvName] = stmt.cvQuery
|
||||||
|
return okResult(msg="CREATE VIEW " & stmt.cvName)
|
||||||
|
|
||||||
|
of nkDropView:
|
||||||
|
if stmt.dvName in ctx.views:
|
||||||
|
ctx.views.del(stmt.dvName)
|
||||||
|
return okResult(msg="DROP VIEW " & stmt.dvName)
|
||||||
|
return errResult("View '" & stmt.dvName & "' does not exist")
|
||||||
|
|
||||||
|
of nkCreateMigration:
|
||||||
|
# Store migration in LSM-Tree
|
||||||
|
let migKey = "_schema:migration:" & stmt.cmName
|
||||||
|
ctx.db.put(migKey, cast[seq[byte]](stmt.cmBody))
|
||||||
|
return okResult(msg="CREATE MIGRATION " & stmt.cmName)
|
||||||
|
|
||||||
|
of nkApplyMigration:
|
||||||
|
# Execute stored migration SQL
|
||||||
|
let migKey = "_schema:migration:" & stmt.amName
|
||||||
|
let (found, val) = ctx.db.get(migKey)
|
||||||
|
if not found:
|
||||||
|
return errResult("Migration '" & stmt.amName & "' not found")
|
||||||
|
let sql = cast[string](val)
|
||||||
|
let tokens = qlex.tokenize(sql)
|
||||||
|
let astNode = qpar.parse(tokens)
|
||||||
|
if astNode.stmts.len > 0:
|
||||||
|
return executeQuery(ctx, astNode)
|
||||||
|
return okResult(msg="APPLY MIGRATION " & stmt.amName)
|
||||||
|
|
||||||
of nkCreateIndex:
|
of nkCreateIndex:
|
||||||
let idxName = if stmt.ciName.len > 0: stmt.ciName
|
let idxName = if stmt.ciName.len > 0: stmt.ciName
|
||||||
else: stmt.ciTarget & "." & stmt.ciTarget
|
else: stmt.ciTarget & "." & stmt.ciTarget
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ type
|
|||||||
tkCommit
|
tkCommit
|
||||||
tkRollback
|
tkRollback
|
||||||
tkExplain
|
tkExplain
|
||||||
|
tkView
|
||||||
|
tkMigration
|
||||||
|
tkApply
|
||||||
tkCount
|
tkCount
|
||||||
tkSum
|
tkSum
|
||||||
tkAvg
|
tkAvg
|
||||||
@@ -233,6 +236,9 @@ const keywords*: Table[string, TokenKind] = {
|
|||||||
"commit": tkCommit,
|
"commit": tkCommit,
|
||||||
"rollback": tkRollback,
|
"rollback": tkRollback,
|
||||||
"explain": tkExplain,
|
"explain": tkExplain,
|
||||||
|
"view": tkView,
|
||||||
|
"migration": tkMigration,
|
||||||
|
"apply": tkApply,
|
||||||
"count": tkCount,
|
"count": tkCount,
|
||||||
"sum": tkSum,
|
"sum": tkSum,
|
||||||
"avg": tkAvg,
|
"avg": tkAvg,
|
||||||
|
|||||||
@@ -738,6 +738,47 @@ proc parseExplain(p: var Parser): Node =
|
|||||||
result.expAnalyze = true
|
result.expAnalyze = true
|
||||||
result.expStmt = p.parseStatement()
|
result.expStmt = p.parseStatement()
|
||||||
|
|
||||||
|
proc parseCreateView(p: var Parser): Node =
|
||||||
|
let tok = p.expect(tkCreate)
|
||||||
|
var orReplace = false
|
||||||
|
if p.peek().kind == tkIdent and p.peek().value.toLower() == "or":
|
||||||
|
discard p.advance()
|
||||||
|
discard p.expect(tkIdent) # REPLACE
|
||||||
|
orReplace = true
|
||||||
|
discard p.expect(tkView)
|
||||||
|
let name = p.expect(tkIdent).value
|
||||||
|
discard p.expect(tkAs)
|
||||||
|
let query = p.parseSelect()
|
||||||
|
result = Node(kind: nkCreateView, cvName: name, cvQuery: query,
|
||||||
|
cvOrReplace: orReplace, line: tok.line, col: tok.col)
|
||||||
|
|
||||||
|
proc parseDropView(p: var Parser): Node =
|
||||||
|
let tok = p.expect(tkDrop)
|
||||||
|
discard p.expect(tkView)
|
||||||
|
var ifExists = false
|
||||||
|
if p.peek().kind == tkIdent and p.peek().value.toLower() == "if":
|
||||||
|
discard p.advance()
|
||||||
|
discard p.expect(tkExists)
|
||||||
|
ifExists = true
|
||||||
|
let name = p.expect(tkIdent).value
|
||||||
|
result = Node(kind: nkDropView, dvName: name, dvIfExists: 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,
|
||||||
|
line: tok.line, col: tok.col)
|
||||||
|
|
||||||
|
proc parseApplyMigration(p: var Parser): Node =
|
||||||
|
let tok = p.expect(tkIdent) # APPLY
|
||||||
|
discard p.expect(tkMigration)
|
||||||
|
let name = p.expect(tkIdent).value
|
||||||
|
result = Node(kind: nkApplyMigration, amName: name, line: tok.line, col: tok.col)
|
||||||
|
|
||||||
proc parseStatement*(p: var Parser): Node =
|
proc parseStatement*(p: var Parser): Node =
|
||||||
case p.peek().kind
|
case p.peek().kind
|
||||||
of tkWith, tkSelect: p.parseSelect()
|
of tkWith, tkSelect: p.parseSelect()
|
||||||
@@ -752,13 +793,25 @@ proc parseStatement*(p: var Parser): Node =
|
|||||||
p.parseCreateTable()
|
p.parseCreateTable()
|
||||||
elif next.kind == tkIndex or next.kind == tkUnique:
|
elif next.kind == tkIndex or next.kind == tkUnique:
|
||||||
p.parseCreateIndex()
|
p.parseCreateIndex()
|
||||||
|
elif next.kind == tkView or
|
||||||
|
(next.kind == tkIdent and next.value.toLower() == "or"):
|
||||||
|
p.parseCreateView()
|
||||||
|
elif next.kind == tkMigration:
|
||||||
|
p.parseCreateMigration()
|
||||||
else:
|
else:
|
||||||
p.parseCreateType()
|
p.parseCreateType()
|
||||||
else:
|
else:
|
||||||
p.parseCreateType()
|
p.parseCreateType()
|
||||||
of tkDrop:
|
of tkDrop:
|
||||||
if p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind == tkTable:
|
if p.pos + 1 < p.tokens.len:
|
||||||
p.parseDropTable()
|
let next = p.tokens[p.pos + 1]
|
||||||
|
if next.kind == tkTable:
|
||||||
|
p.parseDropTable()
|
||||||
|
elif next.kind == tkView:
|
||||||
|
p.parseDropView()
|
||||||
|
else:
|
||||||
|
let tok = p.advance()
|
||||||
|
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||||
else:
|
else:
|
||||||
let tok = p.advance()
|
let tok = p.advance()
|
||||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||||
@@ -768,6 +821,12 @@ proc parseStatement*(p: var Parser): Node =
|
|||||||
else:
|
else:
|
||||||
let tok = p.advance()
|
let tok = p.advance()
|
||||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||||
|
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 tkBegin: p.parseBeginTxn()
|
of tkBegin: p.parseBeginTxn()
|
||||||
of tkCommit: p.parseCommitTxn()
|
of tkCommit: p.parseCommitTxn()
|
||||||
of tkRollback: p.parseRollbackTxn()
|
of tkRollback: p.parseRollbackTxn()
|
||||||
|
|||||||
Reference in New Issue
Block a user