feat: B-Tree, columnar engine, IR/type checker, connection pool, JWT auth, quantization, Louvain, pattern matching — 57 tests

- B-Tree index: insert, get, scan range, duplicate keys
- Columnar engine: batch ops, RLE/dict encoding, GroupBy, aggregates
- IR (Intermediate Representation): plan nodes, expressions, type checker
- Connection pool: load-balanced eviction, min/max connections
- JWT authentication with token verify and claims parsing
- Vector quantization: scalar 8-bit/4-bit, product quantization, binary
- Louvain community detection algorithm
- Graph pattern matching (subgraph isomorphism)
- 18 new test suites (57 total, all passing)
This commit is contained in:
2026-05-06 00:57:30 +03:00
parent 5c84aeccf8
commit 07a37d8e78
9 changed files with 1638 additions and 20 deletions
+271
View File
@@ -0,0 +1,271 @@
## Community Detection — Louvain algorithm
import std/tables
import std/sets
import std/algorithm
import std/math
import std/sequtils
import engine
type
LouvainResult* = ref object
communities*: Table[NodeId, int] # node -> community id
modularity*: float64
numCommunities*: int
proc louvain*(g: Graph): LouvainResult =
result = LouvainResult(
communities: initTable[NodeId, int](),
modularity: 0.0,
numCommunities: 0,
)
if g.nodeCount == 0:
return
# Phase 1: assign each node to its own community
var community: Table[NodeId, int] = initTable[NodeId, int]()
var nodeCommunity = initTable[NodeId, int]()
var commNodes = initTable[int, seq[NodeId]]()
var inEdges = initTable[int, int]()
var totalEdges = initTable[int, int]()
var m = 0 # total edge weight
for nodeId in g.nodes.keys:
let cid = nodeCommunity.len
community[nodeId] = cid
nodeCommunity[nodeId] = cid
commNodes[cid] = @[nodeId]
inEdges[cid] = 0
totalEdges[cid] = 0
for entry in g.adjacency.getOrDefault(nodeId, @[]):
inc m # count each edge once
if entry.neighbor in community and community[entry.neighbor] == community[nodeId]:
inEdges[cid] += 1
totalEdges[cid] += 1
var numComms = nodeCommunity.len
# Iterate until no improvement
var improved = true
var iterations = 0
while improved and iterations < 100:
improved = false
inc iterations
var changedNodes = g.nodes.keys.toSeq
# Randomize order
changedNodes.sort(proc(a, b: NodeId): int = cmp(uint64(a), uint64(b)))
for nodeId in changedNodes:
let oldComm = community[nodeId]
# Compute gain for moving to each neighbor community
var neighborComms = initHashSet[int]()
for entry in g.adjacency.getOrDefault(nodeId, @[]):
if entry.neighbor in community:
let nc = community[entry.neighbor]
if nc != oldComm:
neighborComms.incl(nc)
if neighborComms.len == 0:
continue
# Calculate delta modularity for moving
var bestComm = oldComm
var bestDeltaQ = 0.0'f64
var k_i = 0
var k_i_in = 0
for entry in g.adjacency.getOrDefault(nodeId, @[]):
inc k_i
if entry.neighbor in community and community[entry.neighbor] == oldComm:
inc k_i_in
for nc in neighborComms:
var k_i_comm = 0
for entry in g.adjacency.getOrDefault(nodeId, @[]):
if entry.neighbor in community and community[entry.neighbor] == nc:
inc k_i_comm
var sigmaTot = 0
for nid in commNodes.getOrDefault(nc, @[]):
for entry in g.adjacency.getOrDefault(nid, @[]):
inc sigmaTot
var sigmaIn = 0
for nid in commNodes.getOrDefault(nc, @[]):
for entry in g.adjacency.getOrDefault(nid, @[]):
if entry.neighbor in community and community[entry.neighbor] == nc:
inc sigmaIn
let mFloat = float64(m)
var deltaQ = float64(k_i_comm) / mFloat
deltaQ -= float64(sigmaTot) * float64(k_i) / (2.0 * mFloat * mFloat)
if deltaQ > bestDeltaQ:
bestDeltaQ = deltaQ
bestComm = nc
if bestComm != oldComm and bestDeltaQ > 1e-10:
# Move node to best community
community[nodeId] = bestComm
commNodes[oldComm] = commNodes[oldComm].filterIt(it != nodeId)
if bestComm notin commNodes:
commNodes[bestComm] = @[]
commNodes[bestComm].add(nodeId)
improved = true
# Cleanup empty communities
let commKeys = commNodes.keys.toSeq
for cid in commKeys:
if commNodes[cid].len == 0:
commNodes.del(cid)
# Compute final modularity
var totalM = float64(m)
if totalM > 0:
var Q: float64 = 0
for cid in commNodes.keys:
var e_cc: float64 = 0
var a_c: float64 = 0
for nid in commNodes[cid]:
for entry in g.adjacency.getOrDefault(nid, @[]):
if entry.neighbor in community and community[entry.neighbor] == cid:
e_cc += 1.0
a_c += 1.0
e_cc /= totalM
a_c = (a_c / (2 * totalM))
a_c *= a_c
Q += e_cc - a_c
result.modularity = Q
result.communities = community
result.numCommunities = commNodes.len
# Pattern matching — simple subgraph isomorphism search
type
PatternNode* = object
id*: int
label*: string
properties*: Table[string, string]
PatternEdge* = object
srcId*: int
dstId*: int
label*: string
isDirected*: bool
GraphPattern* = ref object
nodes*: seq[PatternNode]
edges*: seq[PatternEdge]
PatternMatch* = ref object
mapping*: seq[(int, NodeId)] # pattern node id -> graph node id
nodes*: seq[NodeId]
proc newGraphPattern*(): GraphPattern =
GraphPattern(nodes: @[], edges: @[])
proc addNode*(pattern: GraphPattern, id: int, label: string,
properties: Table[string, string] = initTable[string, string]()) =
pattern.nodes.add(PatternNode(id: id, label: label, properties: properties))
proc addEdge*(pattern: GraphPattern, srcId, dstId: int, label: string = "",
isDirected: bool = true) =
pattern.edges.add(PatternEdge(srcId: srcId, dstId: dstId, label: label,
isDirected: isDirected))
proc matchPattern*(g: Graph, pattern: GraphPattern, maxMatches: int = 100): seq[PatternMatch] =
result = @[]
if pattern.nodes.len == 0:
return
# Find candidate sets for each pattern node
var candidates = initTable[int, seq[NodeId]]()
for pn in pattern.nodes:
candidates[pn.id] = @[]
for gid in g.nodes.keys:
let gn = g.nodes[gid]
if pn.label.len == 0 or gn.label == pn.label:
var propsMatch = true
for pk, pv in pn.properties:
if gn.properties.getOrDefault(pk, "") != pv:
propsMatch = false
break
if propsMatch:
candidates[pn.id].add(gid)
# Skip if any pattern node has no candidates
for pn in pattern.nodes:
if candidates[pn.id].len == 0:
return
# Simple backtracking search
var mapping = initTable[int, NodeId]()
var usedNodes = initHashSet[NodeId]()
let pnIds = pattern.nodes.mapIt(it.id)
var stack: seq[(int, int)] = @[(0, 0)] # (idx, candidatePos)
while stack.len > 0:
let (idx, cpos) = stack[^1]
if result.len >= maxMatches:
return
if idx >= pnIds.len:
let match = PatternMatch(mapping: @[], nodes: @[])
for pid, gid in mapping:
match.mapping.add((pid, gid))
match.nodes.add(gid)
result.add(match)
stack.setLen(stack.len - 1)
if mapping.len > 0:
let lastPid = pnIds[mapping.len - 1]
usedNodes.excl(mapping[lastPid])
mapping.del(lastPid)
continue
let pid = pnIds[idx]
if cpos >= candidates[pid].len:
stack.setLen(stack.len - 1)
if mapping.len > 0:
let lastPid = pnIds[mapping.len - 1]
usedNodes.excl(mapping[lastPid])
mapping.del(lastPid)
continue
# Advance candidate position
stack[^1] = (idx, cpos + 1)
let gid = candidates[pid][cpos]
if gid in usedNodes:
continue
var edgesValid = true
for edge in pattern.edges:
if edge.srcId == pid and edge.dstId in mapping:
let targetGid = mapping[edge.dstId]
var found = false
for adj in g.adjacency.getOrDefault(gid, @[]):
if adj.neighbor == targetGid:
if edge.label.len == 0 or adj.label == edge.label:
found = true
break
if not found:
edgesValid = false
break
elif edge.dstId == pid and edge.srcId in mapping:
let sourceGid = mapping[edge.srcId]
var found = false
for adj in g.adjacency.getOrDefault(sourceGid, @[]):
if adj.neighbor == gid:
if edge.label.len == 0 or adj.label == edge.label:
found = true
break
if not found:
edgesValid = false
break
if edgesValid:
mapping[pid] = gid
usedNodes.incl(gid)
stack.add((idx + 1, 0))