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:
@@ -20,6 +20,10 @@ type
|
||||
nkCommitTxn
|
||||
nkRollbackTxn
|
||||
nkExplainStmt
|
||||
nkCreateView
|
||||
nkDropView
|
||||
nkCreateMigration
|
||||
nkApplyMigration
|
||||
|
||||
# Clauses
|
||||
nkFrom
|
||||
@@ -200,6 +204,18 @@ type
|
||||
of nkExplainStmt:
|
||||
expStmt*: Node
|
||||
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:
|
||||
ciTarget*: string
|
||||
ciName*: string
|
||||
|
||||
@@ -32,6 +32,7 @@ type
|
||||
db*: LSMTree
|
||||
tables*: Table[string, TableDef]
|
||||
btrees*: Table[string, BTreeIndex[string, IndexEntry]]
|
||||
views*: Table[string, Node] # view name -> SELECT AST
|
||||
txnManager*: TxnManager
|
||||
pendingTxn*: Transaction
|
||||
onChange*: proc(ev: ChangeEvent) {.closure.}
|
||||
@@ -86,6 +87,7 @@ proc restoreSchema(ctx: ExecutionContext)
|
||||
proc newExecutionContext*(db: LSMTree): ExecutionContext =
|
||||
result = ExecutionContext(db: db, tables: initTable[string, TableDef](),
|
||||
btrees: initTable[string, BTreeIndex[string, IndexEntry]](),
|
||||
views: initTable[string, Node](),
|
||||
onChange: nil)
|
||||
restoreSchema(result)
|
||||
|
||||
@@ -125,7 +127,8 @@ proc restoreSchema(ctx: ExecutionContext) =
|
||||
|
||||
proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
|
||||
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)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
@@ -695,6 +698,48 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node): ExecResult =
|
||||
let stmt = astNode.stmts[0]
|
||||
case stmt.kind
|
||||
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
|
||||
if stmt.selFrom != nil and stmt.selFrom.fromTable.len > 0:
|
||||
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 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:
|
||||
let idxName = if stmt.ciName.len > 0: stmt.ciName
|
||||
else: stmt.ciTarget & "." & stmt.ciTarget
|
||||
|
||||
@@ -90,6 +90,9 @@ type
|
||||
tkCommit
|
||||
tkRollback
|
||||
tkExplain
|
||||
tkView
|
||||
tkMigration
|
||||
tkApply
|
||||
tkCount
|
||||
tkSum
|
||||
tkAvg
|
||||
@@ -233,6 +236,9 @@ const keywords*: Table[string, TokenKind] = {
|
||||
"commit": tkCommit,
|
||||
"rollback": tkRollback,
|
||||
"explain": tkExplain,
|
||||
"view": tkView,
|
||||
"migration": tkMigration,
|
||||
"apply": tkApply,
|
||||
"count": tkCount,
|
||||
"sum": tkSum,
|
||||
"avg": tkAvg,
|
||||
|
||||
@@ -738,6 +738,47 @@ proc parseExplain(p: var Parser): Node =
|
||||
result.expAnalyze = true
|
||||
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 =
|
||||
case p.peek().kind
|
||||
of tkWith, tkSelect: p.parseSelect()
|
||||
@@ -752,13 +793,25 @@ proc parseStatement*(p: var Parser): Node =
|
||||
p.parseCreateTable()
|
||||
elif next.kind == tkIndex or next.kind == tkUnique:
|
||||
p.parseCreateIndex()
|
||||
elif next.kind == tkView or
|
||||
(next.kind == tkIdent and next.value.toLower() == "or"):
|
||||
p.parseCreateView()
|
||||
elif next.kind == tkMigration:
|
||||
p.parseCreateMigration()
|
||||
else:
|
||||
p.parseCreateType()
|
||||
else:
|
||||
p.parseCreateType()
|
||||
of tkDrop:
|
||||
if p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind == tkTable:
|
||||
p.parseDropTable()
|
||||
if p.pos + 1 < p.tokens.len:
|
||||
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:
|
||||
let tok = p.advance()
|
||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||
@@ -768,6 +821,12 @@ proc parseStatement*(p: var Parser): Node =
|
||||
else:
|
||||
let tok = p.advance()
|
||||
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 tkCommit: p.parseCommitTxn()
|
||||
of tkRollback: p.parseRollbackTxn()
|
||||
|
||||
Reference in New Issue
Block a user