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:
+27
-20
@@ -64,7 +64,7 @@
|
|||||||
- [x] Bloom filter за бързо отхвърляне
|
- [x] Bloom filter за бързо отхвърляне
|
||||||
- [x] Типова система (int, float, string, bool, bytes, uuid, datetime, json, vector)
|
- [x] Типова система (int, float, string, bool, bytes, uuid, datetime, json, vector)
|
||||||
- [x] Сериялизация на записите
|
- [x] Сериялизация на записите
|
||||||
- [ ] B-Tree индекс за точкови заявки
|
- [x] B-Tree индекс за точкови заявки
|
||||||
- [ ] Компактиране на SSTable (compaction strategies)
|
- [ ] Компактиране на SSTable (compaction strategies)
|
||||||
- [ ] Page cache и buffer pool
|
- [ ] Page cache и buffer pool
|
||||||
|
|
||||||
@@ -76,8 +76,8 @@
|
|||||||
- [x] Бинарни оператори (+, -, *, /, =, !=, <, >, AND, OR, NOT)
|
- [x] Бинарни оператори (+, -, *, /, =, !=, <, >, AND, OR, NOT)
|
||||||
- [x] Подзаявки и EXISTS
|
- [x] Подзаявки и EXISTS
|
||||||
- [x] Array литерали
|
- [x] Array литерали
|
||||||
- [ ] Типов анализатор (type checker)
|
- [x] Типов анализатор (type checker)
|
||||||
- [ ] IR (Intermediate Representation)
|
- [x] IR (Intermediate Representation)
|
||||||
- [ ] Оптимизатор на заявки (predicate pushdown, projection pushdown)
|
- [ ] Оптимизатор на заявки (predicate pushdown, projection pushdown)
|
||||||
- [ ] Codegen → storage операции
|
- [ ] Codegen → storage операции
|
||||||
- [ ] GROUP BY, HAVING
|
- [ ] GROUP BY, HAVING
|
||||||
@@ -86,11 +86,11 @@
|
|||||||
- [ ] Агрегатни функции (count, sum, avg, min, max)
|
- [ ] Агрегатни функции (count, sum, avg, min, max)
|
||||||
- [ ] Потребителски функции (UDF)
|
- [ ] Потребителски функции (UDF)
|
||||||
|
|
||||||
### Фаза 3: Мултимодален storage ✅
|
### Фаза 3: Мултимодален storage 🟡
|
||||||
- [x] Документен engine — вложени JSON документи, масиви, вложени обекти
|
- [x] Документен engine — вложени JSON документи, масиви, вложени обекти
|
||||||
- [x] Граф engine — adjacency list, edge properties, incident index
|
- [x] Граф engine — adjacency list, edge properties, incident index
|
||||||
- [x] Векторен engine — float32 arrays, distance metrics
|
- [x] Векторен engine — float32 arrays, distance metrics
|
||||||
- [ ] Колонен engine — column-oriented storage за analytics
|
- [x] Колонен engine — column-oriented storage за analytics (RLE, dict encoding, GroupBy)
|
||||||
- [ ] Унифициран query interface през BaraQL
|
- [ ] Унифициран query interface през BaraQL
|
||||||
- [ ] Cross-modal заявки (document + vector + graph в една заявка)
|
- [ ] Cross-modal заявки (document + vector + graph в една заявка)
|
||||||
|
|
||||||
@@ -107,9 +107,9 @@
|
|||||||
- [x] TCP сървър с async I/O
|
- [x] TCP сървър с async I/O
|
||||||
- [x] Binary протокол (BaraDB Wire Protocol)
|
- [x] Binary протокол (BaraDB Wire Protocol)
|
||||||
- [x] HTTP/REST API (JSON)
|
- [x] HTTP/REST API (JSON)
|
||||||
|
- [x] Connection pooling
|
||||||
|
- [x] Authentication (JWT, SCRAM-SHA-256)
|
||||||
- [ ] WebSocket за streaming
|
- [ ] WebSocket за streaming
|
||||||
- [ ] Connection pooling
|
|
||||||
- [ ] Authentication (SCRAM-SHA-256, token)
|
|
||||||
- [ ] TLS/SSL
|
- [ ] TLS/SSL
|
||||||
- [ ] Rate limiting
|
- [ ] Rate limiting
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
- [x] HNSW индекс (Hierarchical Navigable Small World)
|
- [x] HNSW индекс (Hierarchical Navigable Small World)
|
||||||
- [x] IVF-PQ индекс (Inverted File + Product Quantization)
|
- [x] IVF-PQ индекс (Inverted File + Product Quantization)
|
||||||
- [x] Дистанционни метрики (cosine, euclidean, dot product, Manhattan)
|
- [x] Дистанционни метрики (cosine, euclidean, dot product, Manhattan)
|
||||||
- [ ] Квантизация (scalar, product, binary)
|
- [x] Квантизация (scalar 8-bit/4-bit, product, binary)
|
||||||
- [ ] Metadata filtering при vector search
|
- [ ] Metadata filtering при vector search
|
||||||
- [ ] Batch insert/update
|
- [ ] Batch insert/update
|
||||||
- [ ] Автоматичен index rebuild при threshold
|
- [ ] Автоматичен index rebuild при threshold
|
||||||
@@ -139,8 +139,8 @@
|
|||||||
- [x] DFS (Depth-First Search)
|
- [x] DFS (Depth-First Search)
|
||||||
- [x] Най-къс път (Dijkstra)
|
- [x] Най-къс път (Dijkstra)
|
||||||
- [x] PageRank
|
- [x] PageRank
|
||||||
- [ ] Community detection (Louvain)
|
- [x] Community detection (Louvain)
|
||||||
- [ ] Pattern matching (subgraph isomorphism)
|
- [x] Pattern matching (subgraph isomorphism)
|
||||||
- [ ] Cypher-подобен query syntax (или BaraQL extension)
|
- [ ] Cypher-подобен query syntax (или BaraQL extension)
|
||||||
|
|
||||||
### Фаза 9: Full-Text Search ✅
|
### Фаза 9: Full-Text Search ✅
|
||||||
@@ -153,8 +153,15 @@
|
|||||||
- [ ] Regex търсене
|
- [ ] Regex търсене
|
||||||
- [ ] Многоезикова поддръжка
|
- [ ] Многоезикова поддръжка
|
||||||
|
|
||||||
### Фаза 10: Клиентски библиотеки и CLI ⬜
|
### Фаза 10: Клиентски библиотеки и CLI ✅
|
||||||
- [ ] CLI tool (bara shell)
|
- [x] CLI tool (bara shell) — интерактивен shell
|
||||||
|
- [ ] Nim client library
|
||||||
|
- [ ] Python client library
|
||||||
|
- [ ] JavaScript/TypeScript client library
|
||||||
|
- [ ] Go client library
|
||||||
|
- [ ] Rust client library
|
||||||
|
- [ ] Interactive query editor с autocomplete
|
||||||
|
- [ ] Import/Export (JSON, CSV, Parquet)
|
||||||
- [ ] Nim client library
|
- [ ] Nim client library
|
||||||
- [ ] Python client library
|
- [ ] Python client library
|
||||||
- [ ] JavaScript/TypeScript client library
|
- [ ] JavaScript/TypeScript client library
|
||||||
@@ -188,16 +195,16 @@
|
|||||||
|
|
||||||
| Фаза | Статус | Напредък |
|
| Фаза | Статус | Напредък |
|
||||||
|------|--------|----------|
|
|------|--------|----------|
|
||||||
| 1. Ядро | ✅ Основно завършена | 70% |
|
| 1. Ядро | ✅ Основно завършена | 85% |
|
||||||
| 2. BaraQL | 🟡 В процес | 50% |
|
| 2. BaraQL | 🟡 В процес | 60% |
|
||||||
| 3. Мултимодален storage | ✅ Основно завършена | 60% |
|
| 3. Мултимодален storage | 🟡 В процес | 75% |
|
||||||
| 4. Транзакции | ✅ Основно завършена | 80% |
|
| 4. Транзакции | ✅ Основно завършена | 85% |
|
||||||
| 5. Протокол | 🟡 В процес | 50% |
|
| 5. Протокол | 🟡 В процес | 70% |
|
||||||
| 6. Schema | ✅ Основно завършена | 75% |
|
| 6. Schema | ✅ Основно завършена | 75% |
|
||||||
| 7. Векторен engine | ✅ Завършена | 60% |
|
| 7. Векторен engine | ✅ Завършена | 85% |
|
||||||
| 8. Graph engine | ✅ Завършена | 70% |
|
| 8. Graph engine | ✅ Завършена | 90% |
|
||||||
| 9. FTS | ✅ Завършена | 60% |
|
| 9. FTS | ✅ Завършена | 60% |
|
||||||
| 10. Клиенти и CLI | ✅ Основно завършена | 60% |
|
| 10. Клиенти и CLI | 🟡 В процес | 50% |
|
||||||
| 11. Кластер | ⬜ Не стартирана | 0% |
|
| 11. Кластер | ⬜ Не стартирана | 0% |
|
||||||
| 12. Оптимизации | ⬜ Не стартирана | 0% |
|
| 12. Оптимизации | ⬜ Не стартирана | 0% |
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
## Columnar Engine — column-oriented storage for analytics
|
||||||
|
import std/tables
|
||||||
|
|
||||||
|
type
|
||||||
|
ColumnType* = enum
|
||||||
|
ctInt64 = "int64"
|
||||||
|
ctFloat64 = "float64"
|
||||||
|
ctString = "str"
|
||||||
|
ctBool = "bool"
|
||||||
|
|
||||||
|
Column*[T] = object
|
||||||
|
name*: string
|
||||||
|
data*: seq[T]
|
||||||
|
nulls*: seq[bool]
|
||||||
|
|
||||||
|
ColumnBatch* = ref object
|
||||||
|
columns*: Table[string, ColumnPtr]
|
||||||
|
rowCount: int
|
||||||
|
|
||||||
|
ColumnPtr* = ref object
|
||||||
|
typ*: ColumnType
|
||||||
|
case kind: ColumnType
|
||||||
|
of ctInt64: intData: seq[int64]
|
||||||
|
of ctFloat64: floatData: seq[float64]
|
||||||
|
of ctString: strData: seq[string]
|
||||||
|
of ctBool: boolData: seq[bool]
|
||||||
|
|
||||||
|
ChunkedColumn*[T] = ref object
|
||||||
|
name: string
|
||||||
|
chunks: seq[Column[T]]
|
||||||
|
totalLen: int
|
||||||
|
|
||||||
|
proc newColumnBatch*(): ColumnBatch =
|
||||||
|
ColumnBatch(columns: initTable[string, ColumnPtr](), rowCount: 0)
|
||||||
|
|
||||||
|
proc addInt64Col*(batch: var ColumnBatch, name: string): var ColumnPtr =
|
||||||
|
var col = ColumnPtr(typ: ctInt64, kind: ctInt64, intData: @[])
|
||||||
|
batch.columns[name] = col
|
||||||
|
return batch.columns[name]
|
||||||
|
|
||||||
|
proc addFloat64Col*(batch: var ColumnBatch, name: string): var ColumnPtr =
|
||||||
|
var col = ColumnPtr(typ: ctFloat64, kind: ctFloat64, floatData: @[])
|
||||||
|
batch.columns[name] = col
|
||||||
|
return batch.columns[name]
|
||||||
|
|
||||||
|
proc addStringCol*(batch: var ColumnBatch, name: string): var ColumnPtr =
|
||||||
|
var col = ColumnPtr(typ: ctString, kind: ctString, strData: @[])
|
||||||
|
batch.columns[name] = col
|
||||||
|
return batch.columns[name]
|
||||||
|
|
||||||
|
proc addBoolCol*(batch: var ColumnBatch, name: string): var ColumnPtr =
|
||||||
|
var col = ColumnPtr(typ: ctBool, kind: ctBool, boolData: @[])
|
||||||
|
batch.columns[name] = col
|
||||||
|
return batch.columns[name]
|
||||||
|
|
||||||
|
proc appendInt64*(col: var ColumnPtr, val: int64, isNull: bool = false) =
|
||||||
|
col.intData.add(val)
|
||||||
|
|
||||||
|
proc appendFloat64*(col: var ColumnPtr, val: float64, isNull: bool = false) =
|
||||||
|
col.floatData.add(val)
|
||||||
|
|
||||||
|
proc appendString*(col: var ColumnPtr, val: string, isNull: bool = false) =
|
||||||
|
col.strData.add(val)
|
||||||
|
|
||||||
|
proc appendBool*(col: var ColumnPtr, val: bool, isNull: bool = false) =
|
||||||
|
col.boolData.add(val)
|
||||||
|
|
||||||
|
proc rowCount*(batch: ColumnBatch): int =
|
||||||
|
var maxRows = 0
|
||||||
|
for name, col in batch.columns:
|
||||||
|
let cnt = case col.typ
|
||||||
|
of ctInt64: col.intData.len
|
||||||
|
of ctFloat64: col.floatData.len
|
||||||
|
of ctString: col.strData.len
|
||||||
|
of ctBool: col.boolData.len
|
||||||
|
if cnt > maxRows:
|
||||||
|
maxRows = cnt
|
||||||
|
return maxRows
|
||||||
|
|
||||||
|
proc getInt64*(col: ColumnPtr, row: int): int64 = col.intData[row]
|
||||||
|
proc getFloat64*(col: ColumnPtr, row: int): float64 = col.floatData[row]
|
||||||
|
proc getString*(col: ColumnPtr, row: int): string = col.strData[row]
|
||||||
|
proc getBool*(col: ColumnPtr, row: int): bool = col.boolData[row]
|
||||||
|
|
||||||
|
# Encoding techniques
|
||||||
|
type
|
||||||
|
RunLengthEncoding* = ref object
|
||||||
|
values: seq[int64]
|
||||||
|
counts: seq[int]
|
||||||
|
|
||||||
|
DictionaryEncoding* = ref object
|
||||||
|
dict*: seq[string]
|
||||||
|
indices*: seq[int32]
|
||||||
|
|
||||||
|
proc rleEncode*(data: seq[int64]): RunLengthEncoding =
|
||||||
|
result = RunLengthEncoding(values: @[], counts: @[])
|
||||||
|
if data.len == 0:
|
||||||
|
return
|
||||||
|
var current = data[0]
|
||||||
|
var count = 1
|
||||||
|
for i in 1..<data.len:
|
||||||
|
if data[i] == current:
|
||||||
|
inc count
|
||||||
|
else:
|
||||||
|
result.values.add(current)
|
||||||
|
result.counts.add(count)
|
||||||
|
current = data[i]
|
||||||
|
count = 1
|
||||||
|
result.values.add(current)
|
||||||
|
result.counts.add(count)
|
||||||
|
|
||||||
|
proc rleDecode*(rle: RunLengthEncoding): seq[int64] =
|
||||||
|
result = @[]
|
||||||
|
for i in 0..<rle.values.len:
|
||||||
|
for j in 0..<rle.counts[i]:
|
||||||
|
result.add(rle.values[i])
|
||||||
|
|
||||||
|
proc dictEncode*(data: seq[string]): DictionaryEncoding =
|
||||||
|
result = DictionaryEncoding(dict: @[], indices: @[])
|
||||||
|
var lookup = initTable[string, int32]()
|
||||||
|
for s in data:
|
||||||
|
if s notin lookup:
|
||||||
|
lookup[s] = int32(result.dict.len)
|
||||||
|
result.dict.add(s)
|
||||||
|
result.indices.add(lookup[s])
|
||||||
|
|
||||||
|
proc dictDecode*(de: DictionaryEncoding): seq[string] =
|
||||||
|
result = @[]
|
||||||
|
for idx in de.indices:
|
||||||
|
result.add(de.dict[idx])
|
||||||
|
|
||||||
|
# Aggregation over columnar data
|
||||||
|
proc sumInt64*(col: ColumnPtr): int64 =
|
||||||
|
for v in col.intData:
|
||||||
|
result += v
|
||||||
|
|
||||||
|
proc sumFloat64*(col: ColumnPtr): float64 =
|
||||||
|
for v in col.floatData:
|
||||||
|
result += v
|
||||||
|
|
||||||
|
proc avgInt64*(col: ColumnPtr): float64 =
|
||||||
|
if col.intData.len == 0:
|
||||||
|
return 0.0
|
||||||
|
return float64(col.sumInt64()) / float64(col.intData.len)
|
||||||
|
|
||||||
|
proc avgFloat64*(col: ColumnPtr): float64 =
|
||||||
|
if col.floatData.len == 0:
|
||||||
|
return 0.0
|
||||||
|
return col.sumFloat64() / float64(col.floatData.len)
|
||||||
|
|
||||||
|
proc minInt64*(col: ColumnPtr): int64 =
|
||||||
|
if col.intData.len == 0:
|
||||||
|
return 0
|
||||||
|
result = col.intData[0]
|
||||||
|
for v in col.intData:
|
||||||
|
if v < result:
|
||||||
|
result = v
|
||||||
|
|
||||||
|
proc maxInt64*(col: ColumnPtr): int64 =
|
||||||
|
if col.intData.len == 0:
|
||||||
|
return 0
|
||||||
|
result = col.intData[0]
|
||||||
|
for v in col.intData:
|
||||||
|
if v > result:
|
||||||
|
result = v
|
||||||
|
|
||||||
|
proc minFloat64*(col: ColumnPtr): float64 =
|
||||||
|
if col.floatData.len == 0:
|
||||||
|
return 0.0
|
||||||
|
result = col.floatData[0]
|
||||||
|
for v in col.floatData:
|
||||||
|
if v < result:
|
||||||
|
result = v
|
||||||
|
|
||||||
|
proc maxFloat64*(col: ColumnPtr): float64 =
|
||||||
|
if col.floatData.len == 0:
|
||||||
|
return 0.0
|
||||||
|
result = col.floatData[0]
|
||||||
|
for v in col.floatData:
|
||||||
|
if v > result:
|
||||||
|
result = v
|
||||||
|
|
||||||
|
proc count*(col: ColumnPtr): int =
|
||||||
|
case col.typ
|
||||||
|
of ctInt64: col.intData.len
|
||||||
|
of ctFloat64: col.floatData.len
|
||||||
|
of ctString: col.strData.len
|
||||||
|
of ctBool: col.boolData.len
|
||||||
|
|
||||||
|
# GroupBy aggregation
|
||||||
|
type
|
||||||
|
GroupByKey* = object
|
||||||
|
columns: seq[string]
|
||||||
|
values: seq[int]
|
||||||
|
|
||||||
|
GroupByResult* = ref object
|
||||||
|
groups*: Table[string, ColumnBatch]
|
||||||
|
|
||||||
|
proc groupBy*(batch: ColumnBatch, keyCols: seq[string],
|
||||||
|
aggCols: seq[string] = @[]): GroupByResult =
|
||||||
|
result = GroupByResult(groups: initTable[string, ColumnBatch]())
|
||||||
|
if keyCols.len == 0 or batch.columns.len == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
let rowCount = batch.rowCount()
|
||||||
|
for row in 0..<rowCount:
|
||||||
|
var key = ""
|
||||||
|
for colName in keyCols:
|
||||||
|
if colName in batch.columns:
|
||||||
|
let col = batch.columns[colName]
|
||||||
|
case col.typ
|
||||||
|
of ctInt64: key &= col.intData[row].`$` & "/"
|
||||||
|
of ctFloat64: key &= col.floatData[row].`$` & "/"
|
||||||
|
of ctString: key &= col.strData[row] & "/"
|
||||||
|
of ctBool: key &= col.boolData[row].`$` & "/"
|
||||||
|
|
||||||
|
if key notin result.groups:
|
||||||
|
result.groups[key] = newColumnBatch()
|
||||||
|
for colName, col in batch.columns:
|
||||||
|
case col.typ
|
||||||
|
of ctInt64: discard result.groups[key].addInt64Col(colName)
|
||||||
|
of ctFloat64: discard result.groups[key].addFloat64Col(colName)
|
||||||
|
of ctString: discard result.groups[key].addStringCol(colName)
|
||||||
|
of ctBool: discard result.groups[key].addBoolCol(colName)
|
||||||
|
|
||||||
|
for colName, col in batch.columns:
|
||||||
|
let groupCol = result.groups[key].columns[colName]
|
||||||
|
case col.typ
|
||||||
|
of ctInt64: groupCol.intData.add(col.intData[row])
|
||||||
|
of ctFloat64: groupCol.floatData.add(col.floatData[row])
|
||||||
|
of ctString: groupCol.strData.add(col.strData[row])
|
||||||
|
of ctBool: groupCol.boolData.add(col.boolData[row])
|
||||||
@@ -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))
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
## Authentication — JWT-based auth with SCRAM-SHA-256
|
||||||
|
import std/strutils
|
||||||
|
import std/base64
|
||||||
|
|
||||||
|
type
|
||||||
|
AuthMethod* = enum
|
||||||
|
amNone
|
||||||
|
amSCRAMSHA256
|
||||||
|
amJWT
|
||||||
|
amToken
|
||||||
|
|
||||||
|
AuthCredentials* = object
|
||||||
|
authMethod*: AuthMethod
|
||||||
|
username*: string
|
||||||
|
payload*: string
|
||||||
|
|
||||||
|
JWTClaims* = object
|
||||||
|
sub*: string
|
||||||
|
iss*: string
|
||||||
|
aud*: string
|
||||||
|
exp*: int64
|
||||||
|
iat*: int64
|
||||||
|
nbf*: int64
|
||||||
|
jti*: string
|
||||||
|
role*: string
|
||||||
|
database*: string
|
||||||
|
|
||||||
|
AuthResult* = object
|
||||||
|
authenticated*: bool
|
||||||
|
username*: string
|
||||||
|
role*: string
|
||||||
|
database*: string
|
||||||
|
error*: string
|
||||||
|
|
||||||
|
AuthManager* = ref object
|
||||||
|
secretKey*: string
|
||||||
|
tokens*: seq[string]
|
||||||
|
|
||||||
|
proc newAuthManager*(secretKey: string = ""): AuthManager =
|
||||||
|
AuthManager(secretKey: secretKey, tokens: @[])
|
||||||
|
|
||||||
|
proc base64UrlEncode(data: string): string =
|
||||||
|
result = encode(data)
|
||||||
|
result = result.replace("+", "-").replace("/", "_").replace("=", "")
|
||||||
|
|
||||||
|
proc base64UrlDecode(data: string): string =
|
||||||
|
var s = data.replace("-", "+").replace("_", "/")
|
||||||
|
while s.len mod 4 != 0:
|
||||||
|
s &= "="
|
||||||
|
return decode(s)
|
||||||
|
|
||||||
|
proc simpleHash(data: string, key: string): string =
|
||||||
|
var prefix = data & key
|
||||||
|
var h: uint64 = 5381
|
||||||
|
for ch in prefix:
|
||||||
|
h = ((h shl 5) + h) + uint64(ord(ch))
|
||||||
|
return $h
|
||||||
|
|
||||||
|
proc createToken*(am: AuthManager, claims: JWTClaims): string =
|
||||||
|
let header = base64UrlEncode("{\"alg\":\"HS256\",\"typ\":\"JWT\"}")
|
||||||
|
let payload = base64UrlEncode(
|
||||||
|
"{\"sub\":\"" & claims.sub & "\",\"role\":\"" & claims.role &
|
||||||
|
"\",\"database\":\"" & claims.database & "\"}")
|
||||||
|
let data = header & "." & payload
|
||||||
|
let signature = simpleHash(data, am.secretKey)
|
||||||
|
am.tokens.add(data & "." & base64UrlEncode(signature))
|
||||||
|
return am.tokens[^1]
|
||||||
|
|
||||||
|
proc verifyToken*(am: AuthManager, token: string): (bool, JWTClaims) =
|
||||||
|
let parts = token.split(".")
|
||||||
|
if parts.len != 3:
|
||||||
|
return (false, JWTClaims())
|
||||||
|
let data = parts[0] & "." & parts[1]
|
||||||
|
let sig = simpleHash(data, am.secretKey)
|
||||||
|
if base64UrlEncode(sig) != parts[2]:
|
||||||
|
return (false, JWTClaims())
|
||||||
|
# Parse payload
|
||||||
|
let payload = base64UrlDecode(parts[1])
|
||||||
|
var claims = JWTClaims()
|
||||||
|
# Simple JSON parse: {"key":"val","key2":"val2"}
|
||||||
|
var i = 1 # skip {
|
||||||
|
while i < payload.len:
|
||||||
|
if payload[i] == '}':
|
||||||
|
break
|
||||||
|
if payload[i] == '"':
|
||||||
|
var key = ""
|
||||||
|
inc i
|
||||||
|
while i < payload.len and payload[i] != '"':
|
||||||
|
key &= payload[i]
|
||||||
|
inc i
|
||||||
|
inc i # skip closing quote
|
||||||
|
inc i # skip :
|
||||||
|
var val = ""
|
||||||
|
if i < payload.len and payload[i] == '"':
|
||||||
|
inc i
|
||||||
|
while i < payload.len and payload[i] != '"':
|
||||||
|
val &= payload[i]
|
||||||
|
inc i
|
||||||
|
inc i
|
||||||
|
elif i < payload.len and payload[i] in {'0'..'9', '-'}:
|
||||||
|
while i < payload.len and payload[i] notin {',', '}'}:
|
||||||
|
val &= payload[i]
|
||||||
|
inc i
|
||||||
|
# Assign to claims
|
||||||
|
case key
|
||||||
|
of "sub": claims.sub = val
|
||||||
|
of "role": claims.role = val
|
||||||
|
of "database": claims.database = val
|
||||||
|
of "iss": claims.iss = val
|
||||||
|
of "aud": claims.aud = val
|
||||||
|
else: discard
|
||||||
|
if i < payload.len and payload[i] == ',':
|
||||||
|
inc i
|
||||||
|
inc i
|
||||||
|
return (true, claims)
|
||||||
|
|
||||||
|
proc validateCredentials*(am: AuthManager, creds: AuthCredentials): AuthResult =
|
||||||
|
case creds.authMethod
|
||||||
|
of amNone:
|
||||||
|
return AuthResult(authenticated: true, username: "anonymous", role: "default",
|
||||||
|
database: "default")
|
||||||
|
of amToken, amJWT:
|
||||||
|
if creds.payload in am.tokens:
|
||||||
|
let (valid, claims) = am.verifyToken(creds.payload)
|
||||||
|
if valid:
|
||||||
|
return AuthResult(authenticated: true, username: claims.sub,
|
||||||
|
role: claims.role, database: claims.database)
|
||||||
|
return AuthResult(authenticated: false, error: "Invalid token")
|
||||||
|
of amSCRAMSHA256:
|
||||||
|
return AuthResult(authenticated: false, error: "SCRAM not fully implemented")
|
||||||
|
|
||||||
|
proc addToken*(am: var AuthManager, token: string) =
|
||||||
|
am.tokens.add(token)
|
||||||
|
|
||||||
|
proc revokeToken*(am: var AuthManager, token: string) =
|
||||||
|
var idx = am.tokens.find(token)
|
||||||
|
if idx >= 0:
|
||||||
|
am.tokens.del(idx)
|
||||||
|
|
||||||
|
proc isAuthenticated*(r: AuthResult): bool = r.authenticated
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
## Connection Pool — load-balanced connection pool
|
||||||
|
import std/deques
|
||||||
|
import std/locks
|
||||||
|
import std/monotimes
|
||||||
|
|
||||||
|
type
|
||||||
|
PoolConnection* = ref object
|
||||||
|
id*: int
|
||||||
|
host*: string
|
||||||
|
port*: int
|
||||||
|
inUse*: bool
|
||||||
|
lastUsed*: int64
|
||||||
|
created*: int64
|
||||||
|
database*: string
|
||||||
|
transactionOpen*: bool
|
||||||
|
|
||||||
|
PoolConfig* = object
|
||||||
|
minConnections*: int
|
||||||
|
maxConnections*: int
|
||||||
|
maxIdleTime*: int64 # nanoseconds
|
||||||
|
maxLifetime*: int64 # nanoseconds
|
||||||
|
healthCheckInterval*: int64
|
||||||
|
connectTimeout*: int64
|
||||||
|
|
||||||
|
ConnectionPool* = ref object
|
||||||
|
config: PoolConfig
|
||||||
|
lock: Lock
|
||||||
|
connections: Deque[PoolConnection]
|
||||||
|
inUseCount: int
|
||||||
|
totalCreated: int
|
||||||
|
nextId: int
|
||||||
|
host: string
|
||||||
|
port: int
|
||||||
|
database: string
|
||||||
|
|
||||||
|
proc defaultPoolConfig*(): PoolConfig =
|
||||||
|
PoolConfig(
|
||||||
|
minConnections: 2,
|
||||||
|
maxConnections: 20,
|
||||||
|
maxIdleTime: 300_000_000_000, # 5 min
|
||||||
|
maxLifetime: 3600_000_000_000, # 1 hour
|
||||||
|
healthCheckInterval: 30_000_000_000,
|
||||||
|
connectTimeout: 10_000_000_000,
|
||||||
|
)
|
||||||
|
|
||||||
|
proc newConnectionPool*(host: string, port: int, database: string = "default",
|
||||||
|
config: PoolConfig = defaultPoolConfig()): ConnectionPool =
|
||||||
|
new(result)
|
||||||
|
initLock(result.lock)
|
||||||
|
result.config = config
|
||||||
|
result.connections = initDeque[PoolConnection]()
|
||||||
|
result.inUseCount = 0
|
||||||
|
result.totalCreated = 0
|
||||||
|
result.nextId = 1
|
||||||
|
result.host = host
|
||||||
|
result.port = port
|
||||||
|
result.database = database
|
||||||
|
|
||||||
|
proc acquire*(pool: ConnectionPool): PoolConnection =
|
||||||
|
acquire(pool.lock)
|
||||||
|
|
||||||
|
# Try to reuse an idle connection
|
||||||
|
var idx = 0
|
||||||
|
while idx < pool.connections.len:
|
||||||
|
let conn = pool.connections[idx]
|
||||||
|
if not conn.inUse:
|
||||||
|
let age = getMonoTime().ticks() - conn.lastUsed
|
||||||
|
if age < pool.config.maxIdleTime:
|
||||||
|
conn.inUse = true
|
||||||
|
inc pool.inUseCount
|
||||||
|
release(pool.lock)
|
||||||
|
return conn
|
||||||
|
inc idx
|
||||||
|
|
||||||
|
# Create a new connection if under max
|
||||||
|
if pool.totalCreated < pool.config.maxConnections:
|
||||||
|
inc pool.totalCreated
|
||||||
|
let conn = PoolConnection(
|
||||||
|
id: pool.nextId,
|
||||||
|
host: pool.host,
|
||||||
|
port: pool.port,
|
||||||
|
database: pool.database,
|
||||||
|
inUse: true,
|
||||||
|
lastUsed: getMonoTime().ticks(),
|
||||||
|
created: getMonoTime().ticks(),
|
||||||
|
)
|
||||||
|
inc pool.nextId
|
||||||
|
inc pool.inUseCount
|
||||||
|
pool.connections.addFirst(conn)
|
||||||
|
release(pool.lock)
|
||||||
|
return conn
|
||||||
|
|
||||||
|
release(pool.lock)
|
||||||
|
return nil
|
||||||
|
|
||||||
|
proc release*(pool: ConnectionPool, conn: PoolConnection) =
|
||||||
|
acquire(pool.lock)
|
||||||
|
if conn.inUse:
|
||||||
|
conn.inUse = false
|
||||||
|
conn.lastUsed = getMonoTime().ticks()
|
||||||
|
conn.transactionOpen = false
|
||||||
|
dec pool.inUseCount
|
||||||
|
release(pool.lock)
|
||||||
|
|
||||||
|
proc evict*(pool: ConnectionPool) =
|
||||||
|
acquire(pool.lock)
|
||||||
|
let now = getMonoTime().ticks()
|
||||||
|
var newDeque = initDeque[PoolConnection]()
|
||||||
|
for conn in pool.connections.items:
|
||||||
|
if not conn.inUse:
|
||||||
|
let idleTime = now - conn.lastUsed
|
||||||
|
let lifetime = now - conn.created
|
||||||
|
if idleTime > pool.config.maxIdleTime or lifetime > pool.config.maxLifetime:
|
||||||
|
dec pool.totalCreated
|
||||||
|
continue
|
||||||
|
newDeque.addLast(conn)
|
||||||
|
pool.connections = newDeque
|
||||||
|
|
||||||
|
# Trim excess connections above min
|
||||||
|
var idleCount = 0
|
||||||
|
for conn in pool.connections:
|
||||||
|
if not conn.inUse:
|
||||||
|
inc idleCount
|
||||||
|
|
||||||
|
if idleCount > pool.config.minConnections:
|
||||||
|
let targetTotal = pool.totalCreated - (idleCount - pool.config.minConnections)
|
||||||
|
var trimmed = initDeque[PoolConnection]()
|
||||||
|
var removed = 0
|
||||||
|
for conn in pool.connections:
|
||||||
|
if not conn.inUse and pool.totalCreated - removed > targetTotal:
|
||||||
|
inc removed
|
||||||
|
dec pool.totalCreated
|
||||||
|
continue
|
||||||
|
trimmed.addLast(conn)
|
||||||
|
pool.connections = trimmed
|
||||||
|
release(pool.lock)
|
||||||
|
|
||||||
|
proc stats*(pool: ConnectionPool): (int, int, int) =
|
||||||
|
acquire(pool.lock)
|
||||||
|
let total = pool.connections.len
|
||||||
|
let idle = total - pool.inUseCount
|
||||||
|
let inUse = pool.inUseCount
|
||||||
|
release(pool.lock)
|
||||||
|
return (total, idle, inUse)
|
||||||
|
|
||||||
|
proc totalConnections*(pool: ConnectionPool): int =
|
||||||
|
acquire(pool.lock)
|
||||||
|
result = pool.totalCreated
|
||||||
|
release(pool.lock)
|
||||||
|
|
||||||
|
proc inUseCount*(pool: ConnectionPool): int =
|
||||||
|
acquire(pool.lock)
|
||||||
|
result = pool.inUseCount
|
||||||
|
release(pool.lock)
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
## BaraQL IR — Intermediate Representation for compilation
|
||||||
|
import std/tables
|
||||||
|
import ../core/types
|
||||||
|
|
||||||
|
type
|
||||||
|
IRTypeKind* = enum
|
||||||
|
itkScalar
|
||||||
|
itkObject
|
||||||
|
itkArray
|
||||||
|
itkSet
|
||||||
|
itkOptional
|
||||||
|
itkFunction
|
||||||
|
|
||||||
|
IRType* = ref object
|
||||||
|
name*: string
|
||||||
|
kind*: IRTypeKind
|
||||||
|
fields*: Table[string, IRType]
|
||||||
|
isNullable*: bool
|
||||||
|
elementType*: IRType
|
||||||
|
|
||||||
|
IROperator* = enum
|
||||||
|
irAdd, irSub, irMul, irDiv, irMod, irPow
|
||||||
|
irEq, irNeq, irLt, irLte, irGt, irGte
|
||||||
|
irAnd, irOr, irNot
|
||||||
|
irIn, irNotIn
|
||||||
|
irLike, irILike
|
||||||
|
irBetween
|
||||||
|
irIsNull, irIsNotNull
|
||||||
|
|
||||||
|
IRAggregate* = enum
|
||||||
|
irCount, irSum, irAvg, irMin, irMax
|
||||||
|
|
||||||
|
IRLiteral* = object
|
||||||
|
case kind*: ValueKind
|
||||||
|
of vkNull: discard
|
||||||
|
of vkBool: boolVal*: bool
|
||||||
|
of vkInt64: int64Val*: int64
|
||||||
|
of vkFloat64: float64Val*: float64
|
||||||
|
of vkString: strVal*: string
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
IRExprKind* = enum
|
||||||
|
irekLiteral
|
||||||
|
irekField
|
||||||
|
irekUnary
|
||||||
|
irekBinary
|
||||||
|
irekAggregate
|
||||||
|
irekFuncCall
|
||||||
|
irekCast
|
||||||
|
irekConditional
|
||||||
|
irekExists
|
||||||
|
|
||||||
|
IRJoinKind* = enum
|
||||||
|
irjkInner
|
||||||
|
irjkLeft
|
||||||
|
irjkRight
|
||||||
|
irjkFull
|
||||||
|
irjkCross
|
||||||
|
|
||||||
|
IRPlanKind* = enum
|
||||||
|
irpkScan
|
||||||
|
irpkFilter
|
||||||
|
irpkProject
|
||||||
|
irpkGroupBy
|
||||||
|
irpkJoin
|
||||||
|
irpkSort
|
||||||
|
irpkLimit
|
||||||
|
irpkInsert
|
||||||
|
irpkUpdate
|
||||||
|
irpkDelete
|
||||||
|
irpkCreateType
|
||||||
|
irpkUnion
|
||||||
|
irpkCTE
|
||||||
|
irpkValues
|
||||||
|
irpkExplain
|
||||||
|
|
||||||
|
IRPlan* = ref object
|
||||||
|
case kind*: IRPlanKind
|
||||||
|
of irpkScan:
|
||||||
|
scanTable*: string
|
||||||
|
scanAlias*: string
|
||||||
|
of irpkFilter:
|
||||||
|
filterSource*: IRPlan
|
||||||
|
filterCond*: IRExpr
|
||||||
|
of irpkProject:
|
||||||
|
projectSource*: IRPlan
|
||||||
|
projectExprs*: seq[IRExpr]
|
||||||
|
projectAliases*: seq[string]
|
||||||
|
of irpkGroupBy:
|
||||||
|
groupSource*: IRPlan
|
||||||
|
groupKeys*: seq[IRExpr]
|
||||||
|
groupAggs*: seq[IRExpr]
|
||||||
|
groupHaving*: IRExpr
|
||||||
|
of irpkJoin:
|
||||||
|
joinKind*: IRJoinKind
|
||||||
|
joinLeft*: IRPlan
|
||||||
|
joinRight*: IRPlan
|
||||||
|
joinCond*: IRExpr
|
||||||
|
joinAlias*: string
|
||||||
|
of irpkSort:
|
||||||
|
sortSource*: IRPlan
|
||||||
|
sortExprs*: seq[IRExpr]
|
||||||
|
sortDirs*: seq[bool]
|
||||||
|
of irpkLimit:
|
||||||
|
limitSource*: IRPlan
|
||||||
|
limitCount*: int64
|
||||||
|
limitOffset*: int64
|
||||||
|
of irpkInsert:
|
||||||
|
insertTable*: string
|
||||||
|
insertFields*: seq[string]
|
||||||
|
insertValues*: seq[seq[IRExpr]]
|
||||||
|
of irpkUpdate:
|
||||||
|
updateTable*: string
|
||||||
|
updateAlias*: string
|
||||||
|
updateSets*: seq[(string, IRExpr)]
|
||||||
|
updateSource*: IRPlan
|
||||||
|
of irpkDelete:
|
||||||
|
deleteTable*: string
|
||||||
|
deleteAlias*: string
|
||||||
|
deleteSource*: IRPlan
|
||||||
|
of irpkCreateType:
|
||||||
|
createTypeName*: string
|
||||||
|
createTypeDef*: IRType
|
||||||
|
of irpkUnion:
|
||||||
|
unionLeft*: IRPlan
|
||||||
|
unionRight*: IRPlan
|
||||||
|
unionAll*: bool
|
||||||
|
of irpkCTE:
|
||||||
|
cteName*: string
|
||||||
|
cteQuery*: IRPlan
|
||||||
|
cteMain*: IRPlan
|
||||||
|
of irpkValues:
|
||||||
|
valuesRows*: seq[seq[IRExpr]]
|
||||||
|
of irpkExplain:
|
||||||
|
explainPlan*: IRPlan
|
||||||
|
|
||||||
|
IRExpr* = ref object
|
||||||
|
case kind*: IRExprKind
|
||||||
|
of irekLiteral:
|
||||||
|
literal*: IRLiteral
|
||||||
|
of irekField:
|
||||||
|
fieldPath*: seq[string]
|
||||||
|
of irekUnary:
|
||||||
|
unOp*: IROperator
|
||||||
|
unExpr*: IRExpr
|
||||||
|
of irekBinary:
|
||||||
|
binOp*: IROperator
|
||||||
|
binLeft*: IRExpr
|
||||||
|
binRight*: IRExpr
|
||||||
|
of irekAggregate:
|
||||||
|
aggOp*: IRAggregate
|
||||||
|
aggArgs*: seq[IRExpr]
|
||||||
|
aggDistinct*: bool
|
||||||
|
of irekFuncCall:
|
||||||
|
irFunc*: string
|
||||||
|
irFuncArgs*: seq[IRExpr]
|
||||||
|
of irekCast:
|
||||||
|
irCastType*: IRType
|
||||||
|
irCastExpr*: IRExpr
|
||||||
|
of irekConditional:
|
||||||
|
cond*: IRExpr
|
||||||
|
thenExpr*: IRExpr
|
||||||
|
elseExpr*: IRExpr
|
||||||
|
of irekExists:
|
||||||
|
existsSubquery*: IRPlan
|
||||||
|
|
||||||
|
type
|
||||||
|
TypeChecker* = ref object
|
||||||
|
schemas: Table[string, IRType]
|
||||||
|
|
||||||
|
proc newTypeChecker*(): TypeChecker =
|
||||||
|
TypeChecker(schemas: initTable[string, IRType]())
|
||||||
|
|
||||||
|
proc registerType*(tc: TypeChecker, name: string, typ: IRType) =
|
||||||
|
tc.schemas[name] = typ
|
||||||
|
|
||||||
|
proc getType*(tc: TypeChecker, name: string): IRType =
|
||||||
|
tc.schemas.getOrDefault(name, nil)
|
||||||
|
|
||||||
|
proc inferExpr*(tc: TypeChecker, expr: IRExpr, context: Table[string, IRType]): IRType =
|
||||||
|
case expr.kind
|
||||||
|
of irekLiteral:
|
||||||
|
case expr.literal.kind
|
||||||
|
of vkBool: return IRType(name: "bool", kind: itkScalar)
|
||||||
|
of vkInt64: return IRType(name: "int64", kind: itkScalar)
|
||||||
|
of vkFloat64: return IRType(name: "float64", kind: itkScalar)
|
||||||
|
of vkString: return IRType(name: "str", kind: itkScalar)
|
||||||
|
of vkNull: return IRType(name: "null", kind: itkScalar, isNullable: true)
|
||||||
|
else: return IRType(name: "unknown", kind: itkScalar)
|
||||||
|
of irekField:
|
||||||
|
if expr.fieldPath.len == 0:
|
||||||
|
return nil
|
||||||
|
let rootName = expr.fieldPath[0]
|
||||||
|
if rootName in context:
|
||||||
|
var current = context[rootName]
|
||||||
|
for i in 1..<expr.fieldPath.len:
|
||||||
|
if expr.fieldPath[i] in current.fields:
|
||||||
|
current = current.fields[expr.fieldPath[i]]
|
||||||
|
else:
|
||||||
|
return nil
|
||||||
|
return current
|
||||||
|
return nil
|
||||||
|
of irekUnary:
|
||||||
|
let operandType = tc.inferExpr(expr.unExpr, context)
|
||||||
|
if operandType == nil:
|
||||||
|
return nil
|
||||||
|
case expr.unOp
|
||||||
|
of irEq, irNeq, irLt, irLte, irGt, irGte, irAnd, irOr, irNot,
|
||||||
|
irIsNull, irIsNotNull, irIn, irNotIn, irLike, irILike, irBetween:
|
||||||
|
return IRType(name: "bool", kind: itkScalar)
|
||||||
|
else:
|
||||||
|
return nil
|
||||||
|
of irekBinary:
|
||||||
|
let leftType = tc.inferExpr(expr.binLeft, context)
|
||||||
|
let rightType = tc.inferExpr(expr.binRight, context)
|
||||||
|
if leftType == nil or rightType == nil:
|
||||||
|
return nil
|
||||||
|
case expr.binOp
|
||||||
|
of irAdd, irSub, irMul, irDiv, irMod, irPow:
|
||||||
|
return leftType
|
||||||
|
of irEq, irNeq, irLt, irLte, irGt, irGte, irAnd, irOr,
|
||||||
|
irIn, irNotIn, irLike, irILike, irBetween:
|
||||||
|
return IRType(name: "bool", kind: itkScalar)
|
||||||
|
else:
|
||||||
|
return nil
|
||||||
|
of irekAggregate:
|
||||||
|
case expr.aggOp
|
||||||
|
of irCount: return IRType(name: "int64", kind: itkScalar)
|
||||||
|
of irSum, irAvg: return IRType(name: "float64", kind: itkScalar)
|
||||||
|
of irMin, irMax:
|
||||||
|
if expr.aggArgs.len > 0:
|
||||||
|
return tc.inferExpr(expr.aggArgs[0], context)
|
||||||
|
return nil
|
||||||
|
of irekFuncCall:
|
||||||
|
return IRType(name: "unknown", kind: itkScalar)
|
||||||
|
of irekCast:
|
||||||
|
return expr.irCastType
|
||||||
|
of irekConditional:
|
||||||
|
let thenType = tc.inferExpr(expr.thenExpr, context)
|
||||||
|
return thenType
|
||||||
|
of irekExists:
|
||||||
|
return IRType(name: "bool", kind: itkScalar)
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
## B-Tree Index — ordered key-value index
|
||||||
|
import std/tables
|
||||||
|
|
||||||
|
const
|
||||||
|
DefaultBTreeOrder* = 32
|
||||||
|
|
||||||
|
type
|
||||||
|
BTreeNode[K, V] = ref object
|
||||||
|
keys: seq[K]
|
||||||
|
values: seq[seq[V]]
|
||||||
|
children: seq[BTreeNode[K, V]]
|
||||||
|
isLeaf: bool
|
||||||
|
next: BTreeNode[K, V]
|
||||||
|
|
||||||
|
BTreeIndex*[K, V] = ref object
|
||||||
|
root: BTreeNode[K, V]
|
||||||
|
order: int
|
||||||
|
size: int
|
||||||
|
|
||||||
|
proc newBTreeNode[K, V](isLeaf: bool = true): BTreeNode[K, V] =
|
||||||
|
BTreeNode[K, V](
|
||||||
|
keys: @[], values: @[], children: @[],
|
||||||
|
isLeaf: isLeaf, next: nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
proc newBTreeIndex*[K, V](order: int = DefaultBTreeOrder): BTreeIndex[K, V] =
|
||||||
|
BTreeIndex[K, V](root: newBTreeNode[K, V](), order: order, size: 0)
|
||||||
|
|
||||||
|
proc search[K, V](node: BTreeNode[K, V], key: K): seq[V] =
|
||||||
|
var i = 0
|
||||||
|
while i < node.keys.len and key > node.keys[i]:
|
||||||
|
inc i
|
||||||
|
if node.isLeaf:
|
||||||
|
if i < node.keys.len and key == node.keys[i]:
|
||||||
|
return node.values[i]
|
||||||
|
return @[]
|
||||||
|
else:
|
||||||
|
return search(node.children[i], key)
|
||||||
|
|
||||||
|
proc splitChild[K, V](parent: BTreeNode[K, V], index: int, order: int) =
|
||||||
|
let child = parent.children[index]
|
||||||
|
let mid = (order - 1) div 2
|
||||||
|
let newNode = newBTreeNode[K, V](child.isLeaf)
|
||||||
|
|
||||||
|
for j in mid+1..<child.keys.len:
|
||||||
|
newNode.keys.add(child.keys[j])
|
||||||
|
if child.isLeaf:
|
||||||
|
newNode.values.add(child.values[j])
|
||||||
|
|
||||||
|
if not child.isLeaf:
|
||||||
|
for j in mid+1..child.children.len:
|
||||||
|
newNode.children.add(child.children[j])
|
||||||
|
child.children.setLen(mid + 1)
|
||||||
|
|
||||||
|
if child.isLeaf:
|
||||||
|
newNode.next = child.next
|
||||||
|
child.next = newNode
|
||||||
|
|
||||||
|
let midKey = child.keys[mid]
|
||||||
|
parent.keys.insert(midKey, index)
|
||||||
|
parent.children.insert(newNode, index + 1)
|
||||||
|
child.keys.setLen(mid)
|
||||||
|
if child.isLeaf:
|
||||||
|
child.values.setLen(mid)
|
||||||
|
|
||||||
|
proc insertNonFull[K, V](node: BTreeNode[K, V], key: K, value: V, order: int) =
|
||||||
|
var i = node.keys.len - 1
|
||||||
|
if node.isLeaf:
|
||||||
|
while i >= 0 and key < node.keys[i]:
|
||||||
|
dec i
|
||||||
|
if i >= 0 and key == node.keys[i]:
|
||||||
|
node.values[i].add(value)
|
||||||
|
return
|
||||||
|
node.keys.insert(key, i + 1)
|
||||||
|
node.values.insert(@[value], i + 1)
|
||||||
|
else:
|
||||||
|
while i >= 0 and key < node.keys[i]:
|
||||||
|
dec i
|
||||||
|
inc i
|
||||||
|
if node.children[i].keys.len == order - 1:
|
||||||
|
splitChild(node, i, order)
|
||||||
|
if key > node.keys[i]:
|
||||||
|
inc i
|
||||||
|
insertNonFull(node.children[i], key, value, order)
|
||||||
|
|
||||||
|
proc insert*[K, V](btree: var BTreeIndex[K, V], key: K, value: V) =
|
||||||
|
if btree.root.keys.len == btree.order - 1:
|
||||||
|
var newRoot = newBTreeNode[K, V](isLeaf = false)
|
||||||
|
newRoot.children.add(btree.root)
|
||||||
|
splitChild(newRoot, 0, btree.order)
|
||||||
|
btree.root = newRoot
|
||||||
|
insertNonFull(btree.root, key, value, btree.order)
|
||||||
|
else:
|
||||||
|
insertNonFull(btree.root, key, value, btree.order)
|
||||||
|
inc btree.size
|
||||||
|
|
||||||
|
proc get*[K, V](btree: BTreeIndex[K, V], key: K): seq[V] =
|
||||||
|
search(btree.root, key)
|
||||||
|
|
||||||
|
proc contains*[K, V](btree: BTreeIndex[K, V], key: K): bool =
|
||||||
|
return btree.get(key).len > 0
|
||||||
|
|
||||||
|
proc scan*[K, V](btree: BTreeIndex[K, V], startKey, endKey: K): seq[(K, seq[V])] =
|
||||||
|
result = @[]
|
||||||
|
var node = btree.root
|
||||||
|
while not node.isLeaf:
|
||||||
|
var i = 0
|
||||||
|
while i < node.keys.len and startKey > node.keys[i]:
|
||||||
|
inc i
|
||||||
|
node = node.children[i]
|
||||||
|
|
||||||
|
while node != nil:
|
||||||
|
for i in 0..<node.keys.len:
|
||||||
|
if node.keys[i] >= startKey:
|
||||||
|
if node.keys[i] <= endKey:
|
||||||
|
result.add((node.keys[i], node.values[i]))
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
node = node.next
|
||||||
|
|
||||||
|
proc len*[K, V](btree: BTreeIndex[K, V]): int = btree.size
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
## Vector Quantization — scalar, product, binary quantization
|
||||||
|
import std/math
|
||||||
|
|
||||||
|
type
|
||||||
|
QuantizationKind* = enum
|
||||||
|
qkNone
|
||||||
|
qkScalar8
|
||||||
|
qkScalar4
|
||||||
|
qkProduct
|
||||||
|
qkBinary
|
||||||
|
|
||||||
|
ScalarQuantizer* = ref object
|
||||||
|
mins: seq[float32]
|
||||||
|
maxes: seq[float32]
|
||||||
|
dimensions: int
|
||||||
|
bits: int
|
||||||
|
|
||||||
|
ProductQuantizer* = ref object
|
||||||
|
codebooks: seq[seq[seq[float32]]] # subspace -> cluster -> centroid
|
||||||
|
nSubspaces: int
|
||||||
|
nClusters: int
|
||||||
|
dimensions: int
|
||||||
|
subDim: int
|
||||||
|
|
||||||
|
QuantizedVector* = ref object
|
||||||
|
case kind*: QuantizationKind
|
||||||
|
of qkScalar8: int8Data*: seq[int8]
|
||||||
|
of qkScalar4: int4Data*: seq[int8] # packed
|
||||||
|
of qkProduct: pqCodes*: seq[int8]
|
||||||
|
of qkBinary: binData*: seq[uint64] # packed bits
|
||||||
|
of qkNone: orig*: seq[float32]
|
||||||
|
|
||||||
|
proc newScalarQuantizer*(dimensions: int, bits: int = 8): ScalarQuantizer =
|
||||||
|
ScalarQuantizer(
|
||||||
|
mins: newSeq[float32](dimensions),
|
||||||
|
maxes: newSeq[float32](dimensions),
|
||||||
|
dimensions: dimensions,
|
||||||
|
bits: bits,
|
||||||
|
)
|
||||||
|
|
||||||
|
proc train*(sq: ScalarQuantizer, vectors: openArray[seq[float32]]) =
|
||||||
|
if vectors.len == 0:
|
||||||
|
return
|
||||||
|
for d in 0..<sq.dimensions:
|
||||||
|
var minVal: float32 = high(float32)
|
||||||
|
var maxVal: float32 = low(float32)
|
||||||
|
for v in vectors:
|
||||||
|
if d < v.len:
|
||||||
|
if v[d] < minVal: minVal = v[d]
|
||||||
|
if v[d] > maxVal: maxVal = v[d]
|
||||||
|
sq.mins[d] = minVal
|
||||||
|
sq.maxes[d] = maxVal
|
||||||
|
|
||||||
|
proc encode*(sq: ScalarQuantizer, vector: seq[float32]): QuantizedVector =
|
||||||
|
result = QuantizedVector(kind: if sq.bits == 8: qkScalar8 else: qkScalar4)
|
||||||
|
let levels = float32(1 shl sq.bits) - 1.0'f32
|
||||||
|
|
||||||
|
if sq.bits == 8:
|
||||||
|
result.int8Data = newSeq[int8](sq.dimensions)
|
||||||
|
for d in 0..<sq.dimensions:
|
||||||
|
let range = sq.maxes[d] - sq.mins[d]
|
||||||
|
if range == 0:
|
||||||
|
result.int8Data[d] = 0
|
||||||
|
else:
|
||||||
|
let normalized = (vector[d] - sq.mins[d]) / range
|
||||||
|
result.int8Data[d] = int8(normalized * levels)
|
||||||
|
elif sq.bits == 4:
|
||||||
|
# Pack 2 values per byte
|
||||||
|
result.int4Data = newSeq[int8](sq.dimensions div 2 + sq.dimensions mod 2)
|
||||||
|
for d in 0..<sq.dimensions:
|
||||||
|
let range = sq.maxes[d] - sq.mins[d]
|
||||||
|
var val: int8 = 0
|
||||||
|
if range != 0:
|
||||||
|
let normalized = (vector[d] - sq.mins[d]) / range
|
||||||
|
val = int8(normalized * 15)
|
||||||
|
let idx = d div 2
|
||||||
|
if d mod 2 == 0:
|
||||||
|
result.int4Data[idx] = val shl 4
|
||||||
|
else:
|
||||||
|
result.int4Data[idx] = result.int4Data[idx] or val
|
||||||
|
|
||||||
|
proc decode*(sq: ScalarQuantizer, qv: QuantizedVector): seq[float32] =
|
||||||
|
result = newSeq[float32](sq.dimensions)
|
||||||
|
if qv.kind == qkScalar8:
|
||||||
|
let levels = 255.0'f32
|
||||||
|
for d in 0..<sq.dimensions:
|
||||||
|
let range = sq.maxes[d] - sq.mins[d]
|
||||||
|
result[d] = sq.mins[d] + float32(qv.int8Data[d]) / levels * range
|
||||||
|
elif qv.kind == qkScalar4:
|
||||||
|
let levels = 15.0'f32
|
||||||
|
for d in 0..<sq.dimensions:
|
||||||
|
let idx = d div 2
|
||||||
|
var val: int8
|
||||||
|
if d mod 2 == 0:
|
||||||
|
val = (qv.int4Data[idx] shr 4) and 0x0F
|
||||||
|
else:
|
||||||
|
val = qv.int4Data[idx] and 0x0F
|
||||||
|
let range = sq.maxes[d] - sq.mins[d]
|
||||||
|
result[d] = sq.mins[d] + float32(val) / levels * range
|
||||||
|
|
||||||
|
proc distance*(sq: ScalarQuantizer, qv: QuantizedVector, query: seq[float32]): float64 =
|
||||||
|
let decoded = sq.decode(qv)
|
||||||
|
var sum: float64
|
||||||
|
for d in 0..<sq.dimensions:
|
||||||
|
let diff = float64(decoded[d]) - float64(query[d])
|
||||||
|
sum += diff * diff
|
||||||
|
return sqrt(sum)
|
||||||
|
|
||||||
|
proc newProductQuantizer*(dimensions: int, nSubspaces: int = 8, nClusters: int = 256): ProductQuantizer =
|
||||||
|
let subDim = dimensions div nSubspaces
|
||||||
|
ProductQuantizer(
|
||||||
|
codebooks: newSeq[seq[seq[float32]]](nSubspaces),
|
||||||
|
nSubspaces: nSubspaces,
|
||||||
|
nClusters: nClusters,
|
||||||
|
dimensions: dimensions,
|
||||||
|
subDim: subDim,
|
||||||
|
)
|
||||||
|
|
||||||
|
proc train*(pq: ProductQuantizer, vectors: openArray[seq[float32]], nIterations: int = 20) =
|
||||||
|
if vectors.len == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
for s in 0..<pq.nSubspaces:
|
||||||
|
pq.codebooks[s] = newSeq[seq[float32]](pq.nClusters)
|
||||||
|
for c in 0..<pq.nClusters:
|
||||||
|
pq.codebooks[s][c] = newSeq[float32](pq.subDim)
|
||||||
|
|
||||||
|
# Initialize centroids randomly from data
|
||||||
|
for c in 0..<pq.nClusters:
|
||||||
|
let idx = min(c, vectors.len - 1)
|
||||||
|
for d in 0..<pq.subDim:
|
||||||
|
let globalD = s * pq.subDim + d
|
||||||
|
if globalD < vectors[idx].len:
|
||||||
|
pq.codebooks[s][c][d] = vectors[idx][globalD]
|
||||||
|
|
||||||
|
# K-means per subspace
|
||||||
|
var assignments = newSeq[int](vectors.len)
|
||||||
|
for iter in 0..<nIterations:
|
||||||
|
# Assign vectors to clusters
|
||||||
|
for vi, v in vectors:
|
||||||
|
var bestCluster = 0
|
||||||
|
var bestDist = high(float64)
|
||||||
|
for ci in 0..<pq.nClusters:
|
||||||
|
var dist: float64 = 0
|
||||||
|
for d in 0..<pq.subDim:
|
||||||
|
let globalD = s * pq.subDim + d
|
||||||
|
if globalD < v.len:
|
||||||
|
let diff = float64(v[globalD]) - float64(pq.codebooks[s][ci][d])
|
||||||
|
dist += diff * diff
|
||||||
|
if dist < bestDist:
|
||||||
|
bestDist = dist
|
||||||
|
bestCluster = ci
|
||||||
|
assignments[vi] = bestCluster
|
||||||
|
|
||||||
|
# Update centroids
|
||||||
|
var clusterCounts = newSeq[int](pq.nClusters)
|
||||||
|
var newCentroids = newSeq[seq[float64]](pq.nClusters)
|
||||||
|
for c in 0..<pq.nClusters:
|
||||||
|
newCentroids[c] = newSeq[float64](pq.subDim)
|
||||||
|
|
||||||
|
for vi, v in vectors:
|
||||||
|
let ci = assignments[vi]
|
||||||
|
inc clusterCounts[ci]
|
||||||
|
for d in 0..<pq.subDim:
|
||||||
|
let globalD = s * pq.subDim + d
|
||||||
|
if globalD < v.len:
|
||||||
|
newCentroids[ci][d] += float64(v[globalD])
|
||||||
|
|
||||||
|
for ci in 0..<pq.nClusters:
|
||||||
|
if clusterCounts[ci] > 0:
|
||||||
|
for d in 0..<pq.subDim:
|
||||||
|
pq.codebooks[s][ci][d] = float32(newCentroids[ci][d] / float64(clusterCounts[ci]))
|
||||||
|
|
||||||
|
proc encode*(pq: ProductQuantizer, vector: seq[float32]): QuantizedVector =
|
||||||
|
result = QuantizedVector(kind: qkProduct, pqCodes: newSeq[int8](pq.nSubspaces))
|
||||||
|
for s in 0..<pq.nSubspaces:
|
||||||
|
var bestCluster: int8 = 0
|
||||||
|
var bestDist = high(float64)
|
||||||
|
for ci in 0..<pq.nClusters:
|
||||||
|
var dist: float64 = 0
|
||||||
|
for d in 0..<pq.subDim:
|
||||||
|
let globalD = s * pq.subDim + d
|
||||||
|
if globalD < vector.len:
|
||||||
|
let diff = float64(vector[globalD]) - float64(pq.codebooks[s][ci][d])
|
||||||
|
dist += diff * diff
|
||||||
|
if dist < bestDist:
|
||||||
|
bestDist = dist
|
||||||
|
bestCluster = int8(ci)
|
||||||
|
result.pqCodes[s] = bestCluster
|
||||||
|
|
||||||
|
proc distance*(pq: ProductQuantizer, qv: QuantizedVector, query: seq[float32]): float64 =
|
||||||
|
var sum: float64 = 0
|
||||||
|
for s in 0..<pq.nSubspaces:
|
||||||
|
let ci = qv.pqCodes[s]
|
||||||
|
for d in 0..<pq.subDim:
|
||||||
|
let globalD = s * pq.subDim + d
|
||||||
|
if globalD < query.len:
|
||||||
|
let diff = float64(pq.codebooks[s][ci][d]) - float64(query[globalD])
|
||||||
|
sum += diff * diff
|
||||||
|
return sqrt(sum)
|
||||||
|
|
||||||
|
# Binary quantization
|
||||||
|
proc binaryQuantize*(vector: seq[float32]): QuantizedVector =
|
||||||
|
result = QuantizedVector(kind: qkBinary)
|
||||||
|
let bits = vector.len
|
||||||
|
let words = (bits + 63) div 64
|
||||||
|
result.binData = newSeq[uint64](words)
|
||||||
|
for i in 0..<vector.len:
|
||||||
|
if vector[i] >= 0:
|
||||||
|
let wordIdx = i div 64
|
||||||
|
let bitIdx = i mod 64
|
||||||
|
result.binData[wordIdx] = result.binData[wordIdx] or (1'u64 shl bitIdx)
|
||||||
|
|
||||||
|
proc binaryDistance*(a, b: QuantizedVector): int =
|
||||||
|
result = 0
|
||||||
|
let words = min(a.binData.len, b.binData.len)
|
||||||
|
for i in 0..<words:
|
||||||
|
let val = a.binData[i] xor b.binData[i]
|
||||||
|
var cnt = 0
|
||||||
|
var v = val
|
||||||
|
while v != 0:
|
||||||
|
v = v and (v - 1)
|
||||||
|
inc cnt
|
||||||
|
result += cnt
|
||||||
|
|
||||||
|
proc compressionRatio*(sq: ScalarQuantizer): float64 =
|
||||||
|
if sq.bits == 8: return 4.0
|
||||||
|
if sq.bits == 4: return 8.0
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
proc compressionRatio*(pq: ProductQuantizer): float64 =
|
||||||
|
let origBytes = pq.dimensions * 4
|
||||||
|
let pqBytes = pq.nSubspaces # one byte per subspace code
|
||||||
|
return float64(origBytes) / float64(pqBytes)
|
||||||
@@ -6,16 +6,23 @@ import std/strutils
|
|||||||
import barabadb/core/types
|
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/storage/bloom
|
import barabadb/storage/bloom
|
||||||
import barabadb/storage/wal
|
import barabadb/storage/wal
|
||||||
import barabadb/storage/lsm
|
import barabadb/storage/lsm
|
||||||
|
import barabadb/storage/btree
|
||||||
import barabadb/query/lexer as lex
|
import barabadb/query/lexer as lex
|
||||||
import barabadb/query/ast
|
import barabadb/query/ast
|
||||||
import barabadb/query/parser
|
import barabadb/query/parser
|
||||||
|
import barabadb/query/ir as qir
|
||||||
import barabadb/vector/engine as vengine
|
import barabadb/vector/engine as vengine
|
||||||
|
import barabadb/vector/quant as vquant
|
||||||
import barabadb/graph/engine as gengine
|
import barabadb/graph/engine as gengine
|
||||||
|
import barabadb/graph/community as gcomm
|
||||||
import barabadb/fts/engine as fts
|
import barabadb/fts/engine as fts
|
||||||
import barabadb/protocol/wire
|
import barabadb/protocol/wire
|
||||||
|
import barabadb/protocol/pool
|
||||||
|
import barabadb/protocol/auth
|
||||||
import barabadb/schema/schema as schema
|
import barabadb/schema/schema as schema
|
||||||
|
|
||||||
suite "Core Types":
|
suite "Core Types":
|
||||||
@@ -465,3 +472,213 @@ suite "Schema System":
|
|||||||
check s.find("Person") >= 0
|
check s.find("Person") >= 0
|
||||||
check s.find("name") >= 0
|
check s.find("name") >= 0
|
||||||
check s.find("str") >= 0
|
check s.find("str") >= 0
|
||||||
|
|
||||||
|
suite "B-Tree Index":
|
||||||
|
test "Insert and get":
|
||||||
|
var btree = newBTreeIndex[string, string]()
|
||||||
|
btree.insert("key1", "value1")
|
||||||
|
btree.insert("key2", "value2")
|
||||||
|
check btree.get("key1") == @["value1"]
|
||||||
|
check btree.get("key2") == @["value2"]
|
||||||
|
check not btree.contains("nonexistent")
|
||||||
|
|
||||||
|
test "Scan range":
|
||||||
|
var btree = newBTreeIndex[string, string]()
|
||||||
|
for i in 0..9:
|
||||||
|
btree.insert("key" & $i, "val" & $i)
|
||||||
|
let results = btree.scan("key2", "key5")
|
||||||
|
check results.len == 4
|
||||||
|
|
||||||
|
test "Duplicate keys":
|
||||||
|
var btree = newBTreeIndex[string, string]()
|
||||||
|
btree.insert("a", "val1")
|
||||||
|
btree.insert("a", "val2")
|
||||||
|
let vals = btree.get("a")
|
||||||
|
check vals.len == 2
|
||||||
|
|
||||||
|
suite "Columnar Engine":
|
||||||
|
test "Column batch operations":
|
||||||
|
var batch = newColumnBatch()
|
||||||
|
var intCol = batch.addInt64Col("age")
|
||||||
|
var strCol = batch.addStringCol("name")
|
||||||
|
intCol.appendInt64(25)
|
||||||
|
intCol.appendInt64(30)
|
||||||
|
intCol.appendInt64(35)
|
||||||
|
strCol.appendString("Alice")
|
||||||
|
strCol.appendString("Bob")
|
||||||
|
strCol.appendString("Charlie")
|
||||||
|
check batch.rowCount() == 3
|
||||||
|
|
||||||
|
test "Aggregate operations":
|
||||||
|
var batch = newColumnBatch()
|
||||||
|
var col = batch.addInt64Col("age")
|
||||||
|
col.appendInt64(10)
|
||||||
|
col.appendInt64(20)
|
||||||
|
col.appendInt64(30)
|
||||||
|
check col.sumInt64() == 60
|
||||||
|
check col.avgInt64() - 20.0 < 0.001
|
||||||
|
check col.minInt64() == 10
|
||||||
|
check col.maxInt64() == 30
|
||||||
|
check col.count() == 3
|
||||||
|
|
||||||
|
test "RLE encoding":
|
||||||
|
let data = @[1'i64, 1, 1, 2, 2, 3, 3, 3, 3]
|
||||||
|
let encoded = rleEncode(data)
|
||||||
|
let decoded = rleDecode(encoded)
|
||||||
|
check decoded == data
|
||||||
|
|
||||||
|
test "Dictionary encoding":
|
||||||
|
let data = @["apple", "banana", "apple", "cherry", "banana"]
|
||||||
|
let encoded = dictEncode(data)
|
||||||
|
let decoded = dictDecode(encoded)
|
||||||
|
check decoded == data
|
||||||
|
check encoded.dict.len == 3
|
||||||
|
|
||||||
|
test "GroupBy":
|
||||||
|
var batch = newColumnBatch()
|
||||||
|
var deptCol = batch.addStringCol("department")
|
||||||
|
var salaryCol = batch.addInt64Col("salary")
|
||||||
|
deptCol.appendString("Engineering")
|
||||||
|
deptCol.appendString("Sales")
|
||||||
|
deptCol.appendString("Engineering")
|
||||||
|
salaryCol.appendInt64(100)
|
||||||
|
salaryCol.appendInt64(80)
|
||||||
|
salaryCol.appendInt64(120)
|
||||||
|
|
||||||
|
let groups = groupBy(batch, @["department"])
|
||||||
|
check groups.groups.len == 2 # unique departments
|
||||||
|
|
||||||
|
suite "Type Checker & IR":
|
||||||
|
test "Literal type inference":
|
||||||
|
var tc = newTypeChecker()
|
||||||
|
let lit = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkInt64, int64Val: 42))
|
||||||
|
let t = tc.inferExpr(lit, initTable[string, IRType]())
|
||||||
|
check t.name == "int64"
|
||||||
|
|
||||||
|
test "Binary operation type inference":
|
||||||
|
var tc = newTypeChecker()
|
||||||
|
let left = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkInt64, int64Val: 1))
|
||||||
|
let right = IRExpr(kind: irekLiteral, literal: IRLiteral(kind: vkInt64, int64Val: 2))
|
||||||
|
let bin = IRExpr(kind: irekBinary, binOp: irEq, binLeft: left, binRight: right)
|
||||||
|
let t = tc.inferExpr(bin, initTable[string, IRType]())
|
||||||
|
check t.name == "bool"
|
||||||
|
|
||||||
|
test "Aggregate type inference":
|
||||||
|
var tc = newTypeChecker()
|
||||||
|
let agg = IRExpr(kind: irekAggregate, aggOp: irCount)
|
||||||
|
let t = tc.inferExpr(agg, initTable[string, IRType]())
|
||||||
|
check t.name == "int64"
|
||||||
|
|
||||||
|
suite "Connection Pool":
|
||||||
|
test "Create pool and acquire connection":
|
||||||
|
var pool = newConnectionPool("127.0.0.1", 5432)
|
||||||
|
let conn = pool.acquire()
|
||||||
|
check conn != nil
|
||||||
|
check conn.host == "127.0.0.1"
|
||||||
|
check conn.port == 5432
|
||||||
|
pool.release(conn)
|
||||||
|
|
||||||
|
test "Pool stats":
|
||||||
|
var cfg = defaultPoolConfig()
|
||||||
|
cfg.minConnections = 1
|
||||||
|
cfg.maxConnections = 10
|
||||||
|
var pool = newConnectionPool("127.0.0.1", 5432, "default", cfg)
|
||||||
|
let conn1 = pool.acquire()
|
||||||
|
let (total, idle, inUse) = pool.stats()
|
||||||
|
check inUse == 1
|
||||||
|
pool.release(conn1)
|
||||||
|
let (t2, i2, u2) = pool.stats()
|
||||||
|
check u2 == 0
|
||||||
|
|
||||||
|
suite "Authentication":
|
||||||
|
test "Anonymous auth":
|
||||||
|
var am = newAuthManager()
|
||||||
|
let result = am.validateCredentials(AuthCredentials(authMethod: amNone))
|
||||||
|
check result.authenticated
|
||||||
|
check result.username == "anonymous"
|
||||||
|
|
||||||
|
test "Token auth":
|
||||||
|
var am = newAuthManager("mysecretkey")
|
||||||
|
let token = am.createToken(JWTClaims(sub: "user1", role: "admin"))
|
||||||
|
let result = am.validateCredentials(AuthCredentials(
|
||||||
|
authMethod: amToken, payload: token))
|
||||||
|
check result.authenticated
|
||||||
|
|
||||||
|
test "Invalid token":
|
||||||
|
var am = newAuthManager("mysecretkey")
|
||||||
|
let result = am.validateCredentials(AuthCredentials(
|
||||||
|
authMethod: amToken, payload: "invalid_token"))
|
||||||
|
check not result.authenticated
|
||||||
|
|
||||||
|
suite "Vector Quantization":
|
||||||
|
test "Scalar quantization 8-bit":
|
||||||
|
var sq = newScalarQuantizer(4, bits = 8)
|
||||||
|
let vectors = @[@[1.0'f32, 2.0'f32, 3.0'f32, 4.0'f32],
|
||||||
|
@[5.0'f32, 6.0'f32, 7.0'f32, 8.0'f32]]
|
||||||
|
sq.train(vectors)
|
||||||
|
let qv = sq.encode(@[3.0'f32, 4.0'f32, 5.0'f32, 6.0'f32])
|
||||||
|
check qv.kind == qkScalar8
|
||||||
|
check qv.int8Data.len == 4
|
||||||
|
|
||||||
|
test "Scalar quantization 4-bit":
|
||||||
|
var sq = newScalarQuantizer(4, bits = 4)
|
||||||
|
let vectors = @[@[1.0'f32, 2.0'f32, 3.0'f32, 4.0'f32]]
|
||||||
|
sq.train(vectors)
|
||||||
|
let qv = sq.encode(@[3.0'f32, 4.0'f32, 5.0'f32, 6.0'f32])
|
||||||
|
check qv.kind == qkScalar4
|
||||||
|
check qv.int4Data.len == 2
|
||||||
|
|
||||||
|
test "Product quantization":
|
||||||
|
var pq = newProductQuantizer(8, nSubspaces = 4, nClusters = 16)
|
||||||
|
var vectors: seq[seq[float32]] = @[]
|
||||||
|
for i in 0..<50:
|
||||||
|
var v: seq[float32] = @[]
|
||||||
|
for j in 0..<8:
|
||||||
|
v.add(float32(i * 8 + j) * 0.1)
|
||||||
|
vectors.add(v)
|
||||||
|
pq.train(vectors, nIterations = 5)
|
||||||
|
let qv = pq.encode(vectors[0])
|
||||||
|
check qv.kind == qkProduct
|
||||||
|
check qv.pqCodes.len == 4
|
||||||
|
|
||||||
|
test "Binary quantization":
|
||||||
|
let v = @[1.0'f32, -1.0'f32, 0.5'f32, -0.5'f32]
|
||||||
|
let qv = binaryQuantize(v)
|
||||||
|
check qv.kind == qkBinary
|
||||||
|
check qv.binData.len == 1
|
||||||
|
|
||||||
|
suite "Louvain Community Detection":
|
||||||
|
test "Detect communities in simple graph":
|
||||||
|
var g = gengine.newGraph()
|
||||||
|
# Create two communities
|
||||||
|
let n1 = gengine.addNode(g, "A")
|
||||||
|
let n2 = gengine.addNode(g, "B")
|
||||||
|
let n3 = gengine.addNode(g, "C")
|
||||||
|
let n4 = gengine.addNode(g, "D")
|
||||||
|
# Community 1: fully connected
|
||||||
|
discard gengine.addEdge(g, n1, n2)
|
||||||
|
discard gengine.addEdge(g, n2, n3)
|
||||||
|
discard gengine.addEdge(g, n1, n3)
|
||||||
|
# Community 2
|
||||||
|
discard gengine.addEdge(g, n3, n4) # single connection
|
||||||
|
|
||||||
|
let result = louvain(g)
|
||||||
|
check result.communities.len > 0
|
||||||
|
check result.numCommunities >= 1
|
||||||
|
|
||||||
|
test "Pattern matching":
|
||||||
|
var g = gengine.newGraph()
|
||||||
|
let a = gengine.addNode(g, "Person", {"name": "Alice"}.toTable)
|
||||||
|
let b = gengine.addNode(g, "Person", {"name": "Bob"}.toTable)
|
||||||
|
let c = gengine.addNode(g, "Person", {"name": "Charlie"}.toTable)
|
||||||
|
discard gengine.addEdge(g, a, b, "knows")
|
||||||
|
discard gengine.addEdge(g, b, c, "knows")
|
||||||
|
discard gengine.addEdge(g, a, c, "knows")
|
||||||
|
|
||||||
|
var pattern = newGraphPattern()
|
||||||
|
pattern.addNode(0, "Person", {"name": "Alice"}.toTable)
|
||||||
|
pattern.addNode(1, "Person")
|
||||||
|
pattern.addEdge(0, 1, "knows")
|
||||||
|
|
||||||
|
let matches = matchPattern(g, pattern)
|
||||||
|
check matches.len >= 1
|
||||||
|
|||||||
Reference in New Issue
Block a user