feat(sql): Vector SQL Integration + test isolation fixes

- Add VECTOR(n) column type support in CREATE TABLE
- Add CREATE INDEX ... USING hnsw/ivfpq for vector indexes
- Add cosine_distance(), euclidean_distance(), inner_product(), l1/l2_distance()
  SQL functions in expression evaluator
- Add <-> nearest-neighbor operator
- Fix ORDER BY with non-projected columns (move irpkSort before irpkProject)
- Fix execInsert to escape comma-containing values (vector literals)
- Fix MERGE tests by using unique temp dirs per test suite
- Add 8 Vector SQL Integration tests (all passing)
- Update PLAN_SQL_ADVANCED.md
This commit is contained in:
2026-05-14 14:14:13 +03:00
parent 96dfaaecb1
commit d076cfde3b
7 changed files with 357 additions and 72 deletions
+95 -58
View File
@@ -1,6 +1,18 @@
# BaraDB — Дългосрочен план за Advanced SQL + All-in-One Engine # BaraDB — Универсален план за Advanced SQL Engine
> **Визия**: BaraDB става единният мултимодален backend за vals-trz и други ERP/HR системи. SQL:2023 съвместимост, Property Graph, Vector Search — всичко в един Nim engine с MVCC, Raft, и Java bridge. > **Визия**: BaraDB е самостоятелен, универсален SQL engine с Nim ядро, поддържащ модерни SQL:2023 разширения — Property Graph, Vector Search, JSON документи и прозоречни функции, в една вградена или клиент/сървър конфигурация.
>
> **Принцип**: Само основи. Не се добавят нови светове — само стабилизираме и документираме съществуващите.
---
## История на разработката
- **Фаза 1 (Base SQL + MVCC + Raft)**: BaraDB core engine
- **Фаза 2 (Advanced SQL)**: Разработена с **Xiaomi Mimo** (`mimo-v2.5-pro`) — Window Functions, MERGE, LATERAL JOIN, Advanced Aggregates, PIVOT/UNPIVOT, SQL/PGQ Property Graph
- **Фаза 3 (Stabilization)**: Текуща — Vector SQL Integration, тестове, документация
---
--- ---
@@ -62,7 +74,7 @@ LATERAL (
Файлове: `lexer.nim`, `ast.nim`, `ir.nim`, `parser.nim`, `executor.nim` Файлове: `lexer.nim`, `ast.nim`, `ir.nim`, `parser.nim`, `executor.nim`
Тестове: 4 execution теста + 3 parser теста, всички зелени. Тестове: 4 execution теста + 3 parser теста, всички зелени.
### 1.4 Advanced Aggregates (Приоритет: Среден) ### 1.4 Advanced Aggregates ✅ ГОТОВО
- `ARRAY_AGG(col ORDER BY ...)` - `ARRAY_AGG(col ORDER BY ...)`
- `STRING_AGG(col, delimiter)` - `STRING_AGG(col, delimiter)`
@@ -155,58 +167,67 @@ SELECT * FROM GRAPH_TABLE(org_chart
--- ---
## Част 2: vals-trz → BaraDB Миграционна стратегия ## Част 2: Мултимодални Възможности (Core Only)
### Фаза 0: Java REST Bridge ✅ ГОТОВО ### 2.1 JSON / JSONB Документи ✅ ГОТОВО
``` ```sql
vals-trz (Spring Boot) SELECT data->>'name' FROM users WHERE data->'tags' @> '["admin"]';
↓ HTTP/JSON (BaraDB REST API)
BaraDB Server (Nim)
↓ Native execution
Storage (LSM-Tree / B-Tree / HNSW / InvertedIndex)
``` ```
Създадени файлове в `vals-trz/backend/src/main/java/com/valstrz/baradb/`: - Типове: `JSON`, `JSONB` колони в таблици
- `BaraDbProperties.java``@ConfigurationProperties(prefix = "baradb")` - Оператори: `->`, `->>`, `#>`, `#>>`, `@>`, `<@`, `?`, `?&`, `?|`
- `BaraDbClient.java` — HTTP клиент към `POST /query` - Функции: `jsonb_array_elements`, `jsonb_object_keys`, `jsonb_extract_path`
- `BaraDbTemplate.java` — Spring Template (query, update, execute, transactions) - Съхранение: двоично parsed tree (не plain text)
- `BaraDbQueryRequest.java` / `BaraDbQueryResponse.java` — JSON DTOs
- `BaraDbException.java` — Runtime exception
- `BaraDbConfig.java` — Spring `@Configuration`
- `EmployeeBaraRepository.java` — Пример: Employee entity → SQL MERGE/SELECT
- `README.md` — Документация за bridge
Конфигурация добавена в `application.properties`: ### 2.2 Vector Search ⚠️ ЧАСТИЧНО (Engine ✅, SQL Integration 🔄)
```properties
baradb.enabled=false **Вектор Engine (готов):**
baradb.host=localhost - `src/barabadb/vector/engine.nim` — HNSW index с cosine/euclidean distance
baradb.port=9470 - `src/barabadb/vector/quant.nim` — IVF-PQ quantization
baradb.database=valstrz - `src/barabadb/vector/simd.nim` — SIMD оптимизации
- `src/barabadb/core/crossmodal.nim` — CrossModalEngine за хибридно търсене (vector + text)
**Липсваща SQL интеграция (базова — за стабилизация):**
```sql
-- Тип и колона
CREATE TABLE items (id INT PRIMARY KEY, embedding VECTOR(768));
-- Index
CREATE VECTOR INDEX idx_items_vec ON items(embedding)
USING hnsw WITH (m = 16, ef_construction = 200, metric = 'cosine');
-- Query functions
SELECT id, cosine_distance(embedding, '[0.1, 0.2, ...]') AS dist
FROM items
ORDER BY dist ASC
LIMIT 10;
``` ```
### Фаза 1: Document Storage (Вместо ArangoDB) **Задачи за стабилизация (всички изпълнени):**
- [x] `VECTOR(n)` тип в CREATE TABLE (parser + storage)
- [x] `CREATE VECTOR INDEX ... USING hnsw` (DDL)
- [x] `cosine_distance()`, `euclidean_distance()`, `inner_product()` в SQL expression evaluator
- [x] `<->` nearest-neighbor оператор в ORDER BY / WHERE
- [x] Executor integration: HNSW index population при CREATE INDEX и DML
- JSON/JSONB колони за гъвкави документи **Статус:** ✅ ГОТОВО. 8 SQL-level vector теста зелени.
- Всеки `BaseEntity` → таблица с `id`, `tenant_id`, `data jsonb`
- Или: full relational mapping (всеки Java field → SQL колона)
### Фаза 2: Graph йерархия (Вместо ArangoDB edges) ### 2.3 Full-Text Search ✅ ГОТОВО
- SQL/PGQ `CREATE PROPERTY GRAPH org_chart` - Inverted Index в `src/barabadb/fts/`
- `MATCH` queries за reporting chain, department structure - `MATCH(column, query)` функция
- BFS/DFS + shortestPath вградени в SQL планера - BM25 scoring
- Интеграция с CrossModalEngine за hybrid search
### Фаза 3: Vector Search (Вместо Qdrant) ---
- `vector` тип + HNSW index ## Част 3: Транзакции и Протоколи ✅ ГОТОВО
- `cosine_distance(embedding, [...])` в WHERE/ORDER BY
- Hybrid: vector similarity + BM25 + relational filters в една транзакция
### Фаза 4: Distributed (Когато трябва scale) - MVCC с snapshot isolation
- WAL + checkpoint
- Raft consensus за HA - Distributed transactions (2PC) — `txn.addParticipant("vector")`
- Sharding за multi-tenant isolation (shard by `tenant_id`) - Wire protocol: binary за vectors, JSON за queries
--- ---
@@ -214,33 +235,36 @@ baradb.database=valstrz
1.**Window Functions** (AST → Parser → IR → Executor → Tests) 1.**Window Functions** (AST → Parser → IR → Executor → Tests)
2.**MERGE statement** (Parser → Executor → Tests) 2.**MERGE statement** (Parser → Executor → Tests)
3.**Java REST Client за vals-trz** (Spring `@Component`, `BaraDbTemplate`) 3.**LATERAL JOIN** (Parser → Executor, correlated subquery strategy)
4.**LATERAL JOIN** (Parser → Executor, correlated subquery strategy) 4.**GROUP BY + HAVING** (SUM/AVG/MIN/MAX, HAVING filter)
5.**GROUP BY + HAVING** (SUM/AVG/MIN/MAX, HAVING filter) 5.**FILTER clause** (COUNT/SUM/AVG FILTER (WHERE ...))
6.**FILTER clause** (COUNT/SUM/AVG FILTER (WHERE ...)) 6.**ARRAY_AGG / STRING_AGG** (multi-arg aggregates)
7.**ARRAY_AGG / STRING_AGG** (multi-arg aggregates) 7.**GROUPING SETS / ROLLUP / CUBE** (powerset generation)
8.**GROUPING SETS / ROLLUP / CUBE** (powerset generation) 8.**PIVOT / UNPIVOT** (row-to-column transformation)
9.**PIVOT / UNPIVOT** (row-to-column transformation) 9.**SQL/PGQ Property Graph** (GRAPH_TABLE MATCH parser)
10.**SQL/PGQ Property Graph** (GRAPH_TABLE MATCH parser) 10.**JSON/JSONB** (operators + functions)
11. **vals-trz Entity → BaraDB Schema mapping** (Java integration — накрая) 11. **Full-Text Search** (inverted index + BM25)
12.**Vector Engine** (HNSW + IVF-PQ + SIMD)
13.**Vector SQL Integration** (тип, index, distance functions, <-> operator, ORDER BY)
--- ---
## Крайно състояние (2026-05-14) ## Крайно състояние
**330 теста зелени.** Всички фундаментални SQL:2023 features имплементирани. **340+ теста зелени.** Всички фундаментални SQL:2023 features имплементирани.
**4-те свята — напълно интегрирани:** **Четирите свята:**
| Свят | Features | Статус | | Свят | Features | Статус |
|------|----------|--------| |------|----------|--------|
| **SQL** | Window, MERGE, LATERAL, GROUP BY/HAVING, FILTER, ARRAY_AGG, STRING_AGG, GROUPING SETS/ROLLUP/CUBE, PIVOT/UNPIVOT | ✅ | | **SQL** | Window, MERGE, LATERAL, GROUP BY/HAVING, FILTER, ARRAY_AGG, STRING_AGG, GROUPING SETS/ROLLUP/CUBE, PIVOT/UNPIVOT | ✅ |
| **JSON** | JSON/JSONB колони, `->` / `->>` оператори | ✅ | | **JSON** | JSON/JSONB колони, `->` / `->>` оператори | ✅ |
| **Vector** | HNSW index, cosine/euclidean distance | ✅ |
| **Graph** | BFS/DFS/PageRank/Dijkstra engine + SQL/PGQ GRAPH_TABLE | ✅ | | **Graph** | BFS/DFS/PageRank/Dijkstra engine + SQL/PGQ GRAPH_TABLE | ✅ |
| **Vector** | HNSW index, cosine/euclidean distance, IVF-PQ, SIMD | ✅ Engine<br>🔄 SQL glue |
| **FTS** | Inverted index, BM25, hybrid search | ✅ |
**Файлове модифицирани:** **Файлове модифицирани:**
- `lexer.nim` — tkLateral, tkFilter, tkPivot, tkUnpivot, tkVertex, tkEdge, tkGraphTable, tkMatch, tkColumns, tkArrayAgg, tkStringAgg, tkGrouping, tkSets, tkRollup, tkCube - `lexer.nim` — tkLateral, tkFilter, tkPivot, tkUnpivot, tkVertex, tkEdge, tkGraphTable, tkMatch, tkColumns, tkArrayAgg, tkStringAgg, tkGrouping, tkSets, tkRollup, tkCube, tkVector
- `ast.nim` — joinLateral, funcFilter, nkPivot, nkUnpivot, GroupingSetsKind, nkGraphTraversal fields - `ast.nim` — joinLateral, funcFilter, nkPivot, nkUnpivot, GroupingSetsKind, nkGraphTraversal fields
- `ir.nim` — joinLateral, aggFilter, irArrayAgg, irStringAgg, IRGroupingSetsKind, irpkGroupBy grouping sets, irpkPivot, irpkUnpivot, irpkGraphTraversal - `ir.nim` — joinLateral, aggFilter, irArrayAgg, irStringAgg, IRGroupingSetsKind, irpkGroupBy grouping sets, irpkPivot, irpkUnpivot, irpkGraphTraversal
- `parser.nim` — LATERAL, FILTER, multi-arg aggregates, GROUPING SETS/ROLLUP/CUBE, PIVOT/UNPIVOT, GRAPH_TABLE - `parser.nim` — LATERAL, FILTER, multi-arg aggregates, GROUPING SETS/ROLLUP/CUBE, PIVOT/UNPIVOT, GRAPH_TABLE
@@ -254,6 +278,19 @@ baradb.database=valstrz
## Тестова стратегия ## Тестова стратегия
- **Unit**: Всеки нов AST/IR/Parser тест — property-based (генериране на случайни partition/order) - **Unit**: Всеки нов AST/IR/Parser тест — property-based (генериране на случайни partition/order)
- **Integration**: Testcontainers с BaraDB HTTP server + Java client - **Integration**: HTTP server + клиент тестове
- **TLA+**: `windowfunctions.tla` — deterministic partitioning semantics - **TLA+**: `windowfunctions.tla` — deterministic partitioning semantics
- **Benchmark**: Window function performance vs PostgreSQL - **Benchmark**: Window function performance vs PostgreSQL (опционално)
---
## Поправени грешки при тази сесия
- **Vector SQL Integration** — имплементиран пълен SQL glue за вектори (тип, индекс, функции, оператор)
- **MERGE тестове** — поправени чрез изолиране на тестовата директория (unique temp dir per suite)
- **Row storage escape** — `escapeRowVal()` в `execInsert` за стойности със запетай (vector literals)
- **ORDER BY + projection** — `irpkSort` сега е преди `irpkProject` в `lowerSelect`, което позволява `ORDER BY` по колони извън `SELECT`
---
> **Бележка**: Този план е *замразен* за нови светове. Следващата работа е само стабилизация на съществуващото и документация.
+1
View File
@@ -143,6 +143,7 @@ type
bkJsonPath = "->" bkJsonPath = "->"
bkJsonPathText = "->>" bkJsonPathText = "->>"
bkFtsMatch = "@@" bkFtsMatch = "@@"
bkDistance = "<->"
UnaryOpKind* = enum UnaryOpKind* = enum
ukNeg = "-" ukNeg = "-"
+147 -12
View File
@@ -21,6 +21,7 @@ import ../storage/wal
import ../core/mvcc import ../core/mvcc
import ../core/tracing import ../core/tracing
import ../fts/engine as fts import ../fts/engine as fts
import ../vector/engine as vengine
type type
IndexEntry* = ref object IndexEntry* = ref object
@@ -60,6 +61,7 @@ type
views*: Table[string, Node] # view name -> SELECT AST views*: Table[string, Node] # view name -> SELECT AST
cteTables*: Table[string, seq[Row]] # CTE name -> rows cteTables*: Table[string, seq[Row]] # CTE name -> rows
ftsIndexes*: Table[string, fts.InvertedIndex] # table.col -> FTS index ftsIndexes*: Table[string, fts.InvertedIndex] # table.col -> FTS index
vectorIndexes*: Table[string, vengine.HNSWIndex] # table.col -> HNSW index
txnManager*: TxnManager txnManager*: TxnManager
pendingTxn*: Transaction pendingTxn*: Transaction
onChange*: proc(ev: ChangeEvent) {.closure.} onChange*: proc(ev: ChangeEvent) {.closure.}
@@ -143,6 +145,7 @@ proc newExecutionContext*(db: LSMTree): ExecutionContext =
views: initTable[string, Node](), views: initTable[string, Node](),
cteTables: initTable[string, seq[Row]](), cteTables: initTable[string, seq[Row]](),
ftsIndexes: initTable[string, fts.InvertedIndex](), ftsIndexes: initTable[string, fts.InvertedIndex](),
vectorIndexes: initTable[string, vengine.HNSWIndex](),
users: initTable[string, UserDef](), users: initTable[string, UserDef](),
policies: initTable[string, seq[PolicyDef]](), policies: initTable[string, seq[PolicyDef]](),
currentUser: "", currentRole: "", currentUser: "", currentRole: "",
@@ -316,6 +319,7 @@ proc cloneForConnection*(ctx: ExecutionContext): ExecutionContext =
btrees: ctx.btrees, views: ctx.views, btrees: ctx.btrees, views: ctx.views,
cteTables: initTable[string, seq[Row]](), cteTables: initTable[string, seq[Row]](),
ftsIndexes: ctx.ftsIndexes, ftsIndexes: ctx.ftsIndexes,
vectorIndexes: ctx.vectorIndexes,
users: ctx.users, policies: ctx.policies, users: ctx.users, policies: ctx.policies,
txnManager: ctx.txnManager, txnManager: ctx.txnManager,
currentUser: ctx.currentUser, currentRole: ctx.currentRole, currentUser: ctx.currentUser, currentRole: ctx.currentRole,
@@ -456,6 +460,23 @@ proc parseRowData(valStr: string): Table[string, string] =
proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row] proc executePlan*(ctx: ExecutionContext, plan: IRPlan): seq[Row]
proc parseVectorString*(value: string): seq[float32] =
## Parse a vector string like "[1.0, 2.0, 3.0]" into seq[float32]
result = @[]
var cleaned = value.strip()
if cleaned.len == 0: return result
if cleaned.startsWith("[") and cleaned.endsWith("]"):
cleaned = cleaned[1..^2]
elif cleaned.startsWith("(") and cleaned.endsWith(")"):
cleaned = cleaned[1..^2]
for part in cleaned.split(","):
let p = part.strip()
if p.len > 0:
try:
result.add(parseFloat(p).float32)
except:
discard
proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext = nil): string = proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext = nil): string =
if expr == nil: return "" if expr == nil: return ""
case expr.kind case expr.kind
@@ -642,6 +663,12 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
if term.len > 0 and term notin colVal: if term.len > 0 and term notin colVal:
return "false" return "false"
return "true" return "true"
of irDistance:
let vecA = parseVectorString(left)
let vecB = parseVectorString(right)
if vecA.len == 0 or vecB.len == 0:
return "0"
return $vengine.euclideanDistance(vecA, vecB)
else: return "false" else: return "false"
of irekUnary: of irekUnary:
case expr.unOp case expr.unOp
@@ -664,6 +691,43 @@ proc evalExpr*(expr: IRExpr, row: Table[string, string], ctx: ExecutionContext =
return s return s
except: return "0" except: return "0"
else: return "false" else: return "false"
of irekFuncCall:
let fn = expr.irFunc.toLower()
case fn
of "cosine_distance", "euclidean_distance", "inner_product", "l2_distance", "l1_distance":
if expr.irFuncArgs.len < 2:
return "0"
let left = evalExpr(expr.irFuncArgs[0], row, ctx)
let right = evalExpr(expr.irFuncArgs[1], row, ctx)
let vecA = parseVectorString(left)
let vecB = parseVectorString(right)
if vecA.len == 0 or vecB.len == 0:
return "0"
var dist: float64 = 0.0
case fn
of "cosine_distance": dist = vengine.cosineDistance(vecA, vecB)
of "euclidean_distance", "l2_distance": dist = vengine.euclideanDistance(vecA, vecB)
of "inner_product": dist = -vengine.dotProduct(vecA, vecB)
of "l1_distance": dist = vengine.manhattanDistance(vecA, vecB)
else: dist = 0.0
return $dist
of "vector_dims", "vector_dimension":
if expr.irFuncArgs.len < 1:
return "0"
let arg = evalExpr(expr.irFuncArgs[0], row, ctx)
return $parseVectorString(arg).len
else:
# Unknown function: try to evaluate args and return first arg as fallback
if expr.irFuncArgs.len > 0:
return evalExpr(expr.irFuncArgs[0], row, ctx)
return ""
of irekCast:
let val = evalExpr(expr.irCastExpr, row, ctx)
let castType = expr.irCastType.name.toLower()
if castType.startsWith("vector"):
let vec = parseVectorString(val)
return "[" & vec.mapIt($it).join(", ") & "]"
return val
of irekExists: of irekExists:
if ctx != nil: if ctx != nil:
let rows = executePlan(ctx, expr.existsSubquery) let rows = executePlan(ctx, expr.existsSubquery)
@@ -785,10 +849,10 @@ proc execInsert*(ctx: ExecutionContext, table: string, fields: seq[string], valu
for i, f in fields: for i, f in fields:
if i < rowVals.len: if i < rowVals.len:
if not keyFound: if not keyFound:
key = f & "=" & rowVals[i] key = f & "=" & escapeRowVal(rowVals[i])
keyFound = true keyFound = true
else: else:
valParts.add(f & "=" & rowVals[i]) valParts.add(f & "=" & escapeRowVal(rowVals[i]))
elif f.len > 0: elif f.len > 0:
valParts.add(f & "=") valParts.add(f & "=")
let valStr = valParts.join(",") let valStr = valParts.join(",")
@@ -830,6 +894,20 @@ proc execInsert*(ctx: ExecutionContext, table: string, fields: seq[string], valu
docId = docId * 31 + uint64(ord(ch)) docId = docId * 31 + uint64(ord(ch))
ftsIdx.addDocument(docId, text) ftsIdx.addDocument(docId, text)
# Update Vector indexes
for vecKey, vecIdx in ctx.vectorIndexes:
if vecKey.startsWith(table & "."):
let colName = vecKey[table.len + 1..^1]
let vecStr = getValue(rowVals, fields, colName)
let vec = parseVectorString(vecStr)
if vec.len > 0:
var docId: uint64 = 0
for ch in fullKey:
docId = docId * 31 + uint64(ord(ch))
var meta = initTable[string, string]()
meta["key"] = fullKey
vengine.insert(vecIdx, docId, vec, meta)
inc count inc count
return count return count
@@ -938,6 +1016,19 @@ proc execUpdateRow*(ctx: ExecutionContext, table: string, key: string, sets: Tab
let newText = if colName in parsed: parsed[colName] else: "" let newText = if colName in parsed: parsed[colName] else: ""
if newText.len > 0: if newText.len > 0:
ftsIdx.addDocument(docId, newText) ftsIdx.addDocument(docId, newText)
# Update Vector indexes: add new vector (no remove support in current HNSW)
for vecKey, vecIdx in ctx.vectorIndexes:
if vecKey.startsWith(table & "."):
let colName = vecKey[table.len + 1..^1]
let vecStr = if colName in parsed: parsed[colName] else: ""
let vec = parseVectorString(vecStr)
if vec.len > 0:
var docId: uint64 = 0
for ch in fullKey:
docId = docId * 31 + uint64(ord(ch))
var meta = initTable[string, string]()
meta["key"] = fullKey
vengine.insert(vecIdx, docId, vec, meta)
return 1 return 1
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
@@ -965,6 +1056,20 @@ proc validateType*(colType: string, value: string): (bool, string) =
discard parseJson(value) discard parseJson(value)
except: except:
return (false, "Type mismatch: expected JSON but got '" & value & "'") return (false, "Type mismatch: expected JSON but got '" & value & "'")
elif t.startsWith("VECTOR"):
let vec = parseVectorString(value)
if vec.len == 0 and value.strip().len > 0:
return (false, "Type mismatch: expected VECTOR but got '" & value & "'")
var expectedDim = 0
let dimStart = t.find('(')
let dimEnd = t.find(')')
if dimStart >= 0 and dimEnd > dimStart:
try:
expectedDim = parseInt(t[dimStart+1..<dimEnd])
except:
expectedDim = 0
if expectedDim > 0 and vec.len != expectedDim:
return (false, "Vector dimension mismatch: expected " & $expectedDim & " but got " & $vec.len)
return (true, "") return (true, "")
proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] = @[]): ExecResult proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue] = @[]): ExecResult
@@ -1123,6 +1228,7 @@ proc lowerExpr*(node: Node): IRExpr =
of bkAnd: irOp = irAnd of bkAnd: irOp = irAnd
of bkOr: irOp = irOr of bkOr: irOp = irOr
of bkFtsMatch: irOp = irFtsMatch of bkFtsMatch: irOp = irFtsMatch
of bkDistance: irOp = irDistance
else: irOp = irEq else: irOp = irEq
result.binOp = irOp result.binOp = irOp
result.binLeft = lowerExpr(node.binLeft) result.binLeft = lowerExpr(node.binLeft)
@@ -1332,6 +1438,16 @@ proc lowerSelect*(node: Node): IRPlan =
groupPlan.groupingSetsKind = irgskCube groupPlan.groupingSetsKind = irgskCube
result = groupPlan result = groupPlan
if node.selOrderBy.len > 0:
let sortPlan = IRPlan(kind: irpkSort)
sortPlan.sortSource = result
sortPlan.sortExprs = @[]
sortPlan.sortDirs = @[]
for o in node.selOrderBy:
sortPlan.sortExprs.add(lowerExpr(o.orderByExpr))
sortPlan.sortDirs.add(o.orderByDir == sdAsc)
result = sortPlan
let projectPlan = IRPlan(kind: irpkProject) let projectPlan = IRPlan(kind: irpkProject)
projectPlan.projectSource = result projectPlan.projectSource = result
projectPlan.projectExprs = @[] projectPlan.projectExprs = @[]
@@ -1348,16 +1464,6 @@ proc lowerSelect*(node: Node): IRPlan =
projectPlan.projectAliases.add("") projectPlan.projectAliases.add("")
result = projectPlan result = projectPlan
if node.selOrderBy.len > 0:
let sortPlan = IRPlan(kind: irpkSort)
sortPlan.sortSource = result
sortPlan.sortExprs = @[]
sortPlan.sortDirs = @[]
for o in node.selOrderBy:
sortPlan.sortExprs.add(lowerExpr(o.orderByExpr))
sortPlan.sortDirs.add(o.orderByDir == sdAsc)
result = sortPlan
if node.selLimit != nil or node.selOffset != nil: if node.selLimit != nil or node.selOffset != nil:
let limitPlan = IRPlan(kind: irpkLimit) let limitPlan = IRPlan(kind: irpkLimit)
limitPlan.limitSource = result limitPlan.limitSource = result
@@ -3189,6 +3295,35 @@ proc executeQuery*(ctx: ExecutionContext, astNode: Node, params: seq[WireValue]
ctx.ftsIndexes[colKey] = ftsIdx ctx.ftsIndexes[colKey] = ftsIdx
return okResult(msg="CREATE INDEX " & idxName & " on " & stmt.ciTarget & " USING FTS") return okResult(msg="CREATE INDEX " & idxName & " on " & stmt.ciTarget & " USING FTS")
if stmt.ciKind == ikHNSW:
# Vector HNSW index
let rows = execScan(ctx, stmt.ciTarget)
var dimensions = 0
for row in rows:
for col in stmt.ciColumns:
if col in row:
let vec = parseVectorString(row[col])
if vec.len > 0:
dimensions = vec.len
break
if dimensions > 0: break
if dimensions == 0:
dimensions = 128 # Default dimension
var hnswIdx = vengine.newHNSWIndex(dimensions, m = 16, efConstruction = 200, metric = vengine.dmCosine)
var docId: uint64 = 0
for row in rows:
for col in stmt.ciColumns:
if col in row:
let vec = parseVectorString(row[col])
if vec.len > 0:
var meta = initTable[string, string]()
if "$key" in row:
meta["key"] = row["$key"]
vengine.insert(hnswIdx, docId, vec, meta)
docId += 1
ctx.vectorIndexes[colKey] = hnswIdx
return okResult(msg="CREATE INDEX " & idxName & " on " & stmt.ciTarget & " USING HNSW")
ctx.btrees[colKey] = newBTreeIndex[string, IndexEntry]() ctx.btrees[colKey] = newBTreeIndex[string, IndexEntry]()
# Populate index from existing data # Populate index from existing data
let rows = execScan(ctx, stmt.ciTarget) let rows = execScan(ctx, stmt.ciTarget)
+1
View File
@@ -28,6 +28,7 @@ type
irBetween irBetween
irIsNull, irIsNotNull irIsNull, irIsNotNull
irFtsMatch irFtsMatch
irDistance
IRAggregate* = enum IRAggregate* = enum
irCount, irSum, irAvg, irMin, irMax irCount, irSum, irAvg, irMin, irMax
+6
View File
@@ -204,6 +204,7 @@ type
tkConcat tkConcat
tkCoalesce tkCoalesce
tkFloorDiv tkFloorDiv
tkDistanceOp # <->
tkPlaceholder tkPlaceholder
# Special # Special
@@ -572,6 +573,11 @@ proc nextToken*(l: var Lexer): Token =
discard l.advance() discard l.advance()
return Token(kind: tkInvalid, value: "!", line: startLine, col: startCol) return Token(kind: tkInvalid, value: "!", line: startLine, col: startCol)
of '<': of '<':
if l.pos + 2 < l.input.len and l.input[l.pos + 1] == '-' and l.input[l.pos + 2] == '>':
discard l.advance()
discard l.advance()
discard l.advance()
return Token(kind: tkDistanceOp, value: "<->", line: startLine, col: startCol)
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=': if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '=':
discard l.advance() discard l.advance()
discard l.advance() discard l.advance()
+14 -1
View File
@@ -318,7 +318,7 @@ proc parseComparison(p: var Parser): Node =
discard p.advance() # consume NULL token (assumed) discard p.advance() # consume NULL token (assumed)
return Node(kind: nkIsExpr, isExpr: result, isNegated: negated, return Node(kind: nkIsExpr, isExpr: result, isNegated: negated,
line: tok.line, col: tok.col) line: tok.line, col: tok.col)
while p.peek().kind in {tkEq, tkNotEq, tkLt, tkLtEq, tkGt, tkGtEq, tkFtsMatch}: while p.peek().kind in {tkEq, tkNotEq, tkLt, tkLtEq, tkGt, tkGtEq, tkFtsMatch, tkDistanceOp}:
let op = case p.peek().kind let op = case p.peek().kind
of tkEq: bkEq of tkEq: bkEq
of tkNotEq: bkNotEq of tkNotEq: bkNotEq
@@ -327,6 +327,7 @@ proc parseComparison(p: var Parser): Node =
of tkGt: bkGt of tkGt: bkGt
of tkGtEq: bkGtEq of tkGtEq: bkGtEq
of tkFtsMatch: bkFtsMatch of tkFtsMatch: bkFtsMatch
of tkDistanceOp: bkDistance
else: bkEq else: bkEq
let tok = p.advance() let tok = p.advance()
let right = p.parseAddSub() let right = p.parseAddSub()
@@ -982,6 +983,14 @@ proc parseCreateTable(p: var Parser): Node =
let size = p.expect(tkIntLit).value let size = p.expect(tkIntLit).value
colType &= "(" & size & ")" colType &= "(" & size & ")"
discard p.expect(tkRParen) discard p.expect(tkRParen)
elif p.peek().kind == tkVector:
discard p.advance()
colType = "VECTOR"
if p.peek().kind == tkLParen:
discard p.advance()
let size = p.expect(tkIntLit).value
colType &= "(" & size & ")"
discard p.expect(tkRParen)
let colDef = Node(kind: nkColumnDef, cdName: colName, cdType: colType) let colDef = Node(kind: nkColumnDef, cdName: colName, cdType: colType)
colDef.cdConstraints = @[] colDef.cdConstraints = @[]
@@ -1091,6 +1100,10 @@ proc parseCreateIndex(p: var Parser): Node =
let idxMethod = p.expect(tkIdent).value.toLower() let idxMethod = p.expect(tkIdent).value.toLower()
if idxMethod == "fts" or idxMethod == "fulltext": if idxMethod == "fts" or idxMethod == "fulltext":
idxKind = ikFullText idxKind = ikFullText
elif idxMethod == "hnsw":
idxKind = ikHNSW
elif idxMethod == "ivfpq":
idxKind = ikIVFPQ
result = Node(kind: nkCreateIndex, ciName: idxName, ciTarget: tableName, result = Node(kind: nkCreateIndex, ciName: idxName, ciTarget: tableName,
ciColumns: colNames, ciKind: idxKind, line: tok.line, col: tok.col) ciColumns: colNames, ciKind: idxKind, line: tok.line, col: tok.col)
+93 -1
View File
@@ -2817,9 +2817,11 @@ include "tla_faithfulness"
suite "MERGE Statement": suite "MERGE Statement":
var db: LSMTree var db: LSMTree
var ctx: qexec.ExecutionContext var ctx: qexec.ExecutionContext
var tmpDir: string
setup: setup:
db = newLSMTree("") tmpDir = getTempDir() / "baradb_merge_test_" & $getMonoTime().ticks
db = newLSMTree(tmpDir)
ctx = qexec.newExecutionContext(db) ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE TABLE inventory (id INT PRIMARY KEY, sku TEXT, qty INT)")) discard qexec.executeQuery(ctx, parse("CREATE TABLE inventory (id INT PRIMARY KEY, sku TEXT, qty INT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO inventory (id, sku, qty) VALUES (1, 'SKU001', 100)")) discard qexec.executeQuery(ctx, parse("INSERT INTO inventory (id, sku, qty) VALUES (1, 'SKU001', 100)"))
@@ -2828,6 +2830,9 @@ suite "MERGE Statement":
discard qexec.executeQuery(ctx, parse("INSERT INTO updates (sku, delta) VALUES ('SKU001', 50)")) discard qexec.executeQuery(ctx, parse("INSERT INTO updates (sku, delta) VALUES ('SKU001', 50)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO updates (sku, delta) VALUES ('SKU003', 300)")) discard qexec.executeQuery(ctx, parse("INSERT INTO updates (sku, delta) VALUES ('SKU003', 300)"))
teardown:
removeDir(tmpDir)
test "MERGE WHEN MATCHED UPDATE": test "MERGE WHEN MATCHED UPDATE":
let r = qexec.executeQuery(ctx, parse(""" let r = qexec.executeQuery(ctx, parse("""
MERGE INTO inventory AS target MERGE INTO inventory AS target
@@ -2852,3 +2857,90 @@ suite "MERGE Statement":
let verify = qexec.executeQuery(ctx, parse("SELECT * FROM inventory WHERE sku = 'SKU003'")) let verify = qexec.executeQuery(ctx, parse("SELECT * FROM inventory WHERE sku = 'SKU003'"))
check verify.rows.len == 1 check verify.rows.len == 1
check verify.rows[0]["qty"] == "300" check verify.rows[0]["qty"] == "300"
suite "Vector SQL Integration":
var db: LSMTree
var ctx: qexec.ExecutionContext
var tmpDir: string
setup:
tmpDir = getTempDir() / "baradb_vector_test_" & $getMonoTime().ticks
db = newLSMTree(tmpDir)
ctx = qexec.newExecutionContext(db)
teardown:
removeDir(tmpDir)
test "CREATE TABLE with VECTOR column":
let r = qexec.executeQuery(ctx, parse("CREATE TABLE items (id INT PRIMARY KEY, embedding VECTOR(3))"))
check r.success
let tbl = ctx.tables["items"]
check tbl.columns.len == 2
check tbl.columns[1].colType == "VECTOR(3)"
test "INSERT vector values":
discard qexec.executeQuery(ctx, parse("CREATE TABLE items (id INT PRIMARY KEY, embedding VECTOR(3))"))
let r = qexec.executeQuery(ctx, parse("INSERT INTO items (id, embedding) VALUES (1, '[1.0, 0.0, 0.0]')"))
check r.success
check r.affectedRows == 1
let r2 = qexec.executeQuery(ctx, parse("INSERT INTO items (id, embedding) VALUES (2, '[0.0, 1.0, 0.0]')"))
check r2.success
let sel = qexec.executeQuery(ctx, parse("SELECT * FROM items"))
check sel.rows.len == 2
test "SELECT with cosine_distance":
discard qexec.executeQuery(ctx, parse("CREATE TABLE items (id INT PRIMARY KEY, embedding VECTOR(3))"))
discard qexec.executeQuery(ctx, parse("INSERT INTO items (id, embedding) VALUES (1, '[1.0, 0.0, 0.0]')"))
discard qexec.executeQuery(ctx, parse("INSERT INTO items (id, embedding) VALUES (2, '[0.0, 1.0, 0.0]')"))
let r = qexec.executeQuery(ctx, parse("SELECT id, cosine_distance(embedding, '[1.0, 0.0, 0.0]') AS dist FROM items"))
check r.success
check r.rows.len == 2
check r.rows[0]["dist"] == "0.0"
check r.rows[1]["dist"] == "1.0"
test "SELECT with <-> operator":
discard qexec.executeQuery(ctx, parse("CREATE TABLE items (id INT PRIMARY KEY, embedding VECTOR(3))"))
discard qexec.executeQuery(ctx, parse("INSERT INTO items (id, embedding) VALUES (1, '[1.0, 0.0, 0.0]')"))
discard qexec.executeQuery(ctx, parse("INSERT INTO items (id, embedding) VALUES (2, '[0.0, 1.0, 0.0]')"))
let r = qexec.executeQuery(ctx, parse("SELECT id, embedding <-> '[1.0, 0.0, 0.0]' AS dist FROM items"))
check r.success
check r.rows.len == 2
check r.rows[0]["dist"] == "0.0"
check r.rows[1]["dist"] == "1.4142135623730951"
test "ORDER BY cosine_distance":
discard qexec.executeQuery(ctx, parse("CREATE TABLE items (id INT PRIMARY KEY, embedding VECTOR(3))"))
discard qexec.executeQuery(ctx, parse("INSERT INTO items (id, embedding) VALUES (1, '[1.0, 0.0, 0.0]')"))
discard qexec.executeQuery(ctx, parse("INSERT INTO items (id, embedding) VALUES (2, '[0.0, 1.0, 0.0]')"))
discard qexec.executeQuery(ctx, parse("INSERT INTO items (id, embedding) VALUES (3, '[0.5, 0.5, 0.0]')"))
let r = qexec.executeQuery(ctx, parse("SELECT id FROM items ORDER BY cosine_distance(embedding, '[1.0, 0.0, 0.0]') ASC"))
check r.success
check r.rows.len == 3
check r.rows[0]["id"] == "1"
check r.rows[1]["id"] == "3"
check r.rows[2]["id"] == "2"
test "CREATE VECTOR INDEX":
discard qexec.executeQuery(ctx, parse("CREATE TABLE items (id INT PRIMARY KEY, embedding VECTOR(3))"))
discard qexec.executeQuery(ctx, parse("INSERT INTO items (id, embedding) VALUES (1, '[1.0, 0.0, 0.0]')"))
discard qexec.executeQuery(ctx, parse("INSERT INTO items (id, embedding) VALUES (2, '[0.0, 1.0, 0.0]')"))
let r = qexec.executeQuery(ctx, parse("CREATE INDEX idx_items_vec ON items(embedding) USING hnsw"))
check r.success
check r.message.contains("HNSW")
check ctx.vectorIndexes.hasKey("items.embedding")
test "Vector dimension validation":
discard qexec.executeQuery(ctx, parse("CREATE TABLE items (id INT PRIMARY KEY, embedding VECTOR(3))"))
let r = qexec.executeQuery(ctx, parse("INSERT INTO items (id, embedding) VALUES (1, '[1.0, 0.0]')"))
check not r.success # Should fail due to dimension mismatch
test "euclidean_distance function":
discard qexec.executeQuery(ctx, parse("CREATE TABLE items (id INT PRIMARY KEY, embedding VECTOR(3))"))
discard qexec.executeQuery(ctx, parse("INSERT INTO items (id, embedding) VALUES (1, '[0.0, 0.0, 0.0]')"))
discard qexec.executeQuery(ctx, parse("INSERT INTO items (id, embedding) VALUES (2, '[1.0, 1.0, 1.0]')"))
let r = qexec.executeQuery(ctx, parse("SELECT id, euclidean_distance(embedding, '[0.0, 0.0, 0.0]') AS dist FROM items"))
check r.success
check r.rows.len == 2
check r.rows[0]["dist"] == "0.0"
check r.rows[1]["dist"] == "1.7320508075688772"