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] Array литерали
|
||||||
- [x] Типов анализатор (type checker)
|
- [x] Типов анализатор (type checker)
|
||||||
- [x] IR (Intermediate Representation)
|
- [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 операции
|
- [ ] Codegen → storage операции
|
||||||
- [ ] GROUP BY, HAVING
|
|
||||||
- [ ] JOIN (inner, left, right, full)
|
|
||||||
- [ ] CTE (WITH)
|
|
||||||
- [ ] Агрегатни функции (count, sum, avg, min, max)
|
|
||||||
- [ ] Потребителски функции (UDF)
|
- [ ] Потребителски функции (UDF)
|
||||||
|
|
||||||
### Фаза 3: Мултимодален storage 🟡
|
### Фаза 3: Мултимодален storage 🟡
|
||||||
@@ -170,11 +170,11 @@
|
|||||||
- [ ] Interactive query editor с autocomplete
|
- [ ] Interactive query editor с autocomplete
|
||||||
- [ ] Import/Export (JSON, CSV, Parquet)
|
- [ ] Import/Export (JSON, CSV, Parquet)
|
||||||
|
|
||||||
### Фаза 11: Кластеризация и разпределение ⬜
|
### Фаза 11: Кластеризация и разпределение 🟡
|
||||||
- [ ] Raft консенсус протокол
|
- [x] Raft консенсус протокол (leader election + log replication)
|
||||||
- [ ] Sharding (hash-based, range-based)
|
- [x] Sharding (hash-based, range-based, consistent hashing)
|
||||||
- [ ] Replication (sync, async)
|
- [ ] Replication (sync, async)
|
||||||
- [ ] Leader election
|
- [ ] Leader election за multi-node
|
||||||
- [ ] Gossip protocol за membership
|
- [ ] Gossip protocol за membership
|
||||||
- [ ] Distributed transactions
|
- [ ] Distributed transactions
|
||||||
- [ ] Auto-rebalancing
|
- [ ] Auto-rebalancing
|
||||||
@@ -196,16 +196,16 @@
|
|||||||
| Фаза | Статус | Напредък |
|
| Фаза | Статус | Напредък |
|
||||||
|------|--------|----------|
|
|------|--------|----------|
|
||||||
| 1. Ядро | ✅ Завършена | 95% |
|
| 1. Ядро | ✅ Завършена | 95% |
|
||||||
| 2. BaraQL | 🟡 В процес | 60% |
|
| 2. BaraQL | ✅ Основно завършена | 85% |
|
||||||
| 3. Мултимодален storage | 🟡 В процес | 75% |
|
| 3. Мултимодален storage | 🟡 В процес | 75% |
|
||||||
| 4. Транзакции | ✅ Основно завършена | 85% |
|
| 4. Транзакции | ✅ Основно завършена | 85% |
|
||||||
| 5. Протокол | 🟡 В процес | 85% |
|
| 5. Протокол | ✅ Основно завършена | 85% |
|
||||||
| 6. Schema | ✅ Основно завършена | 75% |
|
| 6. Schema | ✅ Основно завършена | 75% |
|
||||||
| 7. Векторен engine | ✅ Завършена | 95% |
|
| 7. Векторен engine | ✅ Завършена | 95% |
|
||||||
| 8. Graph engine | ✅ Завършена | 90% |
|
| 8. Graph engine | ✅ Завършена | 90% |
|
||||||
| 9. FTS | ✅ Завършена | 85% |
|
| 9. FTS | ✅ Завършена | 85% |
|
||||||
| 10. Клиенти и CLI | 🟡 В процес | 50% |
|
| 10. Клиенти и CLI | 🟡 В процес | 50% |
|
||||||
| 11. Кластер | ⬜ Не стартирана | 0% |
|
| 11. Кластер | 🟡 В процес | 30% |
|
||||||
| 12. Оптимизации | ⬜ Не стартирана | 0% |
|
| 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
|
case kind*: NodeKind
|
||||||
of nkSelect:
|
of nkSelect:
|
||||||
selDistinct*: bool
|
selDistinct*: bool
|
||||||
selWith*: seq[Node]
|
selWith*: seq[(string, Node)]
|
||||||
selResult*: seq[Node]
|
selResult*: seq[Node]
|
||||||
selFrom*: Node
|
selFrom*: Node
|
||||||
selJoins*: seq[Node]
|
selJoins*: seq[Node]
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ type
|
|||||||
tkAssign
|
tkAssign
|
||||||
tkArrow
|
tkArrow
|
||||||
tkDoubleColon
|
tkDoubleColon
|
||||||
|
tkColon
|
||||||
tkDot
|
tkDot
|
||||||
tkComma
|
tkComma
|
||||||
tkSemicolon
|
tkSemicolon
|
||||||
@@ -351,7 +352,7 @@ proc nextToken*(l: var Lexer): Token =
|
|||||||
discard l.advance()
|
discard l.advance()
|
||||||
return Token(kind: tkEq, value: "==", line: startLine, col: startCol)
|
return Token(kind: tkEq, value: "==", line: startLine, col: startCol)
|
||||||
discard l.advance()
|
discard l.advance()
|
||||||
return Token(kind: tkAssign, value: ":=", line: startLine, col: startCol)
|
return Token(kind: tkEq, value: "=", line: startLine, col: startCol)
|
||||||
of ':':
|
of ':':
|
||||||
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=':
|
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=':
|
||||||
discard l.advance()
|
discard l.advance()
|
||||||
@@ -362,7 +363,7 @@ proc nextToken*(l: var Lexer): Token =
|
|||||||
discard l.advance()
|
discard l.advance()
|
||||||
return Token(kind: tkDoubleColon, value: "::", line: startLine, col: startCol)
|
return Token(kind: tkDoubleColon, value: "::", line: startLine, col: startCol)
|
||||||
discard l.advance()
|
discard l.advance()
|
||||||
return Token(kind: tkInvalid, value: ":", line: startLine, col: startCol)
|
return Token(kind: tkColon, value: ":", line: startLine, col: startCol)
|
||||||
of '!':
|
of '!':
|
||||||
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=':
|
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=':
|
||||||
discard l.advance()
|
discard l.advance()
|
||||||
|
|||||||
+274
-12
@@ -2,6 +2,7 @@
|
|||||||
import std/strutils
|
import std/strutils
|
||||||
import lexer
|
import lexer
|
||||||
import ast
|
import ast
|
||||||
|
import ../core/types
|
||||||
|
|
||||||
type
|
type
|
||||||
Parser* = object
|
Parser* = object
|
||||||
@@ -56,9 +57,32 @@ proc parsePrimary(p: var Parser): Node =
|
|||||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
||||||
of tkIdent:
|
of tkIdent:
|
||||||
discard p.advance()
|
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:
|
of tkLParen:
|
||||||
discard p.advance()
|
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()
|
let expr = p.parseExpr()
|
||||||
discard p.expect(tkRParen)
|
discard p.expect(tkRParen)
|
||||||
expr
|
expr
|
||||||
@@ -87,14 +111,40 @@ proc parsePrimary(p: var Parser): Node =
|
|||||||
discard p.advance()
|
discard p.advance()
|
||||||
let operand = p.parsePrimary()
|
let operand = p.parsePrimary()
|
||||||
Node(kind: nkUnaryOp, unOp: ukNeg, unOperand: operand, line: tok.line, col: tok.col)
|
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.advance()
|
||||||
discard p.expect(tkLParen)
|
discard p.expect(tkLParen)
|
||||||
var args: seq[Node] = @[]
|
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:
|
if p.peek().kind != tkRParen:
|
||||||
args.add(p.parseExpr())
|
args.add(p.parseExpr())
|
||||||
discard p.expect(tkRParen)
|
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:
|
else:
|
||||||
discard p.advance()
|
discard p.advance()
|
||||||
Node(kind: nkNullLit, line: tok.line, col: tok.col)
|
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 =
|
proc parseComparison(p: var Parser): Node =
|
||||||
result = p.parseAddSub()
|
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}:
|
while p.peek().kind in {tkEq, tkNotEq, tkLt, tkLtEq, tkGt, tkGtEq}:
|
||||||
let op = case p.peek().kind
|
let op = case p.peek().kind
|
||||||
of tkEq: bkEq
|
of tkEq: bkEq
|
||||||
@@ -169,35 +250,156 @@ proc parseOr(p: var Parser): Node =
|
|||||||
proc parseExpr(p: var Parser): Node =
|
proc parseExpr(p: var Parser): Node =
|
||||||
return p.parseOr()
|
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 =
|
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)
|
let tok = p.expect(tkSelect)
|
||||||
result = Node(kind: nkSelect, line: tok.line, col: tok.col)
|
result = Node(kind: nkSelect, line: tok.line, col: tok.col)
|
||||||
|
|
||||||
|
if withClause != nil:
|
||||||
|
result.selWith = withClause.withBindings
|
||||||
|
|
||||||
if p.peek().kind == tkDistinct:
|
if p.peek().kind == tkDistinct:
|
||||||
discard p.advance()
|
discard p.advance()
|
||||||
result.selDistinct = true
|
result.selDistinct = true
|
||||||
|
|
||||||
|
# Parse SELECT list
|
||||||
result.selResult = @[]
|
result.selResult = @[]
|
||||||
result.selResult.add(p.parseExpr())
|
result.selResult.add(p.parseExpr())
|
||||||
while p.match(tkComma):
|
while p.match(tkComma):
|
||||||
result.selResult.add(p.parseExpr())
|
result.selResult.add(p.parseExpr())
|
||||||
|
|
||||||
|
# Parse FROM
|
||||||
|
result.selJoins = @[]
|
||||||
if p.match(tkFrom):
|
if p.match(tkFrom):
|
||||||
let tableTok = p.expect(tkIdent)
|
# Handle subquery: (SELECT ...) AS alias
|
||||||
var alias = ""
|
if p.peek().kind == tkLParen:
|
||||||
if p.match(tkAs):
|
discard p.advance() # consume (
|
||||||
alias = p.expect(tkIdent).value
|
let subquery = p.parseSelect()
|
||||||
elif p.peek().kind == tkIdent:
|
discard p.expect(tkRParen)
|
||||||
alias = p.advance().value
|
var alias = ""
|
||||||
result.selFrom = Node(kind: nkFrom, fromTable: tableTok.value,
|
if p.match(tkAs):
|
||||||
fromAlias: alias, line: tableTok.line, col: tableTok.col)
|
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):
|
if p.match(tkWhere):
|
||||||
result.selWhere = Node(kind: nkWhere, whereExpr: p.parseExpr())
|
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):
|
if p.match(tkLimit):
|
||||||
result.selLimit = Node(kind: nkLimit, limitExpr: p.parseExpr())
|
result.selLimit = Node(kind: nkLimit, limitExpr: p.parseExpr())
|
||||||
|
|
||||||
|
# Parse OFFSET
|
||||||
if p.match(tkOffset):
|
if p.match(tkOffset):
|
||||||
result.selOffset = Node(kind: nkOffset, offsetExpr: p.parseExpr())
|
result.selOffset = Node(kind: nkOffset, offsetExpr: p.parseExpr())
|
||||||
|
|
||||||
@@ -210,21 +412,81 @@ proc parseUpdate(p: var Parser): Node =
|
|||||||
let tok = p.expect(tkUpdate)
|
let tok = p.expect(tkUpdate)
|
||||||
let target = p.expect(tkIdent).value
|
let target = p.expect(tkIdent).value
|
||||||
result = Node(kind: nkUpdate, updTarget: target, line: tok.line, col: tok.col)
|
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 =
|
proc parseDelete(p: var Parser): Node =
|
||||||
let tok = p.expect(tkDelete)
|
let tok = p.expect(tkDelete)
|
||||||
|
discard p.match(tkFrom) # optional FROM keyword
|
||||||
let target = p.expect(tkIdent).value
|
let target = p.expect(tkIdent).value
|
||||||
result = Node(kind: nkDelete, delTarget: target, line: tok.line, col: tok.col)
|
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 =
|
proc parseCreateType(p: var Parser): Node =
|
||||||
let tok = p.expect(tkCreate)
|
let tok = p.expect(tkCreate)
|
||||||
discard p.expect(tkType)
|
discard p.expect(tkType)
|
||||||
let name = p.expect(tkIdent).value
|
let name = p.expect(tkIdent).value
|
||||||
result = Node(kind: nkCreateType, ctName: name, line: tok.line, col: tok.col)
|
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 =
|
proc parseStatement*(p: var Parser): Node =
|
||||||
case p.peek().kind
|
case p.peek().kind
|
||||||
of tkSelect: p.parseSelect()
|
of tkWith, tkSelect: p.parseSelect()
|
||||||
of tkInsert: p.parseInsert()
|
of tkInsert: p.parseInsert()
|
||||||
of tkUpdate: p.parseUpdate()
|
of tkUpdate: p.parseUpdate()
|
||||||
of tkDelete: p.parseDelete()
|
of tkDelete: p.parseDelete()
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import barabadb/core/types
|
|||||||
import barabadb/core/mvcc
|
import barabadb/core/mvcc
|
||||||
import barabadb/core/deadlock
|
import barabadb/core/deadlock
|
||||||
import barabadb/core/columnar
|
import barabadb/core/columnar
|
||||||
|
import barabadb/core/raft
|
||||||
|
import barabadb/core/sharding
|
||||||
import barabadb/storage/bloom
|
import barabadb/storage/bloom
|
||||||
import barabadb/storage/wal
|
import barabadb/storage/wal
|
||||||
import barabadb/storage/lsm
|
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,
|
let results = vengine.searchWithFilter(idx, @[1.0'f32, 0.0'f32, 0.0'f32], 10,
|
||||||
filter = filterA)
|
filter = filterA)
|
||||||
check results.len == 2 # only category A entries
|
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