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:
+12
-12
@@ -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% |
|
||||
|
||||
**Легенда:** ⬜ Не стартирана | 🟡 В процес | ✅ Завършена
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
+274
-12
@@ -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,35 +250,156 @@ 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):
|
||||
let tableTok = p.expect(tkIdent)
|
||||
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: tableTok.value,
|
||||
fromAlias: alias, line: tableTok.line, col: tableTok.col)
|
||||
# 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):
|
||||
alias = p.expect(tkIdent).value
|
||||
elif p.peek().kind == tkIdent:
|
||||
alias = p.advance().value
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user