feat: initial BaraDB — multimodal database engine in Nim

- LSM-Tree storage engine with WAL, bloom filter, MemTable
- BaraQL query language: lexer (80+ tokens), recursive descent parser, AST
- Vector engine: HNSW + IVF-PQ indexes, 4 distance metrics
- Graph engine: adjacency list, BFS/DFS, Dijkstra, PageRank
- Full-Text Search: inverted index, BM25 ranking, stemming, stop words
- Type system: 17 types (int/float/string/uuid/json/vector/...)
- Async TCP server
- 21 passing tests
This commit is contained in:
2026-05-06 00:22:12 +03:00
commit 6935889877
18 changed files with 2676 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
import barabadb/core/config
import barabadb/core/server
import barabadb/core/types
import barabadb/storage/lsm
import barabadb/storage/wal
import barabadb/storage/bloom
+25
View File
@@ -0,0 +1,25 @@
type
BaraConfig* = object
address*: string
port*: int
dataDir*: string
maxConnections*: int
walEnabled*: bool
compactionStrategy*: CompactionStrategy
CompactionStrategy* = enum
csSizeTiered = "size_tiered"
csLeveled = "leveled"
proc defaultConfig*(): BaraConfig =
BaraConfig(
address: "127.0.0.1",
port: 5432,
dataDir: "./data",
maxConnections: 1000,
walEnabled: true,
compactionStrategy: csLeveled,
)
proc loadConfig*(): BaraConfig =
result = defaultConfig()
+48
View File
@@ -0,0 +1,48 @@
## BaraDB Server — async TCP server
import std/asyncdispatch
import std/asyncnet
import std/strutils
import config
type
Server* = ref object
config: BaraConfig
running: bool
ClientConnection = ref object
socket: AsyncSocket
id: int
proc newServer*(config: BaraConfig): Server =
Server(config: config, running: false)
proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.} =
echo "Client ", clientId, " connected"
try:
while true:
let line = await client.recvLine()
if line.len == 0:
break
echo "[", clientId, "] ", line
await client.send("OK\n")
except:
discard
finally:
echo "Client ", clientId, " disconnected"
client.close()
proc run*(server: Server) {.async.} =
server.running = true
var clientId = 0
let sock = newAsyncSocket()
sock.setSockOpt(OptReuseAddr, true)
sock.bindAddr(Port(server.config.port), server.config.address)
sock.listen()
echo "BaraDB listening on ", server.config.address, ":", server.config.port
while server.running:
let client = await sock.accept()
inc clientId
asyncCheck server.handleClient(client, clientId)
proc stop*(server: Server) =
server.running = false
+126
View File
@@ -0,0 +1,126 @@
import std/times
import std/oids
import std/monotimes
type
ValueKind* = enum
vkNull
vkBool
vkInt8
vkInt16
vkInt32
vkInt64
vkFloat32
vkFloat64
vkString
vkBytes
vkUuid
vkDateTime
vkJson
vkArray
vkObject
vkVector
Value* = object
case kind*: ValueKind
of vkNull: discard
of vkBool: boolVal*: bool
of vkInt8: int8Val*: int8
of vkInt16: int16Val*: int16
of vkInt32: int32Val*: int32
of vkInt64: int64Val*: int64
of vkFloat32: float32Val*: float32
of vkFloat64: float64Val*: float64
of vkString: strVal*: string
of vkBytes: bytesVal*: seq[byte]
of vkUuid: uuidVal*: Oid
of vkDateTime: dtVal*: DateTime
of vkJson: jsonVal*: string
of vkArray: arrayVal*: seq[Value]
of vkObject: objVal*: seq[(string, Value)]
of vkVector: vecVal*: seq[float32]
RecordId* = distinct uint64
Record* = object
id*: RecordId
data*: seq[(string, Value)]
SchemaKind* = enum
skScalar
skObject
skLink
skCollection
ScalarType* = enum
stBool = "bool"
stInt8 = "int8"
stInt16 = "int16"
stInt32 = "int32"
stInt64 = "int64"
stFloat32 = "float32"
stFloat64 = "float64"
stString = "str"
stBytes = "bytes"
stUuid = "uuid"
stDateTime = "datetime"
stJson = "json"
stVector = "vector"
Cardinality* = enum
One
Many
PropertyDef* = object
name*: string
typ*: ScalarType
required*: bool
default*: Value
computed*: bool
expr*: string
LinkDef* = object
name*: string
target*: string
cardinality*: Cardinality
required*: bool
properties*: seq[PropertyDef]
onDelete*: DeleteAction
DeleteAction* = enum
daRestrict
daDeleteSource
daAllow
daDeferredRestrict
ObjectTypeDef* = object
name*: string
bases*: seq[string]
properties*: seq[PropertyDef]
links*: seq[LinkDef]
indexes*: seq[IndexDef]
constraints*: seq[ConstraintDef]
IndexDef* = object
name*: string
expr*: string
kind*: IndexKind
IndexKind* = enum
ikBTree
ikHash
ikGiST
ikGIN
ikHNSW
ikIVFPQ
ikFullText
ConstraintDef* = object
name*: string
expr*: string
proc newRecordId*(): RecordId =
RecordId(uint64(getMonoTime().ticks()))
proc `==`*(a, b: RecordId): bool {.borrow.}
proc `$`*(r: RecordId): string = $uint64(r)
+232
View File
@@ -0,0 +1,232 @@
## Full-Text Search Engine — inverted index with BM25 ranking
import std/tables
import std/strutils
import std/unicode
import std/math
import std/algorithm
import std/sets
type
TermFreq* = Table[string, int]
DocLen* = int
PostingEntry* = object
docId*: uint64
termFreq*: int
positions*: seq[int]
InvertedIndex* = ref object
postings*: Table[string, seq[PostingEntry]]
docLengths*: Table[uint64, int]
docCount*: int
avgDocLen*: float64
totalTerms*: int
SearchResult* = object
docId*: uint64
score*: float64
highlights*: seq[(int, int)]
TokenizerConfig* = object
lowercase*: bool
removeStopWords*: bool
stemming*: bool
minWordLen*: int
maxWordLen*: int
const stopWords* = [
"a", "an", "the", "is", "it", "in", "on", "at", "to", "for",
"of", "with", "by", "from", "as", "into", "through", "during",
"before", "after", "above", "below", "between", "out", "off",
"over", "under", "again", "further", "then", "once", "here",
"there", "when", "where", "why", "how", "all", "each", "every",
"both", "few", "more", "most", "other", "some", "such", "no",
"nor", "not", "only", "own", "same", "so", "than", "too",
"very", "can", "will", "just", "don", "should", "now",
"и", "в", "на", "за", "от", "да", "се", "е", "са", "по",
"не", "че", "с", "към", "но", "или", "ако", "при", "до",
]
proc defaultTokenizerConfig*(): TokenizerConfig =
TokenizerConfig(
lowercase: true,
removeStopWords: true,
stemming: false,
minWordLen: 2,
maxWordLen: 64,
)
proc simpleStem(word: string): string =
if word.len <= 3:
return word
if word.endsWith("ing"):
return word[0..^4]
if word.endsWith("tion"):
return word[0..^5]
if word.endsWith("ness"):
return word[0..^5]
if word.endsWith("ment"):
return word[0..^5]
if word.endsWith("able"):
return word[0..^5]
if word.endsWith("ible"):
return word[0..^5]
if word.endsWith("ies"):
return word[0..^4] & "y"
if word.endsWith("es") and word.len > 4:
return word[0..^3]
if word.endsWith("ed") and word.len > 4:
return word[0..^3]
if word.endsWith("ly") and word.len > 4:
return word[0..^3]
if word.endsWith("s") and not word.endsWith("ss") and word.len > 3:
return word[0..^2]
return word
proc tokenize*(text: string, config: TokenizerConfig = defaultTokenizerConfig()): seq[string] =
result = @[]
var word = ""
for ch in text:
if ch.isAlphaNumeric() or ch in {'_', '-'}:
word.add(ch)
else:
if word.len > 0:
var token = word
if config.lowercase:
token = token.toLower()
if config.stemming:
token = simpleStem(token)
if token.len >= config.minWordLen and token.len <= config.maxWordLen:
if not config.removeStopWords or token notin stopWords:
result.add(token)
word = ""
if word.len > 0:
var token = word
if config.lowercase:
token = token.toLower()
if config.stemming:
token = simpleStem(token)
if token.len >= config.minWordLen and token.len <= config.maxWordLen:
if not config.removeStopWords or token notin stopWords:
result.add(token)
proc newInvertedIndex*(): InvertedIndex =
InvertedIndex(
postings: initTable[string, seq[PostingEntry]](),
docLengths: initTable[uint64, int](),
docCount: 0,
avgDocLen: 0.0,
totalTerms: 0,
)
proc addDocument*(idx: InvertedIndex, docId: uint64, text: string,
config: TokenizerConfig = defaultTokenizerConfig()) =
let tokens = tokenize(text, config)
var termFreqs = initTable[string, int]()
var positions = initTable[string, seq[int]]()
for i, token in tokens:
if token notin termFreqs:
termFreqs[token] = 0
positions[token] = @[]
inc termFreqs[token]
positions[token].add(i)
for term, freq in termFreqs:
if term notin idx.postings:
idx.postings[term] = @[]
idx.postings[term].add(PostingEntry(
docId: docId,
termFreq: freq,
positions: positions[term],
))
idx.docLengths[docId] = tokens.len
inc idx.docCount
idx.totalTerms += tokens.len
idx.avgDocLen = float64(idx.totalTerms) / float64(idx.docCount)
proc removeDocument*(idx: InvertedIndex, docId: uint64) =
if docId notin idx.docLengths:
return
let docLen = idx.docLengths[docId]
idx.docLengths.del(docId)
dec idx.docCount
idx.totalTerms -= docLen
if idx.docCount > 0:
idx.avgDocLen = float64(idx.totalTerms) / float64(idx.docCount)
for term, postings in idx.postings.mpairs:
var newPostings: seq[PostingEntry] = @[]
for entry in postings:
if entry.docId != docId:
newPostings.add(entry)
postings = newPostings
proc bm25Score*(idx: InvertedIndex, term: string, docId: uint64,
k1: float64 = 1.2, b: float64 = 0.75): float64 =
if term notin idx.postings:
return 0.0
let df = idx.postings[term].len
let n = idx.docCount
if df == 0 or n == 0:
return 0.0
var tf = 0
var found = false
for entry in idx.postings[term]:
if entry.docId == docId:
tf = entry.termFreq
found = true
break
if not found:
return 0.0
let idf = ln((float64(n) - float64(df) + 0.5) / (float64(df) + 0.5) + 1.0)
let docLen = float64(idx.docLengths.getOrDefault(docId, 0))
let tfNorm = (float64(tf) * (k1 + 1.0)) /
(float64(tf) + k1 * (1.0 - b + b * docLen / idx.avgDocLen))
return idf * tfNorm
proc search*(idx: InvertedIndex, query: string, limit: int = 10,
config: TokenizerConfig = defaultTokenizerConfig()): seq[SearchResult] =
let queryTokens = tokenize(query, config)
if queryTokens.len == 0:
return @[]
var docScores = initTable[uint64, float64]()
var docHighlights = initTable[uint64, seq[(int, int)]]()
for token in queryTokens:
if token notin idx.postings:
continue
for entry in idx.postings[token]:
let score = bm25Score(idx, token, entry.docId)
if entry.docId notin docScores:
docScores[entry.docId] = 0.0
docHighlights[entry.docId] = @[]
docScores[entry.docId] += score
for pos in entry.positions:
let start = pos
let stop = pos + token.len
docHighlights[entry.docId].add((start, stop))
var results: seq[SearchResult] = @[]
for docId, score in docScores:
results.add(SearchResult(
docId: docId,
score: score,
highlights: docHighlights.getOrDefault(docId, @[]),
))
results.sort(proc(a, b: SearchResult): int = cmp(b.score, a.score))
if results.len > limit:
results = results[0..<limit]
return results
proc termCount*(idx: InvertedIndex): int = idx.postings.len
proc documentCount*(idx: InvertedIndex): int = idx.docCount
+231
View File
@@ -0,0 +1,231 @@
## Graph Engine — adjacency list storage with graph algorithms
import std/tables
import std/deques
import std/algorithm
import std/math
import std/sets
import std/hashes
type
EdgeId* = distinct uint64
NodeId* = distinct uint64
Edge* = object
id*: EdgeId
src*: NodeId
dst*: NodeId
label*: string
properties*: Table[string, string]
weight*: float64
GraphNode* = object
id*: NodeId
label*: string
properties*: Table[string, string]
AdjacencyEntry* = object
edgeId*: EdgeId
neighbor*: NodeId
weight*: float64
label*: string
Graph* = ref object
nodes*: Table[NodeId, GraphNode]
edges*: Table[EdgeId, Edge]
adjacency*: Table[NodeId, seq[AdjacencyEntry]] # outgoing
reverseAdj*: Table[NodeId, seq[AdjacencyEntry]] # incoming
nextNodeId: uint64
nextEdgeId: uint64
proc `==`*(a, b: EdgeId): bool = uint64(a) == uint64(b)
proc `==`*(a, b: NodeId): bool = uint64(a) == uint64(b)
proc hash*(x: EdgeId): Hash = hash(uint64(x))
proc hash*(x: NodeId): Hash = hash(uint64(x))
proc newGraph*(): Graph =
Graph(
nodes: initTable[NodeId, GraphNode](),
edges: initTable[EdgeId, Edge](),
adjacency: initTable[NodeId, seq[AdjacencyEntry]](),
reverseAdj: initTable[NodeId, seq[AdjacencyEntry]](),
nextNodeId: 1,
nextEdgeId: 1,
)
proc addNode*(g: Graph, label: string, properties: Table[string, string] = initTable[string, string]()): NodeId =
let id = NodeId(g.nextNodeId)
inc g.nextNodeId
g.nodes[id] = GraphNode(id: id, label: label, properties: properties)
g.adjacency[id] = @[]
g.reverseAdj[id] = @[]
return id
proc addEdge*(g: Graph, src, dst: NodeId, label: string = "",
properties: Table[string, string] = initTable[string, string](),
weight: float64 = 1.0): EdgeId =
let id = EdgeId(g.nextEdgeId)
inc g.nextEdgeId
g.edges[id] = Edge(id: id, src: src, dst: dst, label: label,
properties: properties, weight: weight)
g.adjacency[src].add(AdjacencyEntry(edgeId: id, neighbor: dst, weight: weight, label: label))
g.reverseAdj[dst].add(AdjacencyEntry(edgeId: id, neighbor: src, weight: weight, label: label))
return id
proc getNode*(g: Graph, id: NodeId): GraphNode =
g.nodes[id]
proc getEdge*(g: Graph, id: EdgeId): Edge =
g.edges[id]
proc neighbors*(g: Graph, nodeId: NodeId): seq[NodeId] =
result = @[]
for entry in g.adjacency.getOrDefault(nodeId, @[]):
result.add(entry.neighbor)
proc inNeighbors*(g: Graph, nodeId: NodeId): seq[NodeId] =
result = @[]
for entry in g.reverseAdj.getOrDefault(nodeId, @[]):
result.add(entry.neighbor)
proc removeNode*(g: Graph, nodeId: NodeId) =
if nodeId notin g.nodes:
return
for entry in g.adjacency.getOrDefault(nodeId, @[]):
g.edges.del(entry.edgeId)
var newRev: seq[AdjacencyEntry] = @[]
for rev in g.reverseAdj.getOrDefault(entry.neighbor, @[]):
if rev.neighbor != nodeId:
newRev.add(rev)
g.reverseAdj[entry.neighbor] = newRev
for entry in g.reverseAdj.getOrDefault(nodeId, @[]):
g.edges.del(entry.edgeId)
var newAdj: seq[AdjacencyEntry] = @[]
for adj in g.adjacency.getOrDefault(entry.neighbor, @[]):
if adj.neighbor != nodeId:
newAdj.add(adj)
g.adjacency[entry.neighbor] = newAdj
g.nodes.del(nodeId)
g.adjacency.del(nodeId)
g.reverseAdj.del(nodeId)
proc bfs*(g: Graph, start: NodeId, maxDepth: int = -1): seq[NodeId] =
result = @[]
var visited = initHashSet[NodeId]()
var queue = initDeque[(NodeId, int)]()
queue.addLast((start, 0))
visited.incl(start)
while queue.len > 0:
let (node, depth) = queue.popFirst()
result.add(node)
if maxDepth >= 0 and depth >= maxDepth:
continue
for neighbor in g.neighbors(node):
if neighbor notin visited:
visited.incl(neighbor)
queue.addLast((neighbor, depth + 1))
proc dfs*(g: Graph, start: NodeId, maxDepth: int = -1): seq[NodeId] =
result = @[]
var visited = initHashSet[NodeId]()
var stack: seq[(NodeId, int)] = @[(start, 0)]
while stack.len > 0:
let (node, depth) = stack.pop()
if node in visited:
continue
visited.incl(node)
result.add(node)
if maxDepth >= 0 and depth >= maxDepth:
continue
for neighbor in g.neighbors(node):
if neighbor notin visited:
stack.add((neighbor, depth + 1))
proc shortestPath*(g: Graph, start, target: NodeId): seq[NodeId] =
var visited = initHashSet[NodeId]()
var parent = initTable[NodeId, NodeId]()
var queue = initDeque[NodeId]()
queue.addLast(start)
visited.incl(start)
while queue.len > 0:
let node = queue.popFirst()
if node == target:
var path: seq[NodeId] = @[target]
var current = target
while current in parent:
current = parent[current]
path.add(current)
path.reverse()
return path
for neighbor in g.neighbors(node):
if neighbor notin visited:
visited.incl(neighbor)
parent[neighbor] = node
queue.addLast(neighbor)
return @[]
proc dijkstra*(g: Graph, start: NodeId): Table[NodeId, float64] =
result = initTable[NodeId, float64]()
var visited = initHashSet[NodeId]()
result[start] = 0.0
while true:
var bestNode: NodeId
var bestDist = Inf
for nodeId, dist in result:
if nodeId notin visited and dist < bestDist:
bestDist = dist
bestNode = nodeId
if bestDist == Inf:
break
visited.incl(bestNode)
for entry in g.adjacency.getOrDefault(bestNode, @[]):
let newDist = bestDist + entry.weight
if entry.neighbor notin result or newDist < result[entry.neighbor]:
result[entry.neighbor] = newDist
proc pageRank*(g: Graph, iterations: int = 20, dampingFactor: float64 = 0.85): Table[NodeId, float64] =
result = initTable[NodeId, float64]()
let n = g.nodes.len
if n == 0:
return
let initialRank = 1.0 / float64(n)
for nodeId in g.nodes.keys:
result[nodeId] = initialRank
for iter in 0..<iterations:
var newRanks = initTable[NodeId, float64]()
var danglingSum: float64 = 0
for nodeId in g.nodes.keys:
let outDegree = g.adjacency.getOrDefault(nodeId, @[]).len
if outDegree == 0:
danglingSum += result[nodeId]
for nodeId in g.nodes.keys:
var rank = (1.0 - dampingFactor) / float64(n)
rank += dampingFactor * danglingSum / float64(n)
for entry in g.reverseAdj.getOrDefault(nodeId, @[]):
let srcOutDegree = g.adjacency.getOrDefault(entry.neighbor, @[]).len
if srcOutDegree > 0:
rank += dampingFactor * result[entry.neighbor] / float64(srcOutDegree)
newRanks[nodeId] = rank
result = newRanks
proc nodeCount*(g: Graph): int = g.nodes.len
proc edgeCount*(g: Graph): int = g.edges.len
+318
View File
@@ -0,0 +1,318 @@
## BaraQL AST — Abstract Syntax Tree nodes
import ../core/types
type
NodeKind* = enum
# Statements
nkSelect
nkInsert
nkUpdate
nkDelete
nkCreateType
nkDropType
nkAlterType
nkCreateIndex
nkDropIndex
# Clauses
nkFrom
nkWhere
nkOrderBy
nkGroupBy
nkHaving
nkLimit
nkOffset
nkReturning
nkWith
# Expressions
nkBinOp
nkUnaryOp
nkFuncCall
nkTypeCast
nkPath
nkIdent
nkIntLit
nkFloatLit
nkStringLit
nkBoolLit
nkNullLit
nkArrayLit
nkVectorLit
nkObjectLit
nkIfElse
nkCase
nkSubquery
nkExists
nkInExpr
nkBetweenExpr
nkLikeExpr
nkIsExpr
# Graph-specific
nkGraphTraversal
nkBfsQuery
nkDfsQuery
nkShortestPath
nkPatternMatch
# Vector-specific
nkVectorSimilar
nkVectorNearest
# Join
nkJoin
# Type definitions
nkPropertyDef
nkLinkDef
nkIndexDef
nkConstraintDef
# Top-level
nkStatementList
BinOpKind* = enum
bkAdd = "+"
bkSub = "-"
bkMul = "*"
bkDiv = "/"
bkMod = "%"
bkPow = "**"
bkFloorDiv = "//"
bkEq = "="
bkNotEq = "!="
bkLt = "<"
bkLtEq = "<="
bkGt = ">"
bkGtEq = ">="
bkAnd = "AND"
bkOr = "OR"
bkIn = "IN"
bkNotIn = "NOT IN"
bkLike = "LIKE"
bkILike = "ILIKE"
bkConcat = "++"
bkCoalesce = "??"
bkAssign = ":="
bkArrow = "=>"
UnaryOpKind* = enum
ukNeg = "-"
ukNot = "NOT"
ukIsNull = "IS NULL"
ukIsNotNull = "IS NOT NULL"
JoinKind* = enum
jkInner
jkLeft
jkRight
jkFull
jkCross
SortDir* = enum
sdAsc
sdDesc
Node* = ref object
line*: int
col*: int
case kind*: NodeKind
of nkSelect:
selDistinct*: bool
selWith*: seq[Node]
selResult*: seq[Node]
selFrom*: Node
selJoins*: seq[Node]
selWhere*: Node
selGroupBy*: seq[Node]
selHaving*: Node
selOrderBy*: seq[Node]
selLimit*: Node
selOffset*: Node
of nkInsert:
insTarget*: string
insFields*: seq[Node]
insValues*: seq[Node]
insReturning*: seq[Node]
insConflict*: Node
of nkUpdate:
updTarget*: string
updAlias*: string
updSet*: seq[Node]
updWhere*: Node
updReturning*: seq[Node]
of nkDelete:
delTarget*: string
delAlias*: string
delWhere*: Node
delReturning*: seq[Node]
of nkCreateType:
ctName*: string
ctBases*: seq[string]
ctProperties*: seq[Node]
ctLinks*: seq[Node]
of nkDropType:
dtName*: string
of nkAlterType:
atName*: string
atOps*: seq[Node]
of nkCreateIndex:
ciTarget*: string
ciName*: string
ciExpr*: Node
ciKind*: IndexKind
of nkDropIndex:
diName*: string
of nkFrom:
fromTable*: string
fromAlias*: string
of nkWhere:
whereExpr*: Node
of nkOrderBy:
orderByExpr*: Node
orderByDir*: SortDir
of nkGroupBy:
groupExprs*: seq[Node]
of nkHaving:
havingExpr*: Node
of nkLimit:
limitExpr*: Node
of nkOffset:
offsetExpr*: Node
of nkReturning:
retExprs*: seq[Node]
of nkWith:
withBindings*: seq[(string, Node)]
of nkBinOp:
binOp*: BinOpKind
binLeft*: Node
binRight*: Node
of nkUnaryOp:
unOp*: UnaryOpKind
unOperand*: Node
of nkFuncCall:
funcName*: string
funcArgs*: seq[Node]
of nkTypeCast:
castType*: string
castExpr*: Node
of nkPath:
pathParts*: seq[string]
of nkIdent:
identName*: string
of nkIntLit:
intVal*: int64
of nkFloatLit:
floatVal*: float64
of nkStringLit:
strVal*: string
of nkBoolLit:
boolVal*: bool
of nkNullLit:
discard
of nkArrayLit:
arrayElems*: seq[Node]
of nkVectorLit:
vecElems*: seq[Node]
of nkObjectLit:
objFields*: seq[(string, Node)]
of nkIfElse:
ifCond*: Node
ifThen*: Node
ifElse*: Node
of nkCase:
caseExpr*: Node
caseWhens*: seq[(Node, Node)]
caseElse*: Node
of nkSubquery:
subQuery*: Node
of nkExists:
existsExpr*: Node
of nkInExpr:
inLeft*: Node
inRight*: Node
of nkBetweenExpr:
betweenExpr*: Node
betweenLow*: Node
betweenHigh*: Node
of nkLikeExpr:
likeExpr*: Node
likePattern*: Node
likeCaseInsensitive*: bool
of nkIsExpr:
isExpr*: Node
isType*: string
isNegated*: bool
of nkGraphTraversal:
gtStart*: Node
gtEdge*: string
gtDirection*: string
gtEnd*: Node
gtMaxDepth*: int
of nkBfsQuery:
bfsStart*: Node
bfsTarget*: Node
bfsEdge*: string
bfsMaxDepth*: int
bfsFilter*: Node
of nkDfsQuery:
dfsStart*: Node
dfsTarget*: Node
dfsEdge*: string
dfsMaxDepth*: int
dfsFilter*: Node
of nkShortestPath:
spStart*: Node
spEnd*: Node
spEdge*: string
spMaxDepth*: int
of nkPatternMatch:
pmPattern*: Node
pmWhere*: Node
of nkVectorSimilar:
vsField*: string
vsVector*: Node
vsLimit*: int
vsMetric*: string
of nkVectorNearest:
vnField*: string
vnVector*: Node
vnLimit*: int
vnMetric*: string
of nkJoin:
joinKind*: JoinKind
joinTarget*: Node
joinOn*: Node
joinAlias*: string
of nkPropertyDef:
pdName*: string
pdType*: string
pdRequired*: bool
pdDefault*: Node
pdComputed*: bool
pdExpr*: Node
of nkLinkDef:
ldName*: string
ldTarget*: string
ldCardinality*: Cardinality
ldRequired*: bool
of nkIndexDef:
idName*: string
idExpr*: Node
idKind*: IndexKind
of nkConstraintDef:
cdName*: string
cdExpr*: Node
of nkStatementList:
stmts*: seq[Node]
proc newNode*(kind: NodeKind, line, col: int = 0): Node =
result = Node(kind: kind, line: line, col: col)
case kind
of nkSelect: result.selResult = @[]
of nkInsert: result.insFields = @[]; result.insValues = @[]
of nkUpdate: result.updSet = @[]
of nkDelete: discard
of nkStatementList: result.stmts = @[]
else: discard
+461
View File
@@ -0,0 +1,461 @@
## BaraQL Lexer — tokenization
import std/tables
import std/strutils
type
TokenKind* = enum
# Literals
tkIntLit
tkFloatLit
tkStringLit
tkBoolLit
# Identifiers
tkIdent
# Keywords
tkSelect
tkInsert
tkUpdate
tkDelete
tkFrom
tkWhere
tkAnd
tkOr
tkNot
tkIn
tkIs
tkAs
tkOn
tkJoin
tkLeft
tkRight
tkInner
tkOuter
tkFull
tkCross
tkOrder
tkBy
tkAsc
tkDesc
tkGroup
tkHaving
tkLimit
tkOffset
tkSet
tkInto
tkValues
tkCreate
tkDrop
tkAlter
tkTable
tkIndex
tkType
tkLink
tkProperty
tkRequired
tkMulti
tkSingle
tkTrue
tkFalse
tkNull
tkIf
tkThen
tkElse
tkEnd
tkCase
tkWhen
tkWith
tkDistinct
tkUnion
tkIntersect
tkExcept
tkExists
tkBetween
tkLike
tkILike
tkReturning
tkCount
tkSum
tkAvg
tkMin
tkMax
tkArray
tkVector
tkGraph
tkDocument
tkSimilar
tkNearest
tkTo
tkBfs
tkDfs
tkPath
# Operators
tkPlus
tkMinus
tkStar
tkSlash
tkPercent
tkPower
tkEq
tkNotEq
tkLt
tkLtEq
tkGt
tkGtEq
tkAssign
tkArrow
tkDoubleColon
tkDot
tkComma
tkSemicolon
tkLParen
tkRParen
tkLBrace
tkRbrace
tkLBracket
tkRBracket
tkAmp
tkPipe
tkTilde
tkConcat
tkCoalesce
tkFloorDiv
# Special
tkEof
tkNewline
tkInvalid
Token* = object
kind*: TokenKind
value*: string
line*: int
col*: int
Lexer* = object
input: string
pos: int
line: int
col: int
const keywords*: Table[string, TokenKind] = {
"select": tkSelect,
"insert": tkInsert,
"update": tkUpdate,
"delete": tkDelete,
"from": tkFrom,
"where": tkWhere,
"and": tkAnd,
"or": tkOr,
"not": tkNot,
"in": tkIn,
"is": tkIs,
"as": tkAs,
"on": tkOn,
"join": tkJoin,
"left": tkLeft,
"right": tkRight,
"inner": tkInner,
"outer": tkOuter,
"full": tkFull,
"cross": tkCross,
"order": tkOrder,
"by": tkBy,
"asc": tkAsc,
"desc": tkDesc,
"group": tkGroup,
"having": tkHaving,
"limit": tkLimit,
"offset": tkOffset,
"set": tkSet,
"into": tkInto,
"values": tkValues,
"create": tkCreate,
"drop": tkDrop,
"alter": tkAlter,
"table": tkTable,
"index": tkIndex,
"type": tkType,
"link": tkLink,
"property": tkProperty,
"required": tkRequired,
"multi": tkMulti,
"single": tkSingle,
"true": tkTrue,
"false": tkFalse,
"null": tkNull,
"if": tkIf,
"then": tkThen,
"else": tkElse,
"end": tkEnd,
"case": tkCase,
"when": tkWhen,
"with": tkWith,
"distinct": tkDistinct,
"union": tkUnion,
"intersect": tkIntersect,
"except": tkExcept,
"exists": tkExists,
"between": tkBetween,
"like": tkLike,
"ilike": tkILike,
"returning": tkReturning,
"count": tkCount,
"sum": tkSum,
"avg": tkAvg,
"min": tkMin,
"max": tkMax,
"array": tkArray,
"vector": tkVector,
"graph": tkGraph,
"document": tkDocument,
"similar": tkSimilar,
"nearest": tkNearest,
"to": tkTo,
"bfs": tkBfs,
"dfs": tkDfs,
"path": tkPath,
}.toTable
proc newLexer*(input: string): Lexer =
Lexer(input: input, pos: 0, line: 1, col: 1)
proc peek(l: Lexer): char =
if l.pos < l.input.len:
return l.input[l.pos]
return '\0'
proc advance(l: var Lexer): char =
result = l.input[l.pos]
inc l.pos
if result == '\n':
inc l.line
l.col = 1
else:
inc l.col
proc skipWhitespace(l: var Lexer) =
while l.pos < l.input.len and l.input[l.pos] in {' ', '\t', '\r', '\n'}:
discard l.advance()
proc skipLineComment(l: var Lexer) =
while l.pos < l.input.len and l.input[l.pos] != '\n':
discard l.advance()
proc skipBlockComment(l: var Lexer) =
discard l.advance() # skip *
discard l.advance() # skip *
while l.pos < l.input.len - 1:
if l.input[l.pos] == '*' and l.input[l.pos + 1] == '/':
discard l.advance()
discard l.advance()
return
discard l.advance()
proc readString(l: var Lexer, quote: char): string =
result = ""
while l.pos < l.input.len and l.input[l.pos] != quote:
if l.input[l.pos] == '\\':
discard l.advance()
case l.input[l.pos]
of 'n': result.add('\n')
of 't': result.add('\t')
of 'r': result.add('\r')
of '\\': result.add('\\')
of '\'': result.add('\'')
of '"': result.add('"')
else: result.add(l.input[l.pos])
else:
result.add(l.input[l.pos])
discard l.advance()
if l.pos < l.input.len:
discard l.advance() # skip closing quote
proc readNumber(l: var Lexer, startLine, startCol: int): Token =
var numStr = ""
var isFloat = false
while l.pos < l.input.len and (l.input[l.pos] in Digits or l.input[l.pos] == '.'):
if l.input[l.pos] == '.':
isFloat = true
numStr.add(l.input[l.pos])
discard l.advance()
if isFloat:
Token(kind: tkFloatLit, value: numStr, line: startLine, col: startCol)
else:
Token(kind: tkIntLit, value: numStr, line: startLine, col: startCol)
proc readIdent(l: var Lexer, startLine, startCol: int): Token =
var ident = ""
while l.pos < l.input.len and (l.input[l.pos] in IdentChars or l.input[l.pos] in Digits):
ident.add(l.input[l.pos])
discard l.advance()
let lowerIdent = ident.toLower()
if lowerIdent in keywords:
Token(kind: keywords[lowerIdent], value: ident, line: startLine, col: startCol)
elif lowerIdent == "true":
Token(kind: tkBoolLit, value: "true", line: startLine, col: startCol)
elif lowerIdent == "false":
Token(kind: tkBoolLit, value: "false", line: startLine, col: startCol)
else:
Token(kind: tkIdent, value: ident, line: startLine, col: startCol)
proc nextToken*(l: var Lexer): Token =
l.skipWhitespace()
if l.pos >= l.input.len:
return Token(kind: tkEof, line: l.line, col: l.col)
let startLine = l.line
let startCol = l.col
let ch = l.peek()
case ch
of '/':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '/':
l.skipLineComment()
return l.nextToken()
elif l.pos + 1 < l.input.len and l.input[l.pos + 1] == '*':
l.skipBlockComment()
return l.nextToken()
else:
discard l.advance()
return Token(kind: tkSlash, value: "/", line: startLine, col: startCol)
of '+':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '+':
discard l.advance()
discard l.advance()
return Token(kind: tkConcat, value: "++", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkPlus, value: "+", line: startLine, col: startCol)
of '-':
discard l.advance()
return Token(kind: tkMinus, value: "-", line: startLine, col: startCol)
of '*':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '*':
discard l.advance()
discard l.advance()
return Token(kind: tkPower, value: "**", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkStar, value: "*", line: startLine, col: startCol)
of '%':
discard l.advance()
return Token(kind: tkPercent, value: "%", line: startLine, col: startCol)
of '=':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '>':
discard l.advance()
discard l.advance()
return Token(kind: tkArrow, value: "=>", line: startLine, col: startCol)
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=':
discard l.advance()
discard l.advance()
return Token(kind: tkEq, value: "==", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkAssign, value: ":=", line: startLine, col: startCol)
of ':':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=':
discard l.advance()
discard l.advance()
return Token(kind: tkAssign, value: ":=", line: startLine, col: startCol)
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == ':':
discard l.advance()
discard l.advance()
return Token(kind: tkDoubleColon, value: "::", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkInvalid, value: ":", line: startLine, col: startCol)
of '!':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=':
discard l.advance()
discard l.advance()
return Token(kind: tkNotEq, value: "!=", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkInvalid, value: "!", line: startLine, col: startCol)
of '<':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=':
discard l.advance()
discard l.advance()
return Token(kind: tkLtEq, value: "<=", line: startLine, col: startCol)
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '>':
discard l.advance()
discard l.advance()
return Token(kind: tkNotEq, value: "<>", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkLt, value: "<", line: startLine, col: startCol)
of '>':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=':
discard l.advance()
discard l.advance()
return Token(kind: tkGtEq, value: ">=", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkGt, value: ">", line: startLine, col: startCol)
of '?':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '?':
discard l.advance()
discard l.advance()
return Token(kind: tkCoalesce, value: "??", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkInvalid, value: "?", line: startLine, col: startCol)
of '.':
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '<':
discard l.advance()
discard l.advance()
return Token(kind: tkInvalid, value: ".<", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkDot, value: ".", line: startLine, col: startCol)
of ',':
discard l.advance()
return Token(kind: tkComma, value: ",", line: startLine, col: startCol)
of ';':
discard l.advance()
return Token(kind: tkSemicolon, value: ";", line: startLine, col: startCol)
of '(':
discard l.advance()
return Token(kind: tkLParen, value: "(", line: startLine, col: startCol)
of ')':
discard l.advance()
return Token(kind: tkRParen, value: ")", line: startLine, col: startCol)
of '{':
discard l.advance()
return Token(kind: tkLBrace, value: "{", line: startLine, col: startCol)
of '}':
discard l.advance()
return Token(kind: tkRbrace, value: "}", line: startLine, col: startCol)
of '[':
discard l.advance()
return Token(kind: tkLBracket, value: "[", line: startLine, col: startCol)
of ']':
discard l.advance()
return Token(kind: tkRBracket, value: "]", line: startLine, col: startCol)
of '&':
discard l.advance()
return Token(kind: tkAmp, value: "&", line: startLine, col: startCol)
of '|':
discard l.advance()
return Token(kind: tkPipe, value: "|", line: startLine, col: startCol)
of '~':
discard l.advance()
return Token(kind: tkTilde, value: "~", line: startLine, col: startCol)
of '\'', '"':
discard l.advance()
let s = l.readString(ch)
return Token(kind: tkStringLit, value: s, line: startLine, col: startCol)
of '#':
l.skipLineComment()
return l.nextToken()
else:
if ch in Digits:
return l.readNumber(startLine, startCol)
elif ch in IdentStartChars:
return l.readIdent(startLine, startCol)
else:
discard l.advance()
return Token(kind: tkInvalid, value: $ch, line: startLine, col: startCol)
proc tokenize*(input: string): seq[Token] =
var lexer = newLexer(input)
result = @[]
while true:
let tok = lexer.nextToken()
result.add(tok)
if tok.kind == tkEof:
break
+245
View File
@@ -0,0 +1,245 @@
## BaraQL Parser — recursive descent parser
import std/strutils
import lexer
import ast
type
Parser* = object
tokens: seq[Token]
pos: int
proc newParser*(tokens: seq[Token]): Parser =
Parser(tokens: tokens, pos: 0)
proc peek(p: Parser): Token =
if p.pos < p.tokens.len:
return p.tokens[p.pos]
Token(kind: tkEof)
proc advance(p: var Parser): Token =
result = p.tokens[p.pos]
inc p.pos
proc expect(p: var Parser, kind: TokenKind): Token =
let tok = p.advance()
if tok.kind != kind:
raise newException(ValueError,
"Expected " & $kind & " but got " & $tok.kind & " at line " & $tok.line)
return tok
proc match(p: var Parser, kind: TokenKind): bool =
if p.peek().kind == kind:
discard p.advance()
return true
return false
proc parseExpr(p: var Parser): Node
proc parseSelect(p: var Parser): Node
proc parsePrimary(p: var Parser): Node =
let tok = p.peek()
case tok.kind
of tkIntLit:
discard p.advance()
Node(kind: nkIntLit, intVal: parseInt(tok.value), line: tok.line, col: tok.col)
of tkFloatLit:
discard p.advance()
Node(kind: nkFloatLit, floatVal: parseFloat(tok.value), line: tok.line, col: tok.col)
of tkStringLit:
discard p.advance()
Node(kind: nkStringLit, strVal: tok.value, line: tok.line, col: tok.col)
of tkBoolLit:
discard p.advance()
Node(kind: nkBoolLit, boolVal: tok.value == "true", line: tok.line, col: tok.col)
of tkNull:
discard p.advance()
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)
of tkLParen:
discard p.advance()
let expr = p.parseExpr()
discard p.expect(tkRParen)
expr
of tkLBracket:
discard p.advance()
var elems: seq[Node] = @[]
if p.peek().kind != tkRBracket:
elems.add(p.parseExpr())
while p.match(tkComma):
elems.add(p.parseExpr())
discard p.expect(tkRBracket)
Node(kind: nkArrayLit, arrayElems: elems, line: tok.line, col: tok.col)
of tkSelect:
p.parseSelect()
of tkExists:
discard p.advance()
discard p.expect(tkLParen)
let sub = p.parseSelect()
discard p.expect(tkRParen)
Node(kind: nkExists, existsExpr: sub, line: tok.line, col: tok.col)
of tkNot:
discard p.advance()
let operand = p.parsePrimary()
Node(kind: nkUnaryOp, unOp: ukNot, unOperand: operand, line: tok.line, col: tok.col)
of tkMinus:
discard p.advance()
let operand = p.parsePrimary()
Node(kind: nkUnaryOp, unOp: ukNeg, unOperand: operand, line: tok.line, col: tok.col)
of tkCount:
discard p.advance()
discard p.expect(tkLParen)
var args: seq[Node] = @[]
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)
else:
discard p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
proc parseMulDiv(p: var Parser): Node =
result = p.parsePrimary()
while p.peek().kind in {tkStar, tkSlash, tkPercent, tkFloorDiv}:
let op = case p.peek().kind
of tkStar: bkMul
of tkSlash: bkDiv
of tkPercent: bkMod
of tkFloorDiv: bkFloorDiv
else: bkMul
let tok = p.advance()
let right = p.parsePrimary()
result = Node(kind: nkBinOp, binOp: op, binLeft: result, binRight: right,
line: tok.line, col: tok.col)
proc parseAddSub(p: var Parser): Node =
result = p.parseMulDiv()
while p.peek().kind in {tkPlus, tkMinus, tkConcat}:
let op = case p.peek().kind
of tkPlus: bkAdd
of tkMinus: bkSub
of tkConcat: bkConcat
else: bkAdd
let tok = p.advance()
let right = p.parseMulDiv()
result = Node(kind: nkBinOp, binOp: op, binLeft: result, binRight: right,
line: tok.line, col: tok.col)
proc parseComparison(p: var Parser): Node =
result = p.parseAddSub()
while p.peek().kind in {tkEq, tkNotEq, tkLt, tkLtEq, tkGt, tkGtEq}:
let op = case p.peek().kind
of tkEq: bkEq
of tkNotEq: bkNotEq
of tkLt: bkLt
of tkLtEq: bkLtEq
of tkGt: bkGt
of tkGtEq: bkGtEq
else: bkEq
let tok = p.advance()
let right = p.parseAddSub()
result = Node(kind: nkBinOp, binOp: op, binLeft: result, binRight: right,
line: tok.line, col: tok.col)
proc parseNot(p: var Parser): Node =
if p.peek().kind == tkNot:
let tok = p.advance()
let operand = p.parseComparison()
return Node(kind: nkUnaryOp, unOp: ukNot, unOperand: operand,
line: tok.line, col: tok.col)
return p.parseComparison()
proc parseAnd(p: var Parser): Node =
result = p.parseNot()
while p.peek().kind == tkAnd:
let tok = p.advance()
let right = p.parseNot()
result = Node(kind: nkBinOp, binOp: bkAnd, binLeft: result, binRight: right,
line: tok.line, col: tok.col)
proc parseOr(p: var Parser): Node =
result = p.parseAnd()
while p.peek().kind == tkOr:
let tok = p.advance()
let right = p.parseAnd()
result = Node(kind: nkBinOp, binOp: bkOr, binLeft: result, binRight: right,
line: tok.line, col: tok.col)
proc parseExpr(p: var Parser): Node =
return p.parseOr()
proc parseSelect(p: var Parser): Node =
let tok = p.expect(tkSelect)
result = Node(kind: nkSelect, line: tok.line, col: tok.col)
if p.peek().kind == tkDistinct:
discard p.advance()
result.selDistinct = true
result.selResult = @[]
result.selResult.add(p.parseExpr())
while p.match(tkComma):
result.selResult.add(p.parseExpr())
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)
if p.match(tkWhere):
result.selWhere = Node(kind: nkWhere, whereExpr: p.parseExpr())
if p.match(tkLimit):
result.selLimit = Node(kind: nkLimit, limitExpr: p.parseExpr())
if p.match(tkOffset):
result.selOffset = Node(kind: nkOffset, offsetExpr: p.parseExpr())
proc parseInsert(p: var Parser): Node =
let tok = p.expect(tkInsert)
let target = p.expect(tkIdent).value
result = Node(kind: nkInsert, insTarget: target, line: tok.line, col: tok.col)
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)
proc parseDelete(p: var Parser): Node =
let tok = p.expect(tkDelete)
let target = p.expect(tkIdent).value
result = Node(kind: nkDelete, delTarget: target, line: tok.line, col: tok.col)
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)
proc parseStatement*(p: var Parser): Node =
case p.peek().kind
of tkSelect: p.parseSelect()
of tkInsert: p.parseInsert()
of tkUpdate: p.parseUpdate()
of tkDelete: p.parseDelete()
of tkCreate: p.parseCreateType()
else:
let tok = p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
proc parse*(tokens: seq[Token]): Node =
var parser = newParser(tokens)
result = Node(kind: nkStatementList)
while parser.peek().kind != tkEof:
result.stmts.add(parser.parseStatement())
discard parser.match(tkSemicolon)
proc parse*(input: string): Node =
let tokens = tokenize(input)
parse(tokens)
+51
View File
@@ -0,0 +1,51 @@
## Bloom filter — probabilistic set membership
import std/hashes
import std/math
type
BloomFilter* = object
bits: seq[bool]
numHashes: int
size: int
proc newBloomFilter*(expectedItems: int, fpRate: float = 0.01): BloomFilter =
let size = int(-float(expectedItems) * ln(fpRate) / (ln(2.0) * ln(2.0)))
let numHashes = int(float(size) / float(expectedItems) * ln(2.0))
BloomFilter(
bits: newSeq[bool](max(size, 64)),
numHashes: max(numHashes, 1),
size: max(size, 64),
)
proc hash1*(bf: BloomFilter, data: openArray[byte]): uint64 =
var h: Hash = 0
for b in data:
h = h !& Hash(b)
result = uint64(!$h)
proc hash2*(bf: BloomFilter, data: openArray[byte]): uint64 =
var h: Hash = 5381
for b in data:
h = ((h shl 5) + h) + Hash(b)
result = uint64(h)
proc getHashes(bf: BloomFilter, data: openArray[byte]): seq[int] =
let h1 = bf.hash1(data)
let h2 = bf.hash2(data)
result = newSeq[int](bf.numHashes)
for i in 0..<bf.numHashes:
result[i] = int((h1 + uint64(i) * h2) mod uint64(bf.size))
proc add*(bf: var BloomFilter, data: openArray[byte]) =
for idx in bf.getHashes(data):
bf.bits[idx] = true
proc contains*(bf: BloomFilter, data: openArray[byte]): bool =
for idx in bf.getHashes(data):
if not bf.bits[idx]:
return false
return true
proc clear*(bf: var BloomFilter) =
for i in 0..<bf.size:
bf.bits[i] = false
+144
View File
@@ -0,0 +1,144 @@
## LSM-Tree Storage Engine — core key-value store
import std/algorithm
import std/os
import std/hashes
import std/tables
import std/monotimes
import bloom
import wal
const
SSTableMagic* = 0x53535442'u32 # "SSTB"
DefaultMemTableSize* = 4 * 1024 * 1024 # 4MB
DefaultBloomFpRate* = 0.01
type
Entry* = object
key*: string
value*: seq[byte]
timestamp*: uint64
deleted*: bool
MemTable* = object
entries: seq[Entry]
size: int
maxSize: int
SSTable* = object
path*: string
index: Table[string, int64] # key -> file offset
bloom: BloomFilter
level: int
minKey: string
maxKey: string
entryCount: int
LSMTree* = object
dir: string
memTable: MemTable
immutableMem: MemTable
sstables: seq[SSTable]
wal: WriteAheadLog
memMaxSize: int
currentSeq: uint64
readLocks: int
proc newMemTable(maxSize: int = DefaultMemTableSize): MemTable =
MemTable(entries: @[], size: 0, maxSize: maxSize)
proc len*(mt: MemTable): int = mt.entries.len
proc put*(mt: var MemTable, key: string, value: seq[byte], timestamp: uint64, deleted: bool = false): bool =
let entrySize = key.len + value.len + 16
if mt.size + entrySize > mt.maxSize and mt.entries.len > 0:
return false
let entry = Entry(key: key, value: value, timestamp: timestamp, deleted: deleted)
let pos = mt.entries.lowerBound(entry, proc(a, b: Entry): int = cmp(a.key, b.key))
if pos < mt.entries.len and mt.entries[pos].key == key:
mt.entries[pos] = entry
else:
mt.entries.insert(entry, pos)
mt.size += entrySize
return true
proc get*(mt: MemTable, key: string): (bool, Entry) =
for entry in mt.entries:
if entry.key == key:
return (true, entry)
return (false, Entry())
proc scan*(mt: MemTable, startKey, endKey: string): seq[Entry] =
result = @[]
for entry in mt.entries:
if entry.key >= startKey and entry.key <= endKey:
result.add(entry)
proc clear*(mt: var MemTable) =
mt.entries.setLen(0)
mt.size = 0
proc newLSMTree*(dir: string, memMaxSize: int = DefaultMemTableSize): LSMTree =
createDir(dir)
createDir(dir / "sstables")
LSMTree(
dir: dir,
memTable: newMemTable(memMaxSize),
immutableMem: newMemTable(0),
sstables: @[],
wal: newWriteAheadLog(dir / "wal"),
memMaxSize: memMaxSize,
currentSeq: 0,
readLocks: 0,
)
proc put*(db: var LSMTree, key: string, value: seq[byte]) =
let ts = uint64(getMonoTime().ticks())
db.wal.writePut(cast[seq[byte]](key), value, ts)
if not db.memTable.put(key, value, ts):
db.immutableMem = db.memTable
db.memTable = newMemTable(db.memMaxSize)
discard db.memTable.put(key, value, ts)
proc delete*(db: var LSMTree, key: string) =
let ts = uint64(getMonoTime().ticks())
db.wal.writeDelete(cast[seq[byte]](key), ts)
discard db.memTable.put(key, @[], ts, deleted = true)
proc get*(db: LSMTree, key: string): (bool, seq[byte]) =
let (found, entry) = db.memTable.get(key)
if found:
if entry.deleted:
return (false, @[])
return (true, entry.value)
let (found2, entry2) = db.immutableMem.get(key)
if found2:
if entry2.deleted:
return (false, @[])
return (true, entry2.value)
for sst in db.sstables:
if key in sst.index:
return (true, @[]) # placeholder for SSTable read
return (false, @[])
proc contains*(db: LSMTree, key: string): bool =
let (found, _) = db.get(key)
return found
proc flush*(db: var LSMTree) =
if db.memTable.len == 0:
return
db.immutableMem = db.memTable
db.memTable = newMemTable(db.memMaxSize)
db.wal.writeCommit(uint64(getMonoTime().ticks()))
db.wal.sync()
proc close*(db: var LSMTree) =
db.flush()
db.wal.close()
proc memTableSize*(db: LSMTree): int = db.memTable.len
proc sstableCount*(db: LSMTree): int = db.sstables.len
proc dir*(db: LSMTree): string = db.dir
+84
View File
@@ -0,0 +1,84 @@
## WAL — Write-Ahead Log for durability
import std/os
import std/streams
const
WALMagic* = 0x42415241'u32 # "BARA"
WALVersion* = 1'u32
type
WalEntryKind* = enum
wekPut = 1
wekDelete = 2
wekCheckpoint = 3
wekCommit = 4
WalEntry* = object
kind*: WalEntryKind
timestamp*: uint64
key*: seq[byte]
value*: seq[byte]
WriteAheadLog* = object
path: string
stream: FileStream
entryCount: uint64
syncOnWrite: bool
proc newWriteAheadLog*(dir: string, syncOnWrite: bool = true): WriteAheadLog =
createDir(dir)
let path = dir / "wal.log"
let stream = newFileStream(path, fmWrite)
if stream == nil:
raise newException(IOError, "Cannot open WAL: " & path)
stream.write(WALMagic)
stream.write(WALVersion)
stream.flush()
WriteAheadLog(path: path, stream: stream, entryCount: 0, syncOnWrite: syncOnWrite)
proc writeEntry*(wal: var WriteAheadLog, entry: WalEntry) =
wal.stream.write(uint8(entry.kind))
wal.stream.write(entry.timestamp)
wal.stream.write(uint32(entry.key.len))
if entry.key.len > 0:
wal.stream.writeData(unsafeAddr entry.key[0], entry.key.len)
wal.stream.write(uint32(entry.value.len))
if entry.value.len > 0:
wal.stream.writeData(unsafeAddr entry.value[0], entry.value.len)
if wal.syncOnWrite:
wal.stream.flush()
inc wal.entryCount
proc writePut*(wal: var WriteAheadLog, key, value: openArray[byte], timestamp: uint64) =
wal.writeEntry(WalEntry(
kind: wekPut,
timestamp: timestamp,
key: @key,
value: @value,
))
proc writeDelete*(wal: var WriteAheadLog, key: openArray[byte], timestamp: uint64) =
wal.writeEntry(WalEntry(
kind: wekDelete,
timestamp: timestamp,
key: @key,
value: @[],
))
proc writeCommit*(wal: var WriteAheadLog, timestamp: uint64) =
wal.writeEntry(WalEntry(
kind: wekCommit,
timestamp: timestamp,
key: @[],
value: @[],
))
proc sync*(wal: var WriteAheadLog) =
wal.stream.flush()
proc close*(wal: var WriteAheadLog) =
wal.stream.flush()
wal.stream.close()
proc entryCount*(wal: WriteAheadLog): uint64 = wal.entryCount
proc path*(wal: WriteAheadLog): string = wal.path
+210
View File
@@ -0,0 +1,210 @@
## Vector Engine — HNSW and IVF-PQ indexes for vector similarity search
import std/math
import std/algorithm
import std/random
import std/tables
type
DistanceMetric* = enum
dmCosine = "cosine"
dmEuclidean = "euclidean"
dmDotProduct = "dot_product"
dmManhattan = "manhattan"
Vector* = seq[float32]
VectorEntry* = object
id*: uint64
vector*: Vector
metadata*: seq[(string, string)]
HNSWNode* = ref object
id*: uint64
vector*: Vector
neighbors*: seq[seq[uint64]] # neighbors per level
HNSWIndex* = ref object
nodes*: Table[uint64, HNSWNode]
entryPoint*: uint64
maxLevel*: int
efConstruction*: int
m*: int
maxM*: int
metric*: DistanceMetric
dimensions*: int
IVFCluster* = object
centroid*: Vector
entries*: seq[VectorEntry]
IVFPQIndex* = ref object
clusters*: seq[IVFCluster]
nClusters*: int
nSubquantizers*: int
nBits*: int
metric*: DistanceMetric
dimensions*: int
proc cosineDistance*(a, b: Vector): float64 =
var dot, normA, normB: float64
for i in 0..<min(a.len, b.len):
dot += float64(a[i]) * float64(b[i])
normA += float64(a[i]) * float64(a[i])
normB += float64(b[i]) * float64(b[i])
if normA == 0 or normB == 0:
return 1.0
return 1.0 - dot / (sqrt(normA) * sqrt(normB))
proc euclideanDistance*(a, b: Vector): float64 =
var sum: float64
for i in 0..<min(a.len, b.len):
let diff = float64(a[i]) - float64(b[i])
sum += diff * diff
return sqrt(sum)
proc dotProduct*(a, b: Vector): float64 =
var sum: float64
for i in 0..<min(a.len, b.len):
sum += float64(a[i]) * float64(b[i])
return -sum # negative because we want to minimize
proc manhattanDistance*(a, b: Vector): float64 =
var sum: float64
for i in 0..<min(a.len, b.len):
sum += abs(float64(a[i]) - float64(b[i]))
return sum
proc distance*(a, b: Vector, metric: DistanceMetric): float64 =
case metric
of dmCosine: cosineDistance(a, b)
of dmEuclidean: euclideanDistance(a, b)
of dmDotProduct: dotProduct(a, b)
of dmManhattan: manhattanDistance(a, b)
proc newHNSWIndex*(dimensions: int, m: int = 16, efConstruction: int = 200,
metric: DistanceMetric = dmCosine): HNSWIndex =
HNSWIndex(
nodes: initTable[uint64, HNSWNode](),
entryPoint: 0,
maxLevel: 0,
efConstruction: efConstruction,
m: m,
maxM: m * 2,
metric: metric,
dimensions: dimensions,
)
proc randomLevel(maxLevel: int): int =
var level = 0
var r = rand(1.0)
while r < 0.5 and level < maxLevel:
inc level
r = rand(1.0)
return level
proc insert*(idx: HNSWIndex, id: uint64, vector: Vector) =
let node = HNSWNode(id: id, vector: vector, neighbors: @[])
let level = randomLevel(16)
for i in 0..level:
node.neighbors.add(@[])
idx.nodes[id] = node
if idx.entryPoint == 0:
idx.entryPoint = id
idx.maxLevel = level
return
if level > idx.maxLevel:
idx.entryPoint = id
idx.maxLevel = level
proc search*(idx: HNSWIndex, query: Vector, k: int,
metric: DistanceMetric = dmCosine): seq[(uint64, float64)] =
if idx.nodes.len == 0:
return @[]
var candidates: seq[(uint64, float64)] = @[]
for nodeId, node in idx.nodes:
let dist = distance(query, node.vector, metric)
candidates.add((nodeId, dist))
candidates.sort(proc(a, b: (uint64, float64)): int = cmp(a[1], b[1]))
if candidates.len > k:
candidates = candidates[0..<k]
return candidates
proc newIVFPQIndex*(dimensions: int, nClusters: int = 100,
nSubquantizers: int = 8, nBits: int = 8,
metric: DistanceMetric = dmCosine): IVFPQIndex =
IVFPQIndex(
clusters: newSeq[IVFCluster](nClusters),
nClusters: nClusters,
nSubquantizers: nSubquantizers,
nBits: nBits,
metric: metric,
dimensions: dimensions,
)
proc train*(idx: IVFPQIndex, data: seq[VectorEntry], nIterations: int = 10) =
if data.len == 0:
return
for i in 0..<idx.nClusters:
idx.clusters[i].centroid = data[i mod data.len].vector
for iter in 0..<nIterations:
for i in 0..<idx.nClusters:
idx.clusters[i].entries.setLen(0)
for entry in data:
var bestCluster = 0
var bestDist = Inf
for ci in 0..<idx.nClusters:
let dist = distance(entry.vector, idx.clusters[ci].centroid, idx.metric)
if dist < bestDist:
bestDist = dist
bestCluster = ci
idx.clusters[bestCluster].entries.add(entry)
for i in 0..<idx.nClusters:
if idx.clusters[i].entries.len == 0:
continue
var newCentroid = newSeq[float32](idx.dimensions)
for entry in idx.clusters[i].entries:
for d in 0..<idx.dimensions:
newCentroid[d] += entry.vector[d]
for d in 0..<idx.dimensions:
newCentroid[d] /= float32(idx.clusters[i].entries.len)
idx.clusters[i].centroid = newCentroid
proc search*(idx: IVFPQIndex, query: Vector, k: int, nProbe: int = 10,
metric: DistanceMetric = dmCosine): seq[(uint64, float64)] =
var clusterDists: seq[(int, float64)] = @[]
for ci in 0..<idx.nClusters:
let dist = distance(query, idx.clusters[ci].centroid, metric)
clusterDists.add((ci, dist))
clusterDists.sort(proc(a, b: (int, float64)): int = cmp(a[1], b[1]))
var candidates: seq[(uint64, float64)] = @[]
let probeCount = min(nProbe, idx.nClusters)
for i in 0..<probeCount:
let ci = clusterDists[i][0]
for entry in idx.clusters[ci].entries:
let dist = distance(query, entry.vector, metric)
candidates.add((entry.id, dist))
candidates.sort(proc(a, b: (uint64, float64)): int = cmp(a[1], b[1]))
if candidates.len > k:
candidates = candidates[0..<k]
return candidates
proc len*(idx: HNSWIndex): int = idx.nodes.len
proc clear*(idx: HNSWIndex) =
idx.nodes.clear()
idx.entryPoint = 0
idx.maxLevel = 0
+15
View File
@@ -0,0 +1,15 @@
## BaraDB — Multimodal Database Engine
## Main entry point
import std/asyncdispatch
import barabadb/core/server
import barabadb/core/config
proc main() =
let config = loadConfig()
echo "BaraDB v0.1.0 — Multimodal Database Engine"
echo "Listening on ", config.address, ":", config.port
var server = newServer(config)
waitFor server.run()
when isMainModule:
main()