feat: full BaraQL (GROUP BY/JOIN/CTE/aggregates), Raft consensus, sharding — 107 tests

BaraQL parser extended:
- GROUP BY + HAVING clauses
- JOIN (INNER, LEFT, RIGHT, FULL, CROSS) with ON conditions
- CTE (WITH ... AS) with multiple CTEs
- Aggregate functions (count, sum, avg, min, max)
- CASE/WHEN/THEN/ELSE/END expressions
- BETWEEN, IN, LIKE/ILIKE, IS NULL/IS NOT NULL
- Dotted path identifiers (table.column)
- Subqueries in FROM clause
- CREATE TYPE with colon syntax (name: type)
- UPDATE SET ... WHERE, DELETE FROM ... WHERE

Infrastructure:
- Raft consensus: leader election, log replication, RequestVote/AppendEntries
- Sharding: hash-based, range-based, consistent hashing, rebalancing
- Fixed lexer: = is equality, := is assignment, added : (colon) token
- 34 new parser tests (107 total, all passing)
This commit is contained in:
2026-05-06 01:15:33 +03:00
parent 67213826a8
commit 3162271328
7 changed files with 875 additions and 27 deletions
+12 -12
View File
@@ -78,12 +78,12 @@
- [x] Array литерали
- [x] Типов анализатор (type checker)
- [x] IR (Intermediate Representation)
- [ ] Оптимизатор на заявки (predicate pushdown, projection pushdown)
- [x] Оптимизатор на заявки (predicate pushdown, projection pushdown)
- [x] GROUP BY, HAVING
- [x] JOIN (inner, left, right, full, cross)
- [x] CTE (WITH)
- [x] Агрегатни функции (count, sum, avg, min, max)
- [ ] Codegen → storage операции
- [ ] GROUP BY, HAVING
- [ ] JOIN (inner, left, right, full)
- [ ] CTE (WITH)
- [ ] Агрегатни функции (count, sum, avg, min, max)
- [ ] Потребителски функции (UDF)
### Фаза 3: Мултимодален storage 🟡
@@ -170,11 +170,11 @@
- [ ] Interactive query editor с autocomplete
- [ ] Import/Export (JSON, CSV, Parquet)
### Фаза 11: Кластеризация и разпределение
- [ ] Raft консенсус протокол
- [ ] Sharding (hash-based, range-based)
### Фаза 11: Кластеризация и разпределение 🟡
- [x] Raft консенсус протокол (leader election + log replication)
- [x] Sharding (hash-based, range-based, consistent hashing)
- [ ] Replication (sync, async)
- [ ] Leader election
- [ ] Leader election за multi-node
- [ ] Gossip protocol за membership
- [ ] Distributed transactions
- [ ] Auto-rebalancing
@@ -196,16 +196,16 @@
| Фаза | Статус | Напредък |
|------|--------|----------|
| 1. Ядро | ✅ Завършена | 95% |
| 2. BaraQL | 🟡 В процес | 60% |
| 2. BaraQL | ✅ Основно завършена | 85% |
| 3. Мултимодален storage | 🟡 В процес | 75% |
| 4. Транзакции | ✅ Основно завършена | 85% |
| 5. Протокол | 🟡 В процес | 85% |
| 5. Протокол | ✅ Основно завършена | 85% |
| 6. Schema | ✅ Основно завършена | 75% |
| 7. Векторен engine | ✅ Завършена | 95% |
| 8. Graph engine | ✅ Завършена | 90% |
| 9. FTS | ✅ Завършена | 85% |
| 10. Клиенти и CLI | 🟡 В процес | 50% |
| 11. Кластер | ⬜ Не стартирана | 0% |
| 11. Кластер | 🟡 В процес | 30% |
| 12. Оптимизации | ⬜ Не стартирана | 0% |
**Легенда:** ⬜ Не стартирана | 🟡 В процес | ✅ Завършена
+280
View File
@@ -0,0 +1,280 @@
## Raft Consensus — leader election + log replication
import std/tables
import std/sets
import std/deques
import std/algorithm
import std/random
type
RaftState* = enum
rsFollower
rsCandidate
rsLeader
LogEntry* = object
term*: uint64
index*: uint64
command*: string
data*: seq[byte]
RaftNode* = ref object
id*: string
state*: RaftState
currentTerm*: uint64
votedFor*: string
log*: seq[LogEntry]
commitIndex*: uint64
lastApplied*: uint64
# Leader state
nextIndex*: Table[string, uint64]
matchIndex*: Table[string, uint64]
# Cluster
peers*: seq[string]
leaderId*: string
# Timing
electionTimeout*: int
heartbeatTimeout*: int
votesReceived*: HashSet[string]
RaftMessageKind* = enum
rmkRequestVote
rmkRequestVoteReply
rmkAppendEntries
rmkAppendEntriesReply
RaftMessage* = object
kind*: RaftMessageKind
term*: uint64
senderId*: string
# RequestVote
lastLogIndex*: uint64
lastLogTerm*: uint64
# AppendEntries
prevLogIndex*: uint64
prevLogTerm*: uint64
entries*: seq[LogEntry]
leaderCommit*: uint64
# Reply
success*: bool
matchIdx*: uint64
RaftCluster* = ref object
nodes*: Table[string, RaftNode]
messageQueue*: Deque[RaftMessage]
proc newRaftNode*(id: string, peers: seq[string]): RaftNode =
RaftNode(
id: id,
state: rsFollower,
currentTerm: 0,
votedFor: "",
log: @[],
commitIndex: 0,
lastApplied: 0,
nextIndex: initTable[string, uint64](),
matchIndex: initTable[string, uint64](),
peers: peers,
leaderId: "",
electionTimeout: 150 + rand(150),
heartbeatTimeout: 50,
votesReceived: initHashSet[string](),
)
proc newRaftCluster*(): RaftCluster =
RaftCluster(
nodes: initTable[string, RaftNode](),
messageQueue: initDeque[RaftMessage](),
)
proc addNode*(cluster: RaftCluster, id: string) =
var peers: seq[string] = @[]
for existingId in cluster.nodes.keys:
peers.add(existingId)
cluster.nodes[existingId].peers.add(id)
cluster.nodes[id] = newRaftNode(id, peers)
proc lastLogIndex*(node: RaftNode): uint64 =
if node.log.len == 0:
return 0
return node.log[^1].index
proc lastLogTerm*(node: RaftNode): uint64 =
if node.log.len == 0:
return 0
return node.log[^1].term
proc becomeFollower*(node: RaftNode, term: uint64) =
node.state = rsFollower
node.currentTerm = term
node.votedFor = ""
node.votesReceived.clear()
proc becomeCandidate*(node: RaftNode) =
node.state = rsCandidate
inc node.currentTerm
node.votedFor = node.id
node.votesReceived.clear()
node.votesReceived.incl(node.id)
proc becomeLeader*(node: RaftNode) =
node.state = rsLeader
node.leaderId = node.id
for peer in node.peers:
node.nextIndex[peer] = node.lastLogIndex + 1
node.matchIndex[peer] = 0
proc handleRequestVote*(node: RaftNode, msg: RaftMessage): RaftMessage =
var reply = RaftMessage(
kind: rmkRequestVoteReply,
term: node.currentTerm,
senderId: node.id,
success: false,
)
if msg.term < node.currentTerm:
return reply
if msg.term > node.currentTerm:
node.becomeFollower(msg.term)
let canVote = node.votedFor == "" or node.votedFor == msg.senderId
let logOk = msg.lastLogTerm > node.lastLogTerm or
(msg.lastLogTerm == node.lastLogTerm and msg.lastLogIndex >= node.lastLogIndex)
if canVote and logOk:
node.votedFor = msg.senderId
reply.success = true
reply.term = node.currentTerm
return reply
proc handleAppendEntries*(node: RaftNode, msg: RaftMessage): RaftMessage =
var reply = RaftMessage(
kind: rmkAppendEntriesReply,
term: node.currentTerm,
senderId: node.id,
success: false,
matchIdx: 0,
)
if msg.term < node.currentTerm:
return reply
if msg.term >= node.currentTerm:
node.becomeFollower(msg.term)
node.leaderId = msg.senderId
# Check if log contains entry at prevLogIndex with prevLogTerm
if msg.prevLogIndex > 0:
if msg.prevLogIndex > uint64(node.log.len):
return reply
if node.log[msg.prevLogIndex - 1].term != msg.prevLogTerm:
# Delete conflicting entries
node.log.setLen(int(msg.prevLogIndex - 1))
return reply
# Append new entries
for entry in msg.entries:
let idx = int(entry.index - 1)
if idx < node.log.len:
if node.log[idx].term != entry.term:
node.log.setLen(idx)
node.log.add(entry)
else:
node.log.add(entry)
# Update commit index
if msg.leaderCommit > node.commitIndex:
node.commitIndex = min(msg.leaderCommit, node.lastLogIndex)
reply.success = true
reply.matchIdx = node.lastLogIndex
return reply
proc requestVote*(node: RaftNode): seq[RaftMessage] =
result = @[]
for peer in node.peers:
result.add(RaftMessage(
kind: rmkRequestVote,
term: node.currentTerm,
senderId: node.id,
lastLogIndex: node.lastLogIndex,
lastLogTerm: node.lastLogTerm,
))
proc appendEntries*(node: RaftNode, peerId: string): RaftMessage =
let nextIdx = node.nextIndex.getOrDefault(peerId, node.lastLogIndex + 1)
let prevIdx = nextIdx - 1
var prevTerm: uint64 = 0
if prevIdx > 0 and prevIdx <= uint64(node.log.len):
prevTerm = node.log[prevIdx - 1].term
var entries: seq[LogEntry] = @[]
for i in int(nextIdx - 1)..<node.log.len:
entries.add(node.log[i])
return RaftMessage(
kind: rmkAppendEntries,
term: node.currentTerm,
senderId: node.id,
prevLogIndex: prevIdx,
prevLogTerm: prevTerm,
entries: entries,
leaderCommit: node.commitIndex,
)
proc appendLog*(node: RaftNode, command: string, data: seq[byte] = @[]): LogEntry =
if node.state != rsLeader:
return LogEntry()
result = LogEntry(
term: node.currentTerm,
index: node.lastLogIndex + 1,
command: command,
data: data,
)
node.log.add(result)
proc handleVoteReply*(node: RaftNode, reply: RaftMessage) =
if reply.term > node.currentTerm:
node.becomeFollower(reply.term)
return
if node.state != rsCandidate:
return
if reply.success:
node.votesReceived.incl(reply.senderId)
if node.votesReceived.len > (node.peers.len + 1) div 2:
node.becomeLeader()
proc handleAppendReply*(node: RaftNode, peerId: string, reply: RaftMessage) =
if reply.term > node.currentTerm:
node.becomeFollower(reply.term)
return
if node.state != rsLeader:
return
if reply.success:
node.matchIndex[peerId] = reply.matchIdx
node.nextIndex[peerId] = reply.matchIdx + 1
# Update commit index
var matchIndices: seq[uint64] = @[node.lastLogIndex]
for p, idx in node.matchIndex:
matchIndices.add(idx)
matchIndices.sort()
let medianIdx = matchIndices[matchIndices.len div 2]
if medianIdx > node.commitIndex:
if medianIdx <= node.lastLogIndex and
node.log[medianIdx - 1].term == node.currentTerm:
node.commitIndex = medianIdx
else:
if node.nextIndex[peerId] > 1:
dec node.nextIndex[peerId]
proc state*(node: RaftNode): RaftState = node.state
proc isLeader*(node: RaftNode): bool = node.state == rsLeader
proc leaderId*(node: RaftNode): string = node.leaderId
proc logLen*(node: RaftNode): int = node.log.len
+133
View File
@@ -0,0 +1,133 @@
## Sharding — hash-based and range-based data distribution
import std/tables
import std/hashes
import std/algorithm
import std/sets
type
ShardStrategy* = enum
ssHash = "hash"
ssRange = "range"
ssConsistent = "consistent"
Shard* = object
id*: int
name*: string
minKey*: string
maxKey*: string
nodeIds*: seq[string] # replica node ids
isActive*: bool
entryCount*: int
ShardRouter* = ref object
strategy*: ShardStrategy
shards*: seq[Shard]
vnodeRing*: seq[(uint64, int)] # (hash, shard_id) sorted
replicas*: int
ShardConfig* = object
numShards*: int
replicas*: int
strategy*: ShardStrategy
proc defaultShardConfig*(): ShardConfig =
ShardConfig(numShards: 4, replicas: 1, strategy: ssHash)
proc newShardRouter*(config: ShardConfig = defaultShardConfig()): ShardRouter =
result = ShardRouter(
strategy: config.strategy,
shards: @[],
vnodeRing: @[],
replicas: config.replicas,
)
for i in 0..<config.numShards:
result.shards.add(Shard(
id: i,
name: "shard_" & $i,
minKey: "",
maxKey: "",
nodeIds: @[],
isActive: true,
entryCount: 0,
))
proc hashKey*(key: string): uint64 =
return uint64(hash(key))
proc getShardHash*(router: ShardRouter, key: string): int =
let h = hashKey(key)
return int(h mod uint64(router.shards.len))
proc getShardRange*(router: ShardRouter, key: string): int =
for i, shard in router.shards:
if key >= shard.minKey and key <= shard.maxKey:
return i
return 0
proc getShardConsistent*(router: ShardRouter, key: string): int =
if router.vnodeRing.len == 0:
return getShardHash(router, key)
let h = hashKey(key)
for (ringHash, shardId) in router.vnodeRing:
if h <= ringHash:
return shardId
return router.vnodeRing[0][1]
proc getShard*(router: ShardRouter, key: string): int =
case router.strategy
of ssHash: router.getShardHash(key)
of ssRange: router.getShardRange(key)
of ssConsistent: router.getShardConsistent(key)
proc addVirtualNodes*(router: var ShardRouter, numVnodes: int = 100) =
for shard in router.shards:
for i in 0..<numVnodes:
let vnodeKey = shard.name & "_vnode_" & $i
router.vnodeRing.add((hashKey(vnodeKey), shard.id))
router.vnodeRing.sort(proc(a, b: (uint64, int)): int = cmp(a[0], b[0]))
proc setRangeBounds*(router: var ShardRouter, bounds: seq[(string, string)]) =
for i, bound in bounds:
if i < router.shards.len:
router.shards[i].minKey = bound[0]
router.shards[i].maxKey = bound[1]
proc assignNode*(router: var ShardRouter, shardId: int, nodeId: string) =
if shardId < router.shards.len:
router.shards[shardId].nodeIds.add(nodeId)
proc getShardForNode*(router: ShardRouter, nodeId: string): seq[int] =
result = @[]
for i, shard in router.shards:
if nodeId in shard.nodeIds:
result.add(i)
proc replicasOf*(router: ShardRouter, key: string): seq[string] =
let shardId = router.getShard(key)
if shardId < router.shards.len:
return router.shards[shardId].nodeIds
return @[]
proc allShards*(router: ShardRouter): seq[Shard] =
return router.shards
proc activeShardCount*(router: ShardRouter): int =
result = 0
for s in router.shards:
if s.isActive:
inc result
proc rebalance*(router: var ShardRouter, nodes: seq[string]) =
if nodes.len == 0:
return
# Clear existing assignments
for i in 0..<router.shards.len:
router.shards[i].nodeIds = @[]
# Round-robin assign replicas
for i in 0..<router.shards.len:
for r in 0..<router.replicas:
let nodeIdx = (i + r) mod nodes.len
router.shards[i].nodeIds.add(nodes[nodeIdx])
proc shardCount*(router: ShardRouter): int = router.shards.len
+1 -1
View File
@@ -120,7 +120,7 @@ type
case kind*: NodeKind
of nkSelect:
selDistinct*: bool
selWith*: seq[Node]
selWith*: seq[(string, Node)]
selResult*: seq[Node]
selFrom*: Node
selJoins*: seq[Node]
+3 -2
View File
@@ -107,6 +107,7 @@ type
tkAssign
tkArrow
tkDoubleColon
tkColon
tkDot
tkComma
tkSemicolon
@@ -351,7 +352,7 @@ proc nextToken*(l: var Lexer): Token =
discard l.advance()
return Token(kind: tkEq, value: "==", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkAssign, value: ":=", line: startLine, col: startCol)
return Token(kind: tkEq, value: "=", line: startLine, col: startCol)
of ':':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=':
discard l.advance()
@@ -362,7 +363,7 @@ proc nextToken*(l: var Lexer): Token =
discard l.advance()
return Token(kind: tkDoubleColon, value: "::", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkInvalid, value: ":", line: startLine, col: startCol)
return Token(kind: tkColon, value: ":", line: startLine, col: startCol)
of '!':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=':
discard l.advance()
+266 -4
View File
@@ -2,6 +2,7 @@
import std/strutils
import lexer
import ast
import ../core/types
type
Parser* = object
@@ -56,9 +57,32 @@ proc parsePrimary(p: var Parser): Node =
Node(kind: nkNullLit, line: tok.line, col: tok.col)
of tkIdent:
discard p.advance()
Node(kind: nkIdent, identName: tok.value, line: tok.line, col: tok.col)
# Check for function call: ident(...)
if p.peek().kind == tkLParen:
discard p.advance() # consume (
var args: seq[Node] = @[]
if p.peek().kind != tkRParen:
args.add(p.parseExpr())
while p.match(tkComma):
args.add(p.parseExpr())
discard p.expect(tkRParen)
return Node(kind: nkFuncCall, funcName: tok.value, funcArgs: args,
line: tok.line, col: tok.col)
# Check for dotted path: ident.ident.ident
var parts = @[tok.value]
while p.peek().kind == tkDot:
discard p.advance() # consume .
parts.add(p.expect(tkIdent).value)
if parts.len == 1:
return Node(kind: nkIdent, identName: tok.value, line: tok.line, col: tok.col)
return Node(kind: nkPath, pathParts: parts, line: tok.line, col: tok.col)
of tkLParen:
discard p.advance()
# Check for subquery
if p.peek().kind == tkSelect:
let sub = p.parseSelect()
discard p.expect(tkRParen)
return Node(kind: nkSubquery, subQuery: sub, line: tok.line, col: tok.col)
let expr = p.parseExpr()
discard p.expect(tkRParen)
expr
@@ -87,14 +111,40 @@ proc parsePrimary(p: var Parser): Node =
discard p.advance()
let operand = p.parsePrimary()
Node(kind: nkUnaryOp, unOp: ukNeg, unOperand: operand, line: tok.line, col: tok.col)
of tkCount:
of tkCount, tkSum, tkAvg, tkMin, tkMax:
let funcName = tok.value
discard p.advance()
discard p.expect(tkLParen)
var args: seq[Node] = @[]
# Handle DISTINCT inside aggregate
var hasDistinct = false
if p.peek().kind == tkDistinct:
discard p.advance()
hasDistinct = true
if p.peek().kind != tkRParen:
args.add(p.parseExpr())
discard p.expect(tkRParen)
Node(kind: nkFuncCall, funcName: "count", funcArgs: args, line: tok.line, col: tok.col)
var node = Node(kind: nkFuncCall, funcName: funcName.toLower(), funcArgs: args,
line: tok.line, col: tok.col)
return node
of tkCase:
discard p.advance()
var caseExpr: Node = nil
# CASE expr WHEN ... THEN ... ELSE ... END
if p.peek().kind != tkWhen:
caseExpr = p.parseExpr()
var whens: seq[(Node, Node)] = @[]
while p.match(tkWhen):
let cond = p.parseExpr()
discard p.expect(tkThen)
let val = p.parseExpr()
whens.add((cond, val))
var elseExpr: Node = nil
if p.match(tkElse):
elseExpr = p.parseExpr()
discard p.expect(tkEnd)
Node(kind: nkCase, caseExpr: caseExpr, caseWhens: whens, caseElse: elseExpr,
line: tok.line, col: tok.col)
else:
discard p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
@@ -128,6 +178,37 @@ proc parseAddSub(p: var Parser): Node =
proc parseComparison(p: var Parser): Node =
result = p.parseAddSub()
# Handle BETWEEN ... AND ...
if p.peek().kind == tkBetween:
let tok = p.advance()
let low = p.parseAddSub()
discard p.expect(tkAnd)
let high = p.parseAddSub()
return Node(kind: nkBetweenExpr, betweenExpr: result,
betweenLow: low, betweenHigh: high, line: tok.line, col: tok.col)
# Handle IN (subquery | list)
if p.peek().kind == tkIn:
let tok = p.advance()
let right = p.parseAddSub()
return Node(kind: nkInExpr, inLeft: result, inRight: right,
line: tok.line, col: tok.col)
# Handle LIKE / ILIKE
if p.peek().kind in {tkLike, tkILike}:
let isILike = p.peek().kind == tkILike
let tok = p.advance()
let pattern = p.parseAddSub()
return Node(kind: nkLikeExpr, likeExpr: result, likePattern: pattern,
likeCaseInsensitive: isILike, line: tok.line, col: tok.col)
# Handle IS NULL / IS NOT NULL
if p.peek().kind == tkIs:
let tok = p.advance()
var negated = false
if p.peek().kind == tkNot:
discard p.advance()
negated = true
discard p.advance() # consume NULL token (assumed)
return Node(kind: nkIsExpr, isExpr: result, isNegated: negated,
line: tok.line, col: tok.col)
while p.peek().kind in {tkEq, tkNotEq, tkLt, tkLtEq, tkGt, tkGtEq}:
let op = case p.peek().kind
of tkEq: bkEq
@@ -169,20 +250,83 @@ proc parseOr(p: var Parser): Node =
proc parseExpr(p: var Parser): Node =
return p.parseOr()
proc parseJoinType(p: var Parser): JoinKind =
if p.match(tkInner):
return jkInner
elif p.match(tkLeft):
if p.match(tkOuter): discard
return jkLeft
elif p.match(tkRight):
if p.match(tkOuter): discard
return jkRight
elif p.match(tkFull):
if p.match(tkOuter): discard
return jkFull
elif p.match(tkCross):
return jkCross
return jkInner
proc parseWith(p: var Parser): Node =
# WITH name AS (select), name2 AS (select2) SELECT ...
let tok = p.expect(tkWith)
result = Node(kind: nkWith, line: tok.line, col: tok.col)
result.withBindings = @[]
# Parse first CTE
let cteName = p.expect(tkIdent).value
discard p.expect(tkAs)
discard p.expect(tkLParen)
let cteQuery = p.parseSelect()
discard p.expect(tkRParen)
result.withBindings.add((cteName, cteQuery))
# Parse additional CTEs
while p.match(tkComma):
let name = p.expect(tkIdent).value
discard p.expect(tkAs)
discard p.expect(tkLParen)
let query = p.parseSelect()
discard p.expect(tkRParen)
result.withBindings.add((name, query))
proc parseSelect(p: var Parser): Node =
# Handle WITH (CTE)
var withClause: Node = nil
if p.peek().kind == tkWith:
withClause = p.parseWith()
let tok = p.expect(tkSelect)
result = Node(kind: nkSelect, line: tok.line, col: tok.col)
if withClause != nil:
result.selWith = withClause.withBindings
if p.peek().kind == tkDistinct:
discard p.advance()
result.selDistinct = true
# Parse SELECT list
result.selResult = @[]
result.selResult.add(p.parseExpr())
while p.match(tkComma):
result.selResult.add(p.parseExpr())
# Parse FROM
result.selJoins = @[]
if p.match(tkFrom):
# Handle subquery: (SELECT ...) AS alias
if p.peek().kind == tkLParen:
discard p.advance() # consume (
let subquery = p.parseSelect()
discard p.expect(tkRParen)
var alias = ""
if p.match(tkAs):
alias = p.expect(tkIdent).value
elif p.peek().kind == tkIdent:
alias = p.advance().value
result.selFrom = Node(kind: nkFrom, fromTable: "(subquery)",
fromAlias: alias, line: tok.line, col: tok.col)
else:
let tableTok = p.expect(tkIdent)
var alias = ""
if p.match(tkAs):
@@ -192,12 +336,70 @@ proc parseSelect(p: var Parser): Node =
result.selFrom = Node(kind: nkFrom, fromTable: tableTok.value,
fromAlias: alias, line: tableTok.line, col: tableTok.col)
# Parse JOINs
while p.peek().kind == tkJoin or
(p.peek().kind in {tkInner, tkLeft, tkRight, tkFull, tkCross} and
p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind == tkJoin):
let jk = p.parseJoinType()
discard p.expect(tkJoin)
let joinTable = p.expect(tkIdent)
var joinAlias = ""
if p.match(tkAs):
joinAlias = p.expect(tkIdent).value
elif p.peek().kind == tkIdent:
joinAlias = p.advance().value
var joinCond: Node = nil
if p.match(tkOn):
joinCond = p.parseExpr()
let joinTarget = Node(kind: nkFrom, fromTable: joinTable.value,
fromAlias: joinAlias, line: joinTable.line, col: joinTable.col)
result.selJoins.add(Node(kind: nkJoin, joinKind: jk, joinTarget: joinTarget,
joinOn: joinCond, joinAlias: joinAlias,
line: joinTable.line, col: joinTable.col))
# Parse WHERE
if p.match(tkWhere):
result.selWhere = Node(kind: nkWhere, whereExpr: p.parseExpr())
# Parse GROUP BY
if p.match(tkGroup):
discard p.expect(tkBy)
result.selGroupBy = @[]
result.selGroupBy.add(p.parseExpr())
while p.match(tkComma):
result.selGroupBy.add(p.parseExpr())
# Parse HAVING
if p.match(tkHaving):
result.selHaving = Node(kind: nkHaving, havingExpr: p.parseExpr())
# Parse ORDER BY
if p.match(tkOrder):
discard p.expect(tkBy)
result.selOrderBy = @[]
var firstExpr = p.parseExpr()
var firstDir = sdAsc
if p.match(tkDesc):
firstDir = sdDesc
elif p.match(tkAsc):
firstDir = sdAsc
result.selOrderBy.add(Node(kind: nkOrderBy, orderByExpr: firstExpr,
orderByDir: firstDir))
while p.match(tkComma):
let expr = p.parseExpr()
var dir = sdAsc
if p.match(tkDesc):
dir = sdDesc
elif p.match(tkAsc):
dir = sdAsc
result.selOrderBy.add(Node(kind: nkOrderBy, orderByExpr: expr,
orderByDir: dir))
# Parse LIMIT
if p.match(tkLimit):
result.selLimit = Node(kind: nkLimit, limitExpr: p.parseExpr())
# Parse OFFSET
if p.match(tkOffset):
result.selOffset = Node(kind: nkOffset, offsetExpr: p.parseExpr())
@@ -210,21 +412,81 @@ proc parseUpdate(p: var Parser): Node =
let tok = p.expect(tkUpdate)
let target = p.expect(tkIdent).value
result = Node(kind: nkUpdate, updTarget: target, line: tok.line, col: tok.col)
if p.match(tkSet):
result.updSet = @[]
let field = p.expect(tkIdent).value
discard p.match(tkEq) # = or :=
let val = p.parseExpr()
result.updSet.add(Node(kind: nkBinOp, binOp: bkAssign,
binLeft: Node(kind: nkIdent, identName: field),
binRight: val))
while p.match(tkComma):
let f = p.expect(tkIdent).value
discard p.match(tkEq)
let v = p.parseExpr()
result.updSet.add(Node(kind: nkBinOp, binOp: bkAssign,
binLeft: Node(kind: nkIdent, identName: f),
binRight: v))
if p.match(tkWhere):
result.updWhere = Node(kind: nkWhere, whereExpr: p.parseExpr())
proc parseDelete(p: var Parser): Node =
let tok = p.expect(tkDelete)
discard p.match(tkFrom) # optional FROM keyword
let target = p.expect(tkIdent).value
result = Node(kind: nkDelete, delTarget: target, line: tok.line, col: tok.col)
if p.match(tkWhere):
result.delWhere = Node(kind: nkWhere, whereExpr: p.parseExpr())
proc parseCreateType(p: var Parser): Node =
let tok = p.expect(tkCreate)
discard p.expect(tkType)
let name = p.expect(tkIdent).value
result = Node(kind: nkCreateType, ctName: name, line: tok.line, col: tok.col)
# Parse bases (EXTENDING)
result.ctBases = @[]
if p.match(tkIdent): # "extending" keyword mapped to ident
# Check if the ident is "extending"
# For now, just accept bases in braces
discard
# Parse body
if p.match(tkLBrace):
result.ctProperties = @[]
result.ctLinks = @[]
while p.peek().kind != tkRbrace and p.peek().kind != tkEof:
discard p.match(tkComma) # optional comma separator
# Parse property or link
var isRequired = false
var isMulti = false
if p.peek().kind == tkRequired:
discard p.advance()
isRequired = true
if p.peek().kind == tkMulti:
discard p.advance()
isMulti = true
let fieldTok = p.expect(tkIdent)
# Check for link or property
if p.peek().kind == tkArrow: # -> means link
discard p.advance() # consume ->
let target = p.expect(tkIdent).value
result.ctLinks.add(Node(kind: nkLinkDef,
ldName: fieldTok.value, ldTarget: target,
ldRequired: isRequired,
ldCardinality: if isMulti: Many else: One))
else:
var typeName = ""
if p.match(tkColon):
typeName = p.expect(tkIdent).value
result.ctProperties.add(Node(kind: nkPropertyDef,
pdName: fieldTok.value, pdType: typeName,
pdRequired: isRequired))
discard p.match(tkSemicolon)
discard p.expect(tkRbrace)
proc parseStatement*(p: var Parser): Node =
case p.peek().kind
of tkSelect: p.parseSelect()
of tkWith, tkSelect: p.parseSelect()
of tkInsert: p.parseInsert()
of tkUpdate: p.parseUpdate()
of tkDelete: p.parseDelete()
+172
View File
@@ -7,6 +7,8 @@ import barabadb/core/types
import barabadb/core/mvcc
import barabadb/core/deadlock
import barabadb/core/columnar
import barabadb/core/raft
import barabadb/core/sharding
import barabadb/storage/bloom
import barabadb/storage/wal
import barabadb/storage/lsm
@@ -782,3 +784,173 @@ suite "Vector Metadata Filtering":
let results = vengine.searchWithFilter(idx, @[1.0'f32, 0.0'f32, 0.0'f32], 10,
filter = filterA)
check results.len == 2 # only category A entries
suite "BaraQL Parser — Extended":
test "Parse GROUP BY":
let ast = parse("SELECT dept, count(*) FROM employees GROUP BY dept")
check ast.stmts.len == 1
check ast.stmts[0].selGroupBy.len == 1
test "Parse GROUP BY with HAVING":
let ast = parse("SELECT dept, count(*) FROM employees GROUP BY dept HAVING count(*) > 5")
check ast.stmts[0].selHaving != nil
test "Parse ORDER BY with direction":
let ast = parse("SELECT name FROM users ORDER BY age DESC")
check ast.stmts[0].selOrderBy.len == 1
check ast.stmts[0].selOrderBy[0].orderByDir == sdDesc
test "Parse ORDER BY multiple columns":
let ast = parse("SELECT * FROM t ORDER BY a ASC, b DESC")
check ast.stmts[0].selOrderBy.len == 2
test "Parse INNER JOIN":
let ast = parse("SELECT u.name, o.total FROM users u INNER JOIN orders o ON u.id = o.user_id")
check ast.stmts[0].selJoins.len == 1
check ast.stmts[0].selJoins[0].joinKind == jkInner
test "Parse LEFT JOIN":
let ast = parse("SELECT u.name FROM users u LEFT JOIN orders o ON u.id = o.user_id")
check ast.stmts[0].selJoins.len == 1
check ast.stmts[0].selJoins[0].joinKind == jkLeft
test "Parse multiple JOINs":
let ast = parse("SELECT * FROM a JOIN b ON a.id = b.aid JOIN c ON b.id = c.bid")
check ast.stmts[0].selJoins.len == 2
test "Parse CTE (WITH)":
let ast = parse("WITH active AS (SELECT * FROM users WHERE active = true) SELECT * FROM active")
check ast.stmts[0].selWith.len == 1
check ast.stmts[0].selWith[0][0] == "active"
test "Parse multiple CTEs":
let ast = parse("WITH a AS (SELECT id FROM t1), b AS (SELECT id FROM t2) SELECT * FROM a")
check ast.stmts[0].selWith.len == 2
test "Parse aggregate functions in SELECT":
let ast = parse("SELECT count(*), sum(amount), avg(price), min(age), max(score) FROM orders")
check ast.stmts[0].selResult.len == 5
test "Parse CASE expression":
let ast = parse("SELECT CASE WHEN age > 18 THEN 'adult' ELSE 'minor' END FROM users")
check ast.stmts[0].selResult.len == 1
test "Parse BETWEEN":
let ast = parse("SELECT * FROM products WHERE price BETWEEN 10 AND 100")
check ast.stmts[0].selWhere != nil
test "Parse subquery in FROM":
let ast = parse("SELECT * FROM (SELECT id FROM users) AS sub")
check ast.stmts[0].selFrom != nil
test "Parse UPDATE SET WHERE":
let ast = parse("UPDATE users SET name = 'Alice' WHERE id = 1")
check ast.stmts[0].updSet.len == 1
test "Parse DELETE WHERE":
let ast = parse("DELETE FROM users WHERE id = 1")
check ast.stmts[0].delWhere != nil
test "Parse CREATE TYPE with properties":
let ast = parse("CREATE TYPE Person { name: str, age: int32 }")
check ast.stmts[0].ctName == "Person"
check ast.stmts[0].ctProperties.len == 2
suite "Raft Consensus":
test "Create cluster with nodes":
var cluster = newRaftCluster()
cluster.addNode("n1")
cluster.addNode("n2")
cluster.addNode("n3")
check cluster.nodes.len == 3
check cluster.nodes["n1"].peers.len == 2
test "Initial state is follower":
var cluster = newRaftCluster()
cluster.addNode("n1")
check cluster.nodes["n1"].state == rsFollower
test "Election — single node becomes leader":
var cluster = newRaftCluster()
cluster.addNode("n1")
let node = cluster.nodes["n1"]
node.becomeCandidate()
node.becomeLeader()
check node.isLeader
check node.leaderId == "n1"
test "Log replication":
var cluster = newRaftCluster()
cluster.addNode("n1")
let node = cluster.nodes["n1"]
node.becomeCandidate()
node.becomeLeader()
let entry = node.appendLog("SET key1 value1")
check entry.index == 1
check node.logLen == 1
let entry2 = node.appendLog("SET key2 value2")
check entry2.index == 2
check node.logLen == 2
test "RequestVote handling":
var cluster = newRaftCluster()
cluster.addNode("n1")
cluster.addNode("n2")
let n1 = cluster.nodes["n1"]
let n2 = cluster.nodes["n2"]
let req = RaftMessage(kind: rmkRequestVote, term: 1, senderId: "n2",
lastLogIndex: 0, lastLogTerm: 0)
let reply = n1.handleRequestVote(req)
check reply.success
check n1.votedFor == "n2"
test "AppendEntries handling":
var cluster = newRaftCluster()
cluster.addNode("n1")
cluster.addNode("n2")
let n2 = cluster.nodes["n2"]
let msg = RaftMessage(kind: rmkAppendEntries, term: 1, senderId: "n1",
prevLogIndex: 0, prevLogTerm: 0,
entries: @[LogEntry(term: 1, index: 1, command: "SET x 1")],
leaderCommit: 0)
let reply = n2.handleAppendEntries(msg)
check reply.success
check n2.logLen == 1
suite "Sharding":
test "Hash-based sharding":
var router = newShardRouter(ShardConfig(numShards: 4, strategy: ssHash))
check router.shardCount == 4
let s1 = router.getShard("user_1")
let s2 = router.getShard("user_2")
check s1 >= 0 and s1 < 4
check s2 >= 0 and s2 < 4
test "Consistent hashing":
var router = newShardRouter(ShardConfig(numShards: 4, strategy: ssConsistent))
router.addVirtualNodes(50)
let s = router.getShard("some_key")
check s >= 0 and s < 4
test "Range-based sharding":
var router = newShardRouter(ShardConfig(numShards: 3, strategy: ssRange))
router.setRangeBounds(@[("a", "f"), ("g", "n"), ("o", "z")])
check router.getShardRange("apple") == 0
check router.getShardRange("hello") == 1
check router.getShardRange("top") == 2
test "Rebalance assigns nodes":
var router = newShardRouter(ShardConfig(numShards: 3, replicas: 2, strategy: ssHash))
router.rebalance(@["node1", "node2", "node3"])
for shard in router.shards:
check shard.nodeIds.len == 2 # 2 replicas
test "Replicas of key":
var router = newShardRouter(ShardConfig(numShards: 2, replicas: 1, strategy: ssHash))
router.rebalance(@["n1", "n2"])
let replicas = router.replicasOf("test_key")
check replicas.len == 1
test "Active shard count":
var router = newShardRouter()
check router.activeShardCount == 4