Compare commits

...

16 Commits

Author SHA1 Message Date
dimgigov 40616ad3c0 Update repository URLs from GitHub to Codeberg
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
2026-05-15 12:11:00 +03:00
dimgigov 8c29e700c7 Revert "Update repository URLs from GitHub to Codeberg"
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
This reverts commit 956066631e.
2026-05-15 12:10:36 +03:00
dimgigov 956066631e Update repository URLs from GitHub to Codeberg
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
2026-05-15 12:03:11 +03:00
dimgigov d11e99ab5a ci(clients): install Python dev dependencies for pytest-asyncio
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
2026-05-14 23:41:19 +03:00
dimgigov 359f945170 fix(clients): repair Python & Rust tests, add container test orchestration
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
- Python: fix wire protocol test method names (bool_val -> bool, etc.)
- Python: make integration tests and example fully async with pytest-asyncio
- Rust: add tokio dev-dependency and convert integration tests to async/await
- Rust: update ping_test example to async
- Nim: remove committed ELF build artifact
- Docker: add BARADB_HOST/BARADB_PORT env vars to test containers
- Docker: fix docker-compose.test.yml usage (remove --abort-on-container-exit)
- Add scripts/test-clients.sh for sequential client test runs
- Remove docker-compose.test.yml from .gitignore so it is tracked
- Fix repository URLs in Python and Rust READMEs
2026-05-14 23:35:45 +03:00
dimgigov c55d3080cf Update documentation and clients for v1.1.0
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
Documentation updates:
- Fix v0.1.0 → v1.1.0 version numbers in en, ru, fa, zh docs
- Add missing Window Functions, Multi-Tenant ERP, Supported Keywords sections
  to ru, fa, zh baraql.md (~105 lines each)
- Expand Turkish and Arabic baraql.md (110 → 268 lines)
- Expand Turkish and Arabic installation.md (62 → 307 lines)
- Add new Bulgarian documentation files (18 new files)

Client updates:
- Python: Full async/await rewrite with asyncio, request queueing
- Rust: Full async/await rewrite with tokio, async examples
- Nim: Update README to v1.1.0
- All clients now support async patterns consistently
2026-05-14 23:05:47 +03:00
dimgigov f7d4961125 feat: Multi-tenant ERP support via session variables + RLS
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled
- SET var = value / current_setting('var') for session-scoped variables
- current_user / current_role SQL keywords with auth bridge
- server.nim + httpserver.nim populate ExecutionContext.currentUser/currentRole
- RLS policies can reference current_setting('app.tenant_id') for tenant isolation
- Fixed evalExpr to propagate ctx recursively (fixes current_user in sub-expressions)
- Fixed GROUPING SETS execution (lowerSelect checks selGroupingSetsKind)
- Fixed FTS CREATE INDEX docId mismatch (hash of tableName.$key)
- Fixed all test suites to use isolated temp directories
- Added 5 multi-tenant tests (355 total, all green)
- Updated docs: PLAN_SQL_ADVANCED.md, baraql.md, changelog.md
2026-05-14 16:28:41 +03:00
dimgigov b0978812cb docs(en): Update English docs for Vector SQL Integration
- docs/en/vector.md — add SQL usage section (CREATE TABLE VECTOR,
  distance functions, <-> operator, CREATE INDEX USING hnsw)
- docs/en/baraql.md — update vector search section with real SQL syntax,
  add VECTOR(n) to data types, update keyword table
- docs/en/changelog.md — add Vector SQL Integration and bugfixes to [Unreleased]
- docs/ARCHITECTURE.md — add SQL Integration bullet to Vector Engine
- README.md — update vector engine section with SQL examples,
  add Vector SQL to roadmap, bump test count to 340+
2026-05-14 14:20:57 +03:00
dimgigov d076cfde3b 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
2026-05-14 14:14:13 +03:00
dimgigov 96dfaaecb1 feat(sql): Advanced SQL — LATERAL, GROUP BY/HAVING, FILTER, aggregates, PIVOT, SQL/PGQ
- LATERAL JOIN: correlated subquery strategy (scan + merge + filter/sort/limit)
- GROUP BY: SUM/AVG/MIN/MAX evaluation inside groups, HAVING filter
- FILTER (WHERE ...): conditional aggregates for COUNT/SUM/AVG
- ARRAY_AGG / STRING_AGG: multi-argument aggregate functions
- GROUPING SETS / ROLLUP / CUBE: powerset generation for multi-level aggregation
- PIVOT / UNPIVOT: row-to-column and column-to-row transformation
- SQL/PGQ Property Graph: GRAPH_TABLE MATCH parser + executor skeleton
- 330 tests passing, all 4 modalities (SQL/JSON/Vector/Graph) integrated
2026-05-14 13:14:10 +03:00
dimgigov e2a526df6f feat(sql): Window Functions + MERGE statement + REST bridge plan
- Add Window Functions: ROW_NUMBER, RANK, DENSE_RANK, LEAD, LAG,
  FIRST_VALUE, LAST_VALUE, NTILE with PARTITION BY, ORDER BY,
  ROWS/RANGE frame specifications
- Add MERGE/UPSERT statement with WHEN MATCHED UPDATE and
  WHEN NOT MATCHED INSERT
- Add SQL/PGQ Property Graph long-term plan in PLAN_SQL_ADVANCED.md
- Add 7 new tests for Window Functions and MERGE
- Update baraql.md documentation
2026-05-14 11:02:09 +03:00
dimgigov 18f3c16b2a Fix/nodebara compatibility (#2) 2026-05-14 02:33:46 +03:00
dimgigov 71dcffecce fix(gossip): use async UDP socket to avoid blocking the event loop
The gossip listener used synchronous newSocket + recvFrom,
which blocks the async event loop and prevents other async
operations from running while waiting for UDP packets.

Switch to newAsyncSocket + async recvFrom so the loop can
continue processing other tasks between gossip rounds.
2026-05-14 02:22:07 +03:00
dimgigov 398769ff97 feat(client): add TCP request queue for safe concurrency
The previous implementation allowed multiple concurrent async calls
(query, execute, ping) to interleave writes on the same socket,
which corrupted the binary protocol framing when NodeBB fired
parallel database operations.

Add an internal _requestQueue and _requestLock so that all TCP
requests are serialized: each async operation enqueues a task,
and tasks are drained one at a time via setImmediate().
2026-05-14 02:22:07 +03:00
dimgigov 8fb5dde858 fix(protocol): serialize float32/float64/fkVector in big-endian
The JavaScript client reads floats via readFloatBE/readDoubleBE,
which expect IEEE-754 values in big-endian byte order.
The Nim server was writing them with copyMem in native byte order,
so on little-endian machines (x86_64) the JS side deserialized
FLOAT64 values as garbage (e.g. 1.0 became 3.03865e-319).

Fix serialization by casting to int32/int64 and using bigEndian32/
bigEndian64, mirroring the existing big-endian handling for ints.
Apply the same fix to deserialization and to fkVector elements.
2026-05-14 02:22:07 +03:00
dimgigov a0d2ca7776 Merge release/v1.1.0 into main 2026-05-13 15:07:44 +03:00
89 changed files with 9305 additions and 928 deletions
+1 -2
View File
@@ -80,8 +80,7 @@ jobs:
working-directory: clients/python
run: |
python -m pip install --upgrade pip
pip install pytest
pip install -e .
pip install -e ".[dev]"
- name: Run Python unit tests
working-directory: clients/python
+2 -1
View File
@@ -37,4 +37,5 @@ clients/rust/target/
# Nim compiled binaries
clients/nim/src/baradb/client
src/barabadb/storage/compaction
docker-compose.test.yml
src/barabadb/query/executor
tests/join_tests
+351
View File
@@ -0,0 +1,351 @@
# BaraDB — Универсален план за Advanced SQL Engine
> **Визия**: BaraDB е самостоятелен, универсален SQL engine с Nim ядро, поддържащ модерни SQL:2023 разширения — Property Graph, Vector Search, JSON документи и прозоречни функции, в една вградена или клиент/сървър конфигурация.
>
> **Принцип**: Само основи. Не се добавят нови светове — само стабилизираме и документираме съществуващите.
>
> **Multi-Tenant фокус**: BaraDB е проектирана да поддържа ERP сценарии с много фирми (tenants) в една база данни. Всеки tenant се изолира чрез Row-Level Security (RLS) + session variables (`SET app.tenant_id = 'X'`), а не чрез отделни бази.
---
## История на разработката
- **Фаза 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 + Multi-Tenant)**: Текуща — Vector SQL Integration, Session Variables, `current_user`/`current_role`, RLS tenant isolation, тестове, документация
---
---
## Част 1: BaraDB Advanced SQL Engine
### 1.1 Window Functions ✅ ГОТОВО
Нови AST nodes: `nkWindowExpr`, `nkOverClause`, `nkFrameSpec`. Нов IR plan: `irpkWindow`.
| Функция | Описание | Статус |
|---------|----------|--------|
| `ROW_NUMBER()` | Пореден номер в партишъна | ✅ |
| `RANK()` / `DENSE_RANK()` | Класиране с/без gaps | ✅ |
| `LEAD(col, n, default)` / `LAG(col, n, default)` | Достъп до съседни редове | ✅ |
| `FIRST_VALUE(col)` / `LAST_VALUE(col)` | Краен елемент във frame | ✅ |
| `NTILE(n)` | Bucket-ване в n части | ✅ |
Frame поддръжка: `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`
Файлове: `lexer.nim`, `ast.nim`, `ir.nim`, `parser.nim`, `executor.nim`, `codegen.nim`
Тестове: 5 теста в `tests/test_all.nim`, всички зелени.
### 1.2 MERGE / UPSERT ✅ ГОТОВО
```sql
MERGE INTO inventory AS target
USING updates AS source
ON target.sku = source.sku
WHEN MATCHED THEN UPDATE SET qty = target.qty + source.delta
WHEN NOT MATCHED THEN INSERT (sku, qty) VALUES (source.sku, source.delta);
```
- Поддържа таблица или subquery като source
- WHEN MATCHED UPDATE с eval на изрази (target.col + source.col)
- WHEN NOT MATCHED INSERT с eval на value изрази
- Trigger support (BEFORE/AFTER UPDATE/INSERT)
Файлове: `lexer.nim`, `ast.nim`, `ir.nim`, `parser.nim`, `executor.nim`, `codegen.nim`
Тестове: 2 теста в `tests/test_all.nim`, всички зелени.
### 1.3 LATERAL JOIN / CROSS APPLY ✅ ГОТОВО
Позволява correlated subquery във FROM clause с достъп до лявата таблица.
```sql
SELECT u.name, recent_orders.*
FROM users u,
LATERAL (
SELECT order_id, total FROM orders o
WHERE o.user_id = u.id ORDER BY created_at DESC LIMIT 3
) recent_orders;
```
- Поддържа `JOIN LATERAL`, `LEFT JOIN LATERAL`, `CROSS JOIN LATERAL`
- Correlated references (e.g. `u.id`) чрез scan + merge + filter стратегия
- Sort и Limit от subquery се прилагат след merge
- LEFT LATERAL запазва unmatched редове с NULL padding
Файлове: `lexer.nim`, `ast.nim`, `ir.nim`, `parser.nim`, `executor.nim`
Тестове: 4 execution теста + 3 parser теста, всички зелени.
### 1.4 Advanced Aggregates ✅ ГОТОВО
- `ARRAY_AGG(col ORDER BY ...)`
- `STRING_AGG(col, delimiter)`
- `COUNT(*) FILTER (WHERE ...)`
- `GROUPING SETS`, `CUBE`, `ROLLUP`
#### GROUP BY + HAVING ✅ ГОТОВО
- SUM/AVG/MIN/MAX оценяват се в групите
- HAVING филтрира групите по aggregate условия
- Pre-computed aggregates се съхраняват в group rows
- evalExpr поддържа irekAggregate lookup
Тестове: 6 теста в `tests/test_all.nim`, всички зелени.
#### FILTER (WHERE ...) ✅ ГОТОВО
```sql
SELECT COUNT(*) FILTER (WHERE active = true) FROM users;
SELECT dept, SUM(amount) FILTER (WHERE amount > 100) FROM sales GROUP BY dept;
```
- Parser: `FILTER (WHERE ...)` след aggregate function call
- AST: `funcFilter*: Node` на `nkFuncCall`
- IR: `aggFilter*: IRExpr` на `irekAggregate`
- Executor: филтрира редове преди aggregate computation
Тестове: 2 execution теста + 1 parser тест, всички зелени.
#### ARRAY_AGG / STRING_AGG ✅ ГОТОВО
```sql
SELECT dept, ARRAY_AGG(amount) AS amounts FROM sales GROUP BY dept;
SELECT dept, STRING_AGG(name, ', ') AS names FROM employees GROUP BY dept;
```
- Нови IR aggregate ops: `irArrayAgg`, `irStringAgg`
- Multi-argument aggregate parsing (delimiter за STRING_AGG)
- FILTER support за двете функции
Тестове: 2 теста, всички зелени.
#### GROUPING SETS / ROLLUP / CUBE ✅ ГОТОВО
```sql
SELECT dept, SUM(amount) FROM sales GROUP BY ROLLUP (dept);
SELECT dept, job, SUM(amount) FROM sales GROUP BY CUBE (dept, job);
SELECT dept, job, SUM(amount) FROM sales GROUP BY GROUPING SETS ((dept), (job), ());
```
- ROLLUP(a, b) → GROUPING SETS ((a,b), (a), ())
- CUBE(a, b) → GROUPING SETS ((a,b), (a), (b), ())
- Генериране на subsets за CUBE чрез powerset алгоритъм
Тестове: 4 parser теста + 1 execution тест, всички зелени.
### 1.5 PIVOT / UNPIVOT ✅ ГОТОВО
```sql
SELECT * FROM (SELECT name, dept, salary FROM emp)
PIVOT (SUM(salary) FOR dept IN ('Eng', 'Sales'));
SELECT * FROM emp
UNPIVOT (salary FOR dept IN (eng_salary, sales_salary));
```
- Parser: PIVOT/UNPIVOT в FROM clause
- IR: `irpkPivot`, `irpkUnpivot`
- Executor: group by identity cols → aggregate per pivot value → create columns
- Subquery storage в `nkFrom.fromSubquery`
Тестове: 1 parser + 1 execution тест, всички зелени.
### 1.6 SQL:2023 Property Graph (SQL/PGQ) ✅ ГОТОВО (Parser)
```sql
SELECT * FROM GRAPH_TABLE(org_chart
MATCH (e)-[r]->(d)
COLUMNS (e.name, d.name)
);
```
- Lexer: `tkVertex`, `tkEdge`, `tkLabels`, `tkGraphTable`, `tkMatch`, `tkColumns`, `tkSrc`, `tkDst`
- AST: `nkGraphTraversal` с `gtGraphName`, `gtReturnCols`
- IR: `irpkGraphTraversal` с `graphName`, `graphAlgo`, `graphReturnCols`
- Executor: table-based graph storage (`graph_nodes`, `graph_edges`)
- Parser: `GRAPH_TABLE(name MATCH (pattern) COLUMNS (cols))`
Тестове: 1 parser тест, всички зелени.
---
## Част 1.5: Multi-Tenant ERP Support ✅ ГОТОВО
BaraDB поддържа multi-tenant архитектура, при която множество фирми (tenants) работят в една физическа база данни. Това е критично за ERP сценарии, където поддръжката на "сто бази" не е опция.
### Механизъм
| Компонент | Описание |
|-----------|----------|
| **Session Variables** | `SET app.tenant_id = 'company-123'` — задава tenant за текущата сесия |
| **current_setting()** | `current_setting('app.tenant_id')` — чете session променлива в SQL израз |
| **current_user** | `current_user` — връща автентикирания потребител от JWT/SCRAM |
| **current_role** | `current_role` — връща ролята на автентикирания потребител |
| **RLS Policies** | `CREATE POLICY tenant_isolation ON invoices FOR SELECT USING (tenant_id = current_setting('app.tenant_id'))` |
| **Auth Bridge** | `server.nim` и `httpserver.nim` попълват `ExecutionContext.currentUser`/`currentRole` след верификация |
### Пример
```sql
-- Една таблица за всички фирми
CREATE TABLE invoices (
id SERIAL PRIMARY KEY,
tenant_id TEXT NOT NULL,
data JSONB
);
-- Изолация чрез RLS
CREATE POLICY tenant_isolation ON invoices
FOR SELECT USING (tenant_id = current_setting('app.tenant_id'));
-- Всяка сесия вижда само своя tenant
SET app.tenant_id = 'company-a';
SELECT * FROM invoices; -- → само фактури на company-a
```
### Архитектурни предимства
- **JSONB документи** — schema-flexible, лесно се добавят нови полета без миграции (като ArangoDB)
- **RLS изолация** — базата данни гарантира, че всеки tenant вижда само своите данни
- **Един instance** — един BaraDB сървър обслужва всички tenants, вместо сто отделни бази
- **Auth integration** — JWT/SCRAM токените носят `sub` (user) и `role`, които се пропагират до executor-а
---
## Част 2: Мултимодални Възможности (Core Only)
### 2.1 JSON / JSONB Документи ✅ ГОТОВО
```sql
SELECT data->>'name' FROM users WHERE data->'tags' @> '["admin"]';
```
- Типове: `JSON`, `JSONB` колони в таблици
- Оператори: `->`, `->>`, `#>`, `#>>`, `@>`, `<@`, `?`, `?&`, `?|`
- Функции: `jsonb_array_elements`, `jsonb_object_keys`, `jsonb_extract_path`
- Съхранение: двоично parsed tree (не plain text)
### 2.2 Vector Search ⚠️ ЧАСТИЧНО (Engine ✅, SQL Integration 🔄)
**Вектор Engine (готов):**
- `src/barabadb/vector/engine.nim` — HNSW index с cosine/euclidean distance
- `src/barabadb/vector/quant.nim` — IVF-PQ quantization
- `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;
```
**Задачи за стабилизация (всички изпълнени):**
- [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
**Статус:** ✅ ГОТОВО. 8 SQL-level vector теста зелени.
### 2.3 Full-Text Search ✅ ГОТОВО
- Inverted Index в `src/barabadb/fts/`
- `MATCH(column, query)` функция
- BM25 scoring
- Интеграция с CrossModalEngine за hybrid search
---
## Част 3: Транзакции и Протоколи ✅ ГОТОВО
- MVCC с snapshot isolation
- WAL + checkpoint
- Distributed transactions (2PC) — `txn.addParticipant("vector")`
- Wire protocol: binary за vectors, JSON за queries
---
## Имплементационен ред (финален статус)
1.**Window Functions** (AST → Parser → IR → Executor → Tests)
2.**MERGE statement** (Parser → Executor → Tests)
3.**LATERAL JOIN** (Parser → Executor, correlated subquery strategy)
4.**GROUP BY + HAVING** (SUM/AVG/MIN/MAX, HAVING filter)
5.**FILTER clause** (COUNT/SUM/AVG FILTER (WHERE ...))
6.**ARRAY_AGG / STRING_AGG** (multi-arg aggregates)
7.**GROUPING SETS / ROLLUP / CUBE** (powerset generation)
8.**PIVOT / UNPIVOT** (row-to-column transformation)
9.**SQL/PGQ Property Graph** (GRAPH_TABLE MATCH parser)
10.**JSON/JSONB** (operators + functions)
11.**Full-Text Search** (inverted index + BM25)
12.**Vector Engine** (HNSW + IVF-PQ + SIMD)
13.**Vector SQL Integration** (тип, index, distance functions, <-> operator, ORDER BY)
---
## Крайно състояние
**340+ теста зелени.** Всички фундаментални SQL:2023 features имплементирани.
**Четирите свята:**
| Свят | Features | Статус |
|------|----------|--------|
| **SQL** | Window, MERGE, LATERAL, GROUP BY/HAVING, FILTER, ARRAY_AGG, STRING_AGG, GROUPING SETS/ROLLUP/CUBE, PIVOT/UNPIVOT | ✅ |
| **JSON** | JSON/JSONB колони, `->` / `->>` оператори | ✅ |
| **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, tkVector
- `ast.nim` — joinLateral, funcFilter, nkPivot, nkUnpivot, GroupingSetsKind, nkGraphTraversal fields
- `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
- `executor.nim` — LATERAL correlated strategy, GROUP BY aggregates + HAVING, FILTER in aggregates, ARRAY_AGG/STRING_AGG, GROUPING SETS/ROLLUP/CUBE, PIVOT/UNPIVOT, GRAPH_TABLE, fromTable kind checks
- `codegen.nim` — irpkPivot, irpkUnpivot, irpkGraphTraversal
- `tests/test_all.nim` — 25+ нови теста
- `tests/join_tests.nim` — 4 LATERAL теста
---
## Тестова стратегия
- **Unit**: Всеки нов AST/IR/Parser тест — property-based (генериране на случайни partition/order)
- **Integration**: HTTP server + клиент тестове
- **TLA+**: `windowfunctions.tla` — deterministic partitioning semantics
- **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`
- **GROUPING SETS execution** — `lowerSelect` сега проверява `selGroupingSetsKind != gskNone` освен `selGroupBy.len > 0`, което позволява изпълнение на GROUPING SETS без традиционен GROUP BY
- **FTS CREATE INDEX docId** — поправено несъответствие в изчислението на `docId` при `CREATE INDEX ... USING FTS` (сега използва хеш на `tableName.$key`, съвместим с DML операциите)
- **Тестова изолация (всички suite-ове)** — всички `newLSMTree("")` заменени с уникални temp директории; setup/teardown за suite-ове с изолирана state
- **Multi-tenant ERP support** — имплементирани критични градивни елементи:
- `SET var = value` — session variables за tenant isolation
- `current_setting('var')` — четене на session променливи в SQL изрази
- `current_user` / `current_role` — SQL keywords, които се оценяват от `ExecutionContext`
- Auth bridge — `server.nim` и `httpserver.nim` попълват `currentUser`/`currentRole` след JWT/SCRAM верификация
- RLS tenant isolation тест — `CREATE POLICY` + `current_setting('app.tenant_id')` работи за multi-tenant филтрация
- `evalExpr` вече предава `ctx` рекурсивно — поправен бъг, при който `current_user`/`current_setting` връщаха празни стойности в под-изрази
---
> **Бележка**: Този план е *замразен* за нови светове. Следващата работа е само стабилизация на съществуващото и документация.
+60
View File
@@ -0,0 +1,60 @@
# Nodebara Compatibility Fixes
This PR bundles three independent fixes discovered while integrating BaraDB with [NodeBB](https://nodebb.org/) (a large Node.js forum application). Each commit is self-contained and can be reviewed separately.
---
## 1. fix(protocol): serialize float32/float64 in big-endian
**Problem:**
The JavaScript client reads `FLOAT32`/`FLOAT64` wire values with `readFloatBE()` / `readDoubleBE()` (big-endian), but the Nim server was writing them with `copyMem(..., unsafeAddr fl, N)` — i.e. **native byte order**. On little-endian machines (virtually all x86_64 servers) the client deserializes garbage. Example: a zset `score = 1.0` becomes `3.03865e-319`, which breaks any application relying on numeric scores (user IDs, timestamps, rankings, etc.).
**Fix:**
Cast floats to `int32`/`int64` and route them through the existing `bigEndian32`/`bigEndian64` helpers, exactly like the integer paths already do. Same change applied to deserialization.
**Impact:**
Breaking fix for cross-platform wire compatibility. No API changes.
---
## 2. feat(client): add TCP request queue for safe concurrency
**Problem:**
`Client.query()`, `execute()`, and `ping()` were async methods that wrote directly to the TCP socket. When NodeBB fired multiple parallel DB operations (common on startup), their binary frames interleaved on the wire, causing parse errors, wrong request/response pairing, and random crashes.
**Fix:**
Introduce an internal `_requestQueue` + `_requestLock`. Every public async method enqueues a closure; a tiny drain loop processes them one at a time via `setImmediate()`.
**Impact:**
No breaking API change. Existing single-request usage is unchanged; concurrent usage now works safely.
---
## 3. fix(gossip): use async UDP socket to avoid blocking the event loop
**Problem:**
`startGossipListener` created a **synchronous** UDP socket (`newSocket`) and called blocking `recvFrom` inside an `async` proc. This freezes the entire async event loop until a UDP packet arrives, stalling all other async I/O.
**Fix:**
Replace `newSocket` with `newAsyncSocket` and `recvFrom` with `await recvFrom`.
**Impact:**
Non-breaking. Gossip remains optional; when enabled it no longer blocks the main loop.
---
## Testing
- [x] NodeBB v4.11.2 setup completes end-to-end
- [x] Admin login works (relies on correct FLOAT64 score deserialization)
- [x] Concurrent DB queries during startup no longer corrupt frames
- [x] Gossip listener no longer blocks other async tasks
---
## Checklist
- [x] Each commit is atomic and compiles on its own
- [x] No Nim compiler warnings introduced
- [x] JS client backward-compatible for single-request callers
- [x] Existing tests (`nimble test`) still pass
+39 -3
View File
@@ -285,8 +285,23 @@ let range = btree.scan("key_a", "key_z")
### Vector Engine
Native HNSW and IVF-PQ indexes for similarity search.
Native HNSW and IVF-PQ indexes for similarity search with full SQL integration.
```sql
-- SQL vector search
CREATE TABLE items (id INT PRIMARY KEY, embedding VECTOR(768));
INSERT INTO items (id, embedding) VALUES (1, '[0.1, 0.2, 0.3, ...]');
-- Nearest neighbor search
SELECT id FROM items
ORDER BY cosine_distance(embedding, '[0.1, 0.2, 0.3, ...]') ASC
LIMIT 10;
-- With HNSW index
CREATE INDEX idx_vec ON items(embedding) USING hnsw;
```
Native Nim API:
```nim
import barabadb/vector/engine
@@ -301,7 +316,10 @@ let filtered = idx.searchWithFilter(queryVector, k = 10,
```
Features:
- **HNSW** — hierarchical navigable small world graph
- **SQL vector types** — `VECTOR(n)` with dimension validation
- **SQL distance functions** — `cosine_distance()`, `euclidean_distance()`, `inner_product()`, `l1_distance()`, `l2_distance()`
- **`<->` operator** — Euclidean distance nearest-neighbor shorthand
- **HNSW index** — `CREATE INDEX ... USING hnsw` with automatic maintenance
- **IVF-PQ** — inverted file index with product quantization
- **Distance metrics** — cosine, euclidean, dot product, Manhattan
- **Quantization** — scalar 8-bit/4-bit, product, binary
@@ -606,6 +624,23 @@ See [docs/en/docker.md](docs/en/docker.md) for full Docker documentation.
| `BARADB_CERT_FILE` | — | TLS certificate path |
| `BARADB_KEY_FILE` | — | TLS private key path |
## Built with BaraDB
### NodeBara
**[NodeBara](https://codeberg.org/baraDB/nodebara)** is the first large-scale application running on BaraDB — a modern forum platform forked from NodeBB and fully adapted for BaraDB's native multimodal engine.
- **Concurrent query safety** — TCP request queue in the JS client handles NodeBara's parallel startup queries without frame corruption
- **Numeric accuracy** — Big-endian float serialization guarantees correct zset scores, timestamps, and rankings across platforms
- **Non-blocking cluster gossip** — Async UDP sockets keep the event loop free under load
```bash
git clone https://codeberg.org/baraDB/nodebara
cd nodebara
npm install
npm run setup # uses BaraDB as the default database
```
## Client SDKs
BaraDB provides official clients for multiple languages:
@@ -1214,7 +1249,7 @@ src/barabadb/
## Tests
```bash
# Run all tests (262 tests, 56 suites)
# Run all tests (340+ tests, 60+ suites)
nim c --path:src -r tests/test_all.nim
# Run benchmarks
@@ -1232,6 +1267,7 @@ nim c -d:release -r benchmarks/bench_all.nim
| Protocol (binary + HTTP + WS + pool + auth + ratelimit) | ✅ | 100% | v1.0.0 |
| Schema (inheritance + computed + migrations) | ✅ | 100% | v1.0.0 |
| Vector engine (HNSW + IVF-PQ + quant + SIMD + metadata) | ✅ | 100% | v1.0.0 |
| Vector SQL Integration (VECTOR type, distance functions, <->, HNSW indexes) | ✅ | 100% | v1.1.0 |
| Graph engine (all algorithms + pattern matching) | ✅ | 100% | v1.0.0 |
| FTS (BM25 + TF-IDF + fuzzy + regex + multi-language) | ✅ | 100% | v1.0.0 |
| CLI shell | ✅ | 100% | v1.0.0 |
+1 -1
View File
@@ -20,7 +20,7 @@ npm install baradb
Or from source:
```bash
git clone https://github.com/barabadb/baradadb.git
git clone https://codeberg.org/baraba/baradb
cd clients/javascript
npm link
```
+78 -48
View File
@@ -244,6 +244,8 @@ class Client {
this.requestId = 0;
this._buffer = Buffer.alloc(0);
this._pendingResolve = null;
this._requestQueue = [];
this._requestLock = false;
}
async connect() {
@@ -478,70 +480,98 @@ class Client {
throw new Error(`Unexpected auth response: 0x${header.kind.toString(16)}`);
}
async _processQueue() {
if (this._requestLock || this._requestQueue.length === 0) return;
this._requestLock = true;
const { task, resolve, reject } = this._requestQueue.shift();
try {
const result = await task();
resolve(result);
} catch (err) {
reject(err);
} finally {
this._requestLock = false;
setImmediate(() => this._processQueue());
}
}
_enqueue(task) {
return new Promise((resolve, reject) => {
this._requestQueue.push({ task, resolve, reject });
this._processQueue();
});
}
async ping() {
if (!this.connected) throw new Error('Not connected');
const msg = this._build(MsgKind.PING, Buffer.alloc(0));
this.socket.write(msg);
return this._enqueue(async () => {
const msg = this._build(MsgKind.PING, Buffer.alloc(0));
this.socket.write(msg);
const header = await this._readHeader();
if (header.kind === MsgKind.PONG) return true;
if (header.kind === MsgKind.ERROR) throw await this._readError(header.length);
return false;
const header = await this._readHeader();
if (header.kind === MsgKind.PONG) return true;
if (header.kind === MsgKind.ERROR) throw await this._readError(header.length);
return false;
});
}
async query(sql) {
if (!this.connected) throw new Error('Not connected');
const queryBuf = Buffer.from(sql, 'utf-8');
const payload = Buffer.alloc(4 + queryBuf.length + 1);
payload.writeUInt32BE(queryBuf.length, 0);
queryBuf.copy(payload, 4);
payload[4 + queryBuf.length] = ResultFormat.BINARY;
return this._enqueue(async () => {
const queryBuf = Buffer.from(sql, 'utf-8');
const payload = Buffer.alloc(4 + queryBuf.length + 1);
payload.writeUInt32BE(queryBuf.length, 0);
queryBuf.copy(payload, 4);
payload[4 + queryBuf.length] = ResultFormat.BINARY;
const msg = this._build(MsgKind.QUERY, payload);
this.socket.write(msg);
const msg = this._build(MsgKind.QUERY, payload);
this.socket.write(msg);
const header = await this._readHeader();
if (header.kind === MsgKind.ERROR) throw await this._readError(header.length);
if (header.kind === MsgKind.DATA) return await this._readDataResponse(header.length);
if (header.kind === MsgKind.COMPLETE) {
const data = await this._recvExact(header.length);
const result = new QueryResult();
result.affectedRows = data.readUInt32BE(0);
return result;
}
return new QueryResult();
const header = await this._readHeader();
if (header.kind === MsgKind.ERROR) throw await this._readError(header.length);
if (header.kind === MsgKind.DATA) return await this._readDataResponse(header.length);
if (header.kind === MsgKind.COMPLETE) {
const data = await this._recvExact(header.length);
const result = new QueryResult();
result.affectedRows = data.readUInt32BE(0);
return result;
}
return new QueryResult();
});
}
async queryParams(sql, params = []) {
if (!this.connected) throw new Error('Not connected');
const queryBuf = Buffer.from(sql, 'utf-8');
const paramParts = [];
for (const p of params) {
paramParts.push(p.serialize());
}
const paramsBuf = Buffer.concat(paramParts);
return this._enqueue(async () => {
const queryBuf = Buffer.from(sql, 'utf-8');
const paramParts = [];
for (const p of params) {
paramParts.push(p.serialize());
}
const paramsBuf = Buffer.concat(paramParts);
const payload = Buffer.alloc(4 + queryBuf.length + 1 + 4 + paramsBuf.length);
let pos = 0;
payload.writeUInt32BE(queryBuf.length, pos); pos += 4;
queryBuf.copy(payload, pos); pos += queryBuf.length;
payload[pos] = ResultFormat.BINARY; pos++;
payload.writeUInt32BE(params.length, pos); pos += 4;
paramsBuf.copy(payload, pos);
const payload = Buffer.alloc(4 + queryBuf.length + 1 + 4 + paramsBuf.length);
let pos = 0;
payload.writeUInt32BE(queryBuf.length, pos); pos += 4;
queryBuf.copy(payload, pos); pos += queryBuf.length;
payload[pos] = ResultFormat.BINARY; pos++;
payload.writeUInt32BE(params.length, pos); pos += 4;
paramsBuf.copy(payload, pos);
const msg = this._build(MsgKind.QUERY_PARAMS, payload);
this.socket.write(msg);
const msg = this._build(MsgKind.QUERY_PARAMS, payload);
this.socket.write(msg);
const header = await this._readHeader();
if (header.kind === MsgKind.ERROR) throw await this._readError(header.length);
if (header.kind === MsgKind.DATA) return await this._readDataResponse(header.length);
if (header.kind === MsgKind.COMPLETE) {
const data = await this._recvExact(header.length);
const result = new QueryResult();
result.affectedRows = data.readUInt32BE(0);
return result;
}
return new QueryResult();
const header = await this._readHeader();
if (header.kind === MsgKind.ERROR) throw await this._readError(header.length);
if (header.kind === MsgKind.DATA) return await this._readDataResponse(header.length);
if (header.kind === MsgKind.COMPLETE) {
const data = await this._recvExact(header.length);
const result = new QueryResult();
result.affectedRows = data.readUInt32BE(0);
return result;
}
return new QueryResult();
});
}
async execute(sql) {
+2 -2
View File
@@ -8,8 +8,8 @@ const assert = require('node:assert');
const net = require('net');
const { Client, WireValue, QueryBuilder } = require('../baradb');
const HOST = 'localhost';
const PORT = 9472;
const HOST = process.env.BARADB_HOST || 'localhost';
const PORT = parseInt(process.env.BARADB_PORT || '9472', 10);
function serverAvailable() {
return new Promise((resolve) => {
+2 -2
View File
@@ -15,13 +15,13 @@ Official Nim client for **BaraDB** — a multimodal database engine.
Add to your `.nimble` file:
```nim
requires "baradb >= 1.0.0"
requires "baradb >= 1.1.0"
```
Or clone locally:
```bash
git clone https://github.com/barabadb/baradadb.git
git clone https://codeberg.org/baraba/baradb
cd clients/nim
nimble develop
```
+5 -3
View File
@@ -1,15 +1,17 @@
## BaraDB Nim Client — Integration Tests
## Requires a running BaraDB server on localhost:9472.
## Requires a running BaraDB server.
## Set BARADB_HOST / BARADB_PORT env vars to override defaults.
import std/unittest
import std/asyncdispatch
import std/asyncnet
import std/strutils
import std/os
import baradb/client
const
TestHost = "127.0.0.1"
TestPort = 9472
TestHost = getEnv("BARADB_HOST", "127.0.0.1")
TestPort = parseInt(getEnv("BARADB_PORT", "9472"))
proc serverAvailable(): bool =
try:
+71 -49
View File
@@ -1,15 +1,16 @@
# BaraDB Python Client
# BaraDB Python Async Client
Official Python client for **BaraDB** — a multimodal database engine written in Nim.
Official async Python client for **BaraDB** — a multimodal database engine written in Nim.
## Features
- **Async/await** — fully non-blocking, concurrent query support
- **Binary wire protocol** — fast, compact TCP communication
- **Sync & blocking API** — simple to use in scripts and apps
- **Request queueing** — sequential processing with concurrent execution
- **Query builder** — fluent SQL construction
- **Parameterized queries** — safe from SQL injection
- **Vector & JSON support** — first-class multimodal types
- **Context managers** — `with` statement support
- **Context managers** — async `async with` statement support
## Installation
@@ -20,7 +21,7 @@ pip install baradb
Or from source:
```bash
git clone https://github.com/barabadb/baradadb.git
git clone https://codeberg.org/baraba/baradb
cd clients/python
pip install -e ".[dev]"
```
@@ -28,71 +29,92 @@ pip install -e ".[dev]"
## Quick Start
```python
import asyncio
from baradb import Client
client = Client("localhost", 9472)
client.connect()
async def main():
client = Client("localhost", 9472)
await client.connect()
result = client.query("SELECT name, age FROM users WHERE age > 18")
for row in result:
print(row["name"], row["age"])
result = await client.query("SELECT name, age FROM users WHERE age > 18")
for row in result:
print(row["name"], row["age"])
client.close()
await client.close()
asyncio.run(main())
```
### Context Manager
```python
import asyncio
from baradb import Client
with Client("localhost", 9472) as client:
result = client.query("SELECT 1")
print(result.row_count)
async def main():
async with Client("localhost", 9472) as client:
result = await client.query("SELECT 1")
print(result.row_count)
asyncio.run(main())
```
### Parameterized Queries
```python
import asyncio
from baradb import Client, WireValue
with Client("localhost", 9472) as client:
result = client.query_params(
"SELECT * FROM users WHERE age > $1 AND country = $2",
[WireValue.int64(18), WireValue.string("BG")],
)
for row in result:
print(row)
async def main():
async with Client("localhost", 9472) as client:
result = await client.query_params(
"SELECT * FROM users WHERE age > $1 AND country = $2",
[WireValue.int64(18), WireValue.string("BG")],
)
for row in result:
print(row)
asyncio.run(main())
```
### Query Builder
```python
import asyncio
from baradb import Client, QueryBuilder
with Client("localhost", 9472) as client:
qb = (
QueryBuilder(client)
.select("name", "email")
.from_("users")
.where("active = true")
.order_by("name")
.limit(10)
)
result = qb.exec()
for row in result:
print(row)
async def main():
async with Client("localhost", 9472) as client:
qb = (
QueryBuilder(client)
.select("name", "email")
.from_("users")
.where("active = true")
.order_by("name")
.limit(10)
)
result = await qb.exec()
for row in result:
print(row)
asyncio.run(main())
```
### Vector Search
```python
import asyncio
from baradb import Client, WireValue
with Client("localhost", 9472) as client:
result = client.query_params(
"SELECT id, name FROM products ORDER BY embedding <-> $1 LIMIT 5",
[WireValue.vector([0.1, 0.2, 0.3])],
)
async def main():
async with Client("localhost", 9472) as client:
result = await client.query_params(
"SELECT id, name FROM products ORDER BY embedding <-> $1 LIMIT 5",
[WireValue.vector([0.1, 0.2, 0.3])],
)
print(result.rows)
asyncio.run(main())
```
## Running Tests
@@ -107,7 +129,7 @@ Integration tests (requires server on `localhost:9472`):
```bash
# Start server
docker run -d -p 9472:9472 baradb:latest
docker run -d -p 9472:9472 barabadb:latest
# Run all tests
pytest
@@ -124,18 +146,18 @@ pytest
| `database` | `default` | Default database |
| `username` | `admin` | Username |
| `password` | `""` | Password |
| `timeout` | `30` | Socket timeout in seconds |
| `timeout` | `30.0` | Socket timeout in seconds |
### Methods
### Methods (all async)
- `connect()` — open TCP connection
- `close()` — close connection
- `query(sql) -> QueryResult` — execute SELECT-like query
- `query_params(sql, params) -> QueryResult` — parameterized query
- `execute(sql) -> int` — execute DDL/DML, returns affected rows
- `auth(token)` — JWT authentication
- `ping() -> bool` — health check
- `await client.connect()` — open TCP connection
- `await client.close()` — close connection
- `await client.query(sql) -> QueryResult` — execute SELECT-like query
- `await client.query_params(sql, params) -> QueryResult` — parameterized query
- `await client.execute(sql) -> int` — execute DDL/DML, returns affected rows
- `await client.auth(token)` — JWT authentication
- `await client.ping() -> bool` — health check
## License
Apache-2.0
Apache-2.0
+24 -10
View File
@@ -1,20 +1,34 @@
"""
BaraDB Python Client
BaraDB Python Async Client
Official Python client for BaraDB — Multimodal Database Engine.
Official async Python client for BaraDB — Multimodal Database Engine.
Communicates via the BaraDB Wire Protocol (binary, big-endian, TCP).
Install:
pip install baradb
Quick Start:
import asyncio
from baradb import Client
client = Client("localhost", 9472)
client.connect()
result = client.query("SELECT name FROM users WHERE age > 18")
for row in result:
print(row["name"])
client.close()
async def main():
client = Client("localhost", 9472)
await client.connect()
result = await client.query("SELECT name FROM users WHERE age > 18")
for row in result:
print(row["name"])
await client.close()
asyncio.run(main())
Parameterized Queries:
result = await client.query_params(
"SELECT * FROM users WHERE age > $1",
[WireValue.int64(18)]
)
Authentication:
await client.auth("jwt-token-here")
"""
from .core import (
@@ -27,7 +41,7 @@ from .core import (
ResultFormat,
)
__version__ = "1.0.0"
__version__ = "1.1.0"
__all__ = [
"Client",
"QueryBuilder",
@@ -36,4 +50,4 @@ __all__ = [
"MsgKind",
"FieldKind",
"ResultFormat",
]
]
+166 -100
View File
@@ -1,33 +1,38 @@
"""
BaraDB Python Client
BaraDB Python Async Client
Binary protocol client for BaraDB database.
Communicates via the BaraDB Wire Protocol (binary, big-endian).
Official async Python client for BaraDB — Multimodal Database Engine.
Communicates via the BaraDB Wire Protocol (binary, big-endian, TCP).
Install:
pip install baradb
Quick Start:
import asyncio
from baradb import Client
client = Client("localhost", 9472)
client.connect()
result = client.query("SELECT name FROM users WHERE age > 18")
for row in result:
print(row["name"])
client.close()
async def main():
client = Client("localhost", 9472)
await client.connect()
result = await client.query("SELECT name FROM users WHERE age > 18")
for row in result:
print(row["name"])
await client.close()
asyncio.run(main())
Parameterized Queries:
result = client.query_params("SELECT * FROM users WHERE age > $1", [WireValue.int64(18)])
result = await client.query_params(
"SELECT * FROM users WHERE age > $1",
[WireValue.int64(18)]
)
Authentication:
client = Client("localhost", 9472, username="admin", password="secret")
client.connect()
client.auth("jwt-token-here")
await client.auth("jwt-token-here")
"""
import socket
import asyncio
import struct
import json
from typing import Any, Optional, Sequence
@@ -49,7 +54,6 @@ class FieldKind:
class MsgKind:
# Client messages
CLIENT_HANDSHAKE = 0x01
QUERY = 0x02
QUERY_PARAMS = 0x03
@@ -59,7 +63,6 @@ class MsgKind:
CLOSE = 0x07
PING = 0x08
AUTH = 0x09
# Server messages
SERVER_HANDSHAKE = 0x80
READY = 0x81
DATA = 0x82
@@ -88,7 +91,7 @@ class WireValue:
return WireValue(FieldKind.NULL)
@staticmethod
def bool_val(val: bool):
def bool(val: bool):
return WireValue(FieldKind.BOOL, val)
@staticmethod
@@ -120,15 +123,15 @@ class WireValue:
return WireValue(FieldKind.STRING, val)
@staticmethod
def bytes_val(val: bytes):
def bytes(val: bytes):
return WireValue(FieldKind.BYTES, val)
@staticmethod
def array_val(val: list):
def array(val: list):
return WireValue(FieldKind.ARRAY, val)
@staticmethod
def object_val(val: dict):
def object(val: dict):
return WireValue(FieldKind.OBJECT, val)
@staticmethod
@@ -136,7 +139,7 @@ class WireValue:
return WireValue(FieldKind.VECTOR, val)
@staticmethod
def json_val(val: str):
def json(val: str):
return WireValue(FieldKind.JSON, val)
def serialize(self) -> bytes:
@@ -202,72 +205,92 @@ class QueryResult:
class Client:
"""BaraDB database client."""
"""Async BaraDB database client."""
def __init__(self, host: str = "localhost", port: int = 9472,
database: str = "default", username: str = "admin",
password: str = "", timeout: int = 30):
password: str = "", timeout: float = 30.0):
self.host = host
self.port = port
self.database = database
self.username = username
self.password = password
self.timeout = timeout
self._sock: Optional[socket.socket] = None
self._reader: Optional[asyncio.StreamReader] = None
self._writer: Optional[asyncio.StreamWriter] = None
self._connected = False
self._request_id = 0
self._buffer = bytearray()
self._pending_resolve = None
self._request_queue: list = []
self._request_lock = False
def connect(self) -> None:
async def connect(self) -> None:
"""Connect to the BaraDB server."""
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._sock.settimeout(self.timeout)
self._sock.connect((self.host, self.port))
self._reader, self._writer = await asyncio.wait_for(
asyncio.open_connection(self.host, self.port),
timeout=self.timeout
)
self._connected = True
def close(self) -> None:
if self._sock:
async def close(self) -> None:
"""Close the connection to the server."""
if self._writer and self._connected:
try:
msg = self._build_message(MsgKind.CLOSE, b"")
self._sock.send(msg)
self._writer.write(msg)
await self._writer.drain()
except Exception:
pass
self._sock.close()
if self._writer:
self._writer.close()
await self._writer.wait_closed()
self._writer = None
self._reader = None
self._connected = False
def is_connected(self) -> bool:
"""Check if client is connected."""
return self._connected
def _next_id(self) -> int:
self._request_id += 1
return self._request_id
def _recv_exact(self, size: int) -> bytes:
async def _recv_exact(self, size: int) -> bytes:
"""Receive exactly `size` bytes from the socket."""
data = b""
while len(data) < size:
chunk = self._sock.recv(size - len(data))
if not chunk:
raise ConnectionError("Connection closed by server")
data += chunk
while len(self._buffer) < size:
try:
chunk = await asyncio.wait_for(
self._reader.read(size - len(self._buffer)),
timeout=self.timeout
)
if not chunk:
raise ConnectionError("Connection closed by server")
self._buffer.extend(chunk)
except asyncio.TimeoutError:
raise TimeoutError("Receive timeout")
data = bytes(self._buffer[:size])
del self._buffer[:size]
return data
def _read_response_header(self) -> tuple[int, int, int]:
async def _read_header(self) -> tuple[int, int, int]:
"""Read a 12-byte message header. Returns (kind, length, request_id)."""
header = self._recv_exact(12)
header = await self._recv_exact(12)
kind, length, req_id = struct.unpack(">III", header)
return kind, length, req_id
def _read_error(self, length: int) -> Exception:
async def _read_error(self, length: int) -> Exception:
"""Read and parse an ERROR payload."""
data = self._recv_exact(length)
data = await self._recv_exact(length)
code = struct.unpack(">I", data[:4])[0]
msg_len = struct.unpack(">I", data[4:8])[0]
error_msg = data[8:8 + msg_len].decode("utf-8")
return Exception(f"BaraDB error {code}: {error_msg}")
def _read_data_response(self, length: int) -> QueryResult:
async def _read_data_response(self, length: int) -> QueryResult:
"""Read and parse a DATA payload, then follow up with COMPLETE."""
data = self._recv_exact(length)
data = await self._recv_exact(length)
pos = [0]
col_count = struct.unpack(">I", data[pos[0]:pos[0]+4])[0]
@@ -299,95 +322,138 @@ class Client:
result.rows = rows
result.row_count = row_count
comp_kind, comp_len, _ = self._read_response_header()
comp_kind, comp_len, _ = await self._read_header()
if comp_kind == MsgKind.COMPLETE:
comp_data = self._recv_exact(comp_len)
comp_data = await self._recv_exact(comp_len)
result.affected_rows = struct.unpack(">I", comp_data[:4])[0]
elif comp_kind == MsgKind.ERROR:
raise self._read_error(comp_len)
raise await self._read_error(comp_len)
return result
def auth(self, token: str) -> None:
async def auth(self, token: str) -> None:
"""Authenticate with the server using a JWT token."""
if not self._connected:
raise Exception("Not connected")
encoded = token.encode("utf-8")
payload = struct.pack(">I", len(encoded)) + encoded
msg = self._build_message(MsgKind.AUTH, payload)
self._sock.send(msg)
self._writer.write(msg)
await self._writer.drain()
kind, length, _ = self._read_response_header()
kind, length, _ = await self._read_header()
if kind == MsgKind.AUTH_OK:
return
elif kind == MsgKind.ERROR:
raise self._read_error(length)
raise await self._read_error(length)
else:
raise Exception(f"Unexpected auth response: 0x{kind:02x}")
def ping(self) -> bool:
async def ping(self) -> bool:
"""Ping the server. Returns True if pong received."""
if not self._connected:
raise Exception("Not connected")
msg = self._build_message(MsgKind.PING, b"")
self._sock.send(msg)
self._writer.write(msg)
await self._writer.drain()
kind, length, _ = self._read_response_header()
kind, length, _ = await self._read_header()
if kind == MsgKind.PONG:
return True
elif kind == MsgKind.ERROR:
raise self._read_error(length)
raise await self._read_error(length)
return False
def query(self, sql: str) -> QueryResult:
async def _process_queue(self) -> None:
"""Process queued requests sequentially."""
if self._request_lock or len(self._request_queue) == 0:
return
self._request_lock = True
task_data = self._request_queue.pop(0)
try:
result = await task_data["task"]()
task_data["resolve"](result)
except Exception as err:
task_data["reject"](err)
finally:
self._request_lock = False
asyncio.create_task(self._process_queue())
def _enqueue(self, task) -> None:
"""Add a task to the request queue."""
future = asyncio.Future()
self._request_queue.append({"task": task, "resolve": future.set_result, "reject": future.set_exception})
asyncio.create_task(self._process_queue())
return future
async def query(self, sql: str) -> QueryResult:
"""Execute a BaraQL query."""
payload = self._encode_string(sql)
payload += bytes([ResultFormat.BINARY])
if not self._connected:
raise Exception("Not connected")
msg = self._build_message(MsgKind.QUERY, payload)
self._sock.send(msg)
async def _do_query():
payload = self._encode_string(sql)
payload += bytes([ResultFormat.BINARY])
kind, length, _ = self._read_response_header()
msg = self._build_message(MsgKind.QUERY, payload)
self._writer.write(msg)
await self._writer.drain()
if kind == MsgKind.ERROR:
raise self._read_error(length)
kind, length, _ = await self._read_header()
if kind == MsgKind.DATA:
return self._read_data_response(length)
if kind == MsgKind.ERROR:
raise await self._read_error(length)
if kind == MsgKind.COMPLETE:
data = self._recv_exact(length)
result = QueryResult()
result.affected_rows = struct.unpack(">I", data[:4])[0]
return result
if kind == MsgKind.DATA:
return await self._read_data_response(length)
return QueryResult()
if kind == MsgKind.COMPLETE:
data = await self._recv_exact(length)
result = QueryResult()
result.affected_rows = struct.unpack(">I", data[:4])[0]
return result
def query_params(self, sql: str, params: Sequence[WireValue]) -> QueryResult:
return QueryResult()
return await self._enqueue(_do_query)
async def query_params(self, sql: str, params: Sequence[WireValue]) -> QueryResult:
"""Execute a parameterized BaraQL query."""
payload = self._encode_string(sql)
payload += bytes([ResultFormat.BINARY])
payload += struct.pack(">I", len(params))
for p in params:
payload += p.serialize()
if not self._connected:
raise Exception("Not connected")
msg = self._build_message(MsgKind.QUERY_PARAMS, payload)
self._sock.send(msg)
async def _do_query_params():
payload = self._encode_string(sql)
payload += bytes([ResultFormat.BINARY])
payload += struct.pack(">I", len(params))
for p in params:
payload += p.serialize()
kind, length, _ = self._read_response_header()
msg = self._build_message(MsgKind.QUERY_PARAMS, payload)
self._writer.write(msg)
await self._writer.drain()
if kind == MsgKind.ERROR:
raise self._read_error(length)
kind, length, _ = await self._read_header()
if kind == MsgKind.DATA:
return self._read_data_response(length)
if kind == MsgKind.ERROR:
raise await self._read_error(length)
if kind == MsgKind.COMPLETE:
data = self._recv_exact(length)
result = QueryResult()
result.affected_rows = struct.unpack(">I", data[:4])[0]
return result
if kind == MsgKind.DATA:
return await self._read_data_response(length)
return QueryResult()
if kind == MsgKind.COMPLETE:
data = await self._recv_exact(length)
result = QueryResult()
result.affected_rows = struct.unpack(">I", data[:4])[0]
return result
def execute(self, sql: str) -> int:
result = self.query(sql)
return QueryResult()
return await self._enqueue(_do_query_params)
async def execute(self, sql: str) -> int:
"""Execute a query and return affected rows count."""
result = await self.query(sql)
return result.affected_rows
def _build_message(self, kind: int, payload: bytes) -> bytes:
@@ -477,12 +543,12 @@ class Client:
return self._read_string(data, pos)
return None
def __enter__(self):
self.connect()
async def __aenter__(self):
await self.connect()
return self
def __exit__(self, *args):
self.close()
async def __aexit__(self, *args):
await self.close()
class QueryBuilder:
@@ -559,5 +625,5 @@ class QueryBuilder:
sql += " OFFSET " + str(self._offset)
return sql
def exec(self) -> QueryResult:
return self.client.query(self.build())
async def exec(self) -> QueryResult:
return await self.client.query(self.build())
+30 -29
View File
@@ -6,35 +6,36 @@ This script demonstrates common operations with the BaraDB Python client.
Run it while a BaraDB server is listening on localhost:9472.
"""
import asyncio
import sys
from baradb import Client, QueryBuilder, WireValue
def example_connection():
async def example_connection():
print("=== Connection ===")
client = Client("localhost", 9472)
client.connect()
await client.connect()
print(f"Connected: {client.is_connected()}")
print(f"Ping: {client.ping()}")
client.close()
print(f"Ping: {await client.ping()}")
await client.close()
print(f"Connected after close: {client.is_connected()}")
print()
def example_context_manager():
async def example_context_manager():
print("=== Context Manager ===")
with Client("localhost", 9472) as client:
async with Client("localhost", 9472) as client:
print(f"Inside context: {client.is_connected()}")
print(f"Ping: {client.ping()}")
print(f"Ping: {await client.ping()}")
print("Outside context: closed automatically")
print()
def example_simple_query():
async def example_simple_query():
print("=== Simple Query ===")
with Client("localhost", 9472) as client:
result = client.query("SELECT 42 as answer, 'BaraDB' as db")
async with Client("localhost", 9472) as client:
result = await client.query("SELECT 42 as answer, 'BaraDB' as db")
print(f"Columns: {result.columns}")
print(f"Rows: {result.rows}")
print(f"Row count: {result.row_count}")
@@ -43,15 +44,15 @@ def example_simple_query():
print()
def example_parameterized_query():
async def example_parameterized_query():
print("=== Parameterized Query ===")
with Client("localhost", 9472) as client:
result = client.query_params(
async with Client("localhost", 9472) as client:
result = await client.query_params(
"SELECT $1 as num, $2 as txt, $3 as flag",
[
WireValue.int64(123),
WireValue.string("hello world"),
WireValue.bool_val(True),
WireValue.bool(True),
],
)
for row in result:
@@ -59,9 +60,9 @@ def example_parameterized_query():
print()
def example_query_builder():
async def example_query_builder():
print("=== Query Builder ===")
with Client("localhost", 9472) as client:
async with Client("localhost", 9472) as client:
sql = (
QueryBuilder(client)
.select("id", "name")
@@ -75,10 +76,10 @@ def example_query_builder():
print()
def example_vector():
async def example_vector():
print("=== Vector Value ===")
with Client("localhost", 9472) as client:
result = client.query_params(
async with Client("localhost", 9472) as client:
result = await client.query_params(
"SELECT $1 as embedding",
[WireValue.vector([0.1, 0.2, 0.3, 0.4])],
)
@@ -87,34 +88,34 @@ def example_vector():
print()
def example_ddl_dml():
async def example_ddl_dml():
print("=== DDL & DML ===")
with Client("localhost", 9472) as client:
async with Client("localhost", 9472) as client:
# Clean up
try:
client.execute("DROP TABLE IF EXISTS demo_products")
await client.execute("DROP TABLE IF EXISTS demo_products")
except Exception as exc:
print(f"Cleanup warning: {exc}")
client.execute(
await client.execute(
"CREATE TABLE demo_products (id INT PRIMARY KEY, name STRING, price FLOAT)"
)
affected = client.execute(
affected = await client.execute(
"INSERT INTO demo_products (id, name, price) VALUES (1, 'Widget', 9.99)"
)
print(f"Insert affected rows: {affected}")
result = client.query("SELECT * FROM demo_products")
result = await client.query("SELECT * FROM demo_products")
print(f"Select returned {result.row_count} row(s)")
for row in result:
print(f" {row}")
client.execute("DROP TABLE demo_products")
await client.execute("DROP TABLE demo_products")
print("Table dropped")
print()
def main():
async def main():
print("BaraDB Python Client Examples")
print("Make sure BaraDB is running on localhost:9472")
print()
@@ -131,10 +132,10 @@ def main():
for fn in examples:
try:
fn()
await fn()
except Exception as exc:
print(f"ERROR in {fn.__name__}: {exc}", file=sys.stderr)
if __name__ == "__main__":
main()
asyncio.run(main())
+1
View File
@@ -48,6 +48,7 @@ packages = ["baradb"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["."]
asyncio_mode = "auto"
[tool.mypy]
strict = true
+42 -39
View File
@@ -5,13 +5,16 @@ Requires a running BaraDB server on localhost:9472.
These tests are skipped automatically if the server is unreachable.
"""
import asyncio
import os
import socket
import pytest
import pytest_asyncio
from baradb import Client, WireValue, QueryBuilder
BARADB_HOST = "localhost"
BARADB_PORT = 9472
BARADB_HOST = os.environ.get("BARADB_HOST", "localhost")
BARADB_PORT = int(os.environ.get("BARADB_PORT", "9472"))
def _server_available() -> bool:
@@ -29,41 +32,41 @@ pytestmark = pytest.mark.skipif(
)
@pytest.fixture
def client():
@pytest_asyncio.fixture
async def client():
c = Client(BARADB_HOST, BARADB_PORT)
c.connect()
await c.connect()
yield c
c.close()
await c.close()
class TestConnection:
def test_connect_and_close(self):
async def test_connect_and_close(self):
c = Client(BARADB_HOST, BARADB_PORT)
assert not c.is_connected()
c.connect()
await c.connect()
assert c.is_connected()
c.close()
await c.close()
assert not c.is_connected()
def test_context_manager(self):
with Client(BARADB_HOST, BARADB_PORT) as c:
async def test_context_manager(self):
async with Client(BARADB_HOST, BARADB_PORT) as c:
assert c.is_connected()
assert not c.is_connected()
class TestPing:
def test_ping(self, client):
assert client.ping() is True
async def test_ping(self, client):
assert await client.ping() is True
class TestQuery:
def test_simple_select(self, client):
result = client.query("SELECT 1 as one")
async def test_simple_select(self, client):
result = await client.query("SELECT 1 as one")
assert result.row_count >= 0 # server may return rows or empty
def test_query_with_params(self, client):
result = client.query_params(
async def test_query_with_params(self, client):
result = await client.query_params(
"SELECT $1 as num, $2 as txt",
[WireValue.int64(42), WireValue.string("hello")],
)
@@ -71,22 +74,22 @@ class TestQuery:
class TestExecute:
def test_create_table_and_insert(self, client):
async def test_create_table_and_insert(self, client):
# Clean up first (best-effort)
try:
client.execute("DROP TABLE IF EXISTS test_users")
await client.execute("DROP TABLE IF EXISTS test_users")
except Exception:
pass
client.execute(
await client.execute(
"CREATE TABLE test_users (id INT PRIMARY KEY, name STRING, age INT)"
)
affected = client.execute(
affected = await client.execute(
"INSERT INTO test_users (id, name, age) VALUES (1, 'Alice', 30)"
)
assert affected >= 0
result = client.query("SELECT name, age FROM test_users WHERE id = 1")
result = await client.query("SELECT name, age FROM test_users WHERE id = 1")
assert result.row_count == 1
row = result.rows[0]
# Server returns all columns; map by name via dict iteration
@@ -94,20 +97,20 @@ class TestExecute:
assert row_dict["name"] == "Alice"
assert row_dict["age"] == 30
client.execute("DROP TABLE test_users")
await client.execute("DROP TABLE test_users")
class TestQueryBuilder:
def test_builder_exec(self, client):
async def test_builder_exec(self, client):
try:
client.execute("DROP TABLE IF EXISTS test_products")
await client.execute("DROP TABLE IF EXISTS test_products")
except Exception:
pass
client.execute(
await client.execute(
"CREATE TABLE test_products (id INT PRIMARY KEY, name STRING, price FLOAT)"
)
client.execute(
await client.execute(
"INSERT INTO test_products (id, name, price) VALUES (1, 'Widget', 9.99)"
)
@@ -117,36 +120,36 @@ class TestQueryBuilder:
.from_("test_products")
.where("id = 1")
)
result = qb.exec()
result = await qb.exec()
assert result.row_count == 1
client.execute("DROP TABLE test_products")
await client.execute("DROP TABLE test_products")
class TestAuth:
def test_auth_with_dummy_token(self, client):
async def test_auth_with_dummy_token(self, client):
# Server uses default JWT secret in dev mode, any token format is accepted
# depending on server config. We just verify the method does not crash.
try:
client.auth("dummy-token-for-testing")
await client.auth("dummy-token-for-testing")
except Exception as exc:
# Auth may fail with invalid token — that's acceptable for this test
assert "Auth" in str(exc) or "error" in str(exc).lower()
class TestTransactions:
def test_transaction_begin_commit(self, client):
async def test_transaction_begin_commit(self, client):
try:
client.execute("DROP TABLE IF EXISTS test_txn")
await client.execute("DROP TABLE IF EXISTS test_txn")
except Exception:
pass
client.execute("CREATE TABLE test_txn (id INT PRIMARY KEY)")
await client.execute("CREATE TABLE test_txn (id INT PRIMARY KEY)")
client.execute("BEGIN")
client.execute("INSERT INTO test_txn (id) VALUES (1)")
client.execute("COMMIT")
await client.execute("BEGIN")
await client.execute("INSERT INTO test_txn (id) VALUES (1)")
await client.execute("COMMIT")
result = client.query("SELECT COUNT(*) FROM test_txn")
result = await client.query("SELECT COUNT(*) FROM test_txn")
assert result.row_count >= 0
client.execute("DROP TABLE test_txn")
await client.execute("DROP TABLE test_txn")
+6 -6
View File
@@ -18,11 +18,11 @@ class TestWireValue:
assert data == b"\x00"
def test_bool_true(self):
wv = WireValue.bool_val(True)
wv = WireValue.bool(True)
assert wv.serialize() == b"\x01\x01"
def test_bool_false(self):
wv = WireValue.bool_val(False)
wv = WireValue.bool(False)
assert wv.serialize() == b"\x01\x00"
def test_int8(self):
@@ -70,7 +70,7 @@ class TestWireValue:
assert data[5:] == b"hello"
def test_bytes(self):
wv = WireValue.bytes_val(b"\xde\xad\xbe\xef")
wv = WireValue.bytes(b"\xde\xad\xbe\xef")
data = wv.serialize()
assert data[0] == FieldKind.BYTES
length = struct.unpack(">I", data[1:5])[0]
@@ -87,7 +87,7 @@ class TestWireValue:
assert floats == [1.0, 2.0, 3.0]
def test_json(self):
wv = WireValue.json_val('{"key": "value"}')
wv = WireValue.json('{"key": "value"}')
data = wv.serialize()
assert data[0] == FieldKind.JSON
length = struct.unpack(">I", data[1:5])[0]
@@ -95,7 +95,7 @@ class TestWireValue:
def test_array(self):
inner = [WireValue.string("a"), WireValue.string("b")]
wv = WireValue.array_val(inner)
wv = WireValue.array(inner)
data = wv.serialize()
assert data[0] == FieldKind.ARRAY
count = struct.unpack(">I", data[1:5])[0]
@@ -103,7 +103,7 @@ class TestWireValue:
def test_object(self):
inner = {"name": WireValue.string("Bara"), "age": WireValue.int32(42)}
wv = WireValue.object_val(inner)
wv = WireValue.object(inner)
data = wv.serialize()
assert data[0] == FieldKind.OBJECT
count = struct.unpack(">I", data[1:5])[0]
+5 -3
View File
@@ -3,9 +3,9 @@ name = "baradb"
version = "1.1.0"
edition = "2021"
authors = ["BaraDB Team <team@baradb.dev>"]
description = "Official Rust client for BaraDB — binary protocol client"
description = "Official async Rust client for BaraDB — binary protocol client"
license = "Apache-2.0"
repository = "https://github.com/barabadb/baradadb"
repository = "https://github.com/barabadb/baradadb.git"
documentation = "https://docs.baradb.dev"
readme = "README.md"
keywords = ["database", "baradb", "multimodal", "client", "wire-protocol"]
@@ -13,9 +13,11 @@ categories = ["database", "network-programming"]
rust-version = "1.70"
[dependencies]
tokio = { version = "1.35", features = ["full"] }
[dev-dependencies]
tokio = { version = "1.35", features = ["full"] }
[[example]]
name = "basic"
path = "examples/basic.rs"
path = "examples/basic.rs"
+42 -36
View File
@@ -1,13 +1,13 @@
# BaraDB Rust Client
# BaraDB Async Rust Client
Official Rust client for **BaraDB** — a multimodal database engine written in Nim.
Official async Rust client for **BaraDB** — a multimodal database engine written in Nim.
## Features
- **Binary wire protocol** — fast TCP communication using only `std`
- **Zero dependencies** — no external crates required
- **Sync API** — blocking I/O suitable for most applications
- **Async/await** — fully non-blocking with Tokio runtime
- **Binary wire protocol** — fast TCP communication
- **Query builder** — fluent SQL construction
- **Parameterized queries** — safe from SQL injection
- **Vector & JSON support** — first-class multimodal types
## Installation
@@ -16,13 +16,14 @@ Add to your `Cargo.toml`:
```toml
[dependencies]
baradb = "1.0"
baradb = "1.1"
tokio = { version = "1.35", features = ["full"] }
```
Or from source:
```bash
git clone https://github.com/barabadb/baradadb.git
git clone https://codeberg.org/baraba/baradb
cd clients/rust
cargo build
```
@@ -32,13 +33,14 @@ cargo build
```rust
use baradb::Client;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::connect("localhost", 9472)?;
let result = client.query("SELECT name, age FROM users WHERE age > 18")?;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut client = Client::connect("localhost", 9472).await?;
let result = client.query("SELECT name, age FROM users WHERE age > 18").await?;
for row in result.rows() {
println!("{:?}", row);
}
client.close();
client.close().await;
Ok(())
}
```
@@ -48,16 +50,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
```rust
use baradb::{Client, WireValue};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::connect("localhost", 9472)?;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut client = Client::connect("localhost", 9472).await?;
let result = client.query_params(
"SELECT * FROM users WHERE age > $1 AND country = $2",
&[WireValue::Int64(18), WireValue::String("BG".to_string())],
)?;
).await?;
for row in result.rows() {
println!("{:?}", row);
}
client.close();
client.close().await;
Ok(())
}
```
@@ -65,21 +68,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
### Query Builder
```rust
use baradb::Client;
use baradb::{Client, QueryBuilder};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::connect("localhost", 9472)?;
let result = baradb::QueryBuilder::new(&mut client)
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut client = Client::connect("localhost", 9472).await?;
let result = QueryBuilder::new(&mut client)
.select(&["name", "email"])
.from("users")
.where_clause("active = true")
.order_by("name", "ASC")
.limit(10)
.exec()?;
.exec()
.await?;
for row in result.rows() {
println!("{:?}", row);
}
client.close();
client.close().await;
Ok(())
}
```
@@ -89,13 +94,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
```rust
use baradb::{Client, WireValue};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::connect("localhost", 9472)?;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut client = Client::connect("localhost", 9472).await?;
let result = client.query_params(
"SELECT id, name FROM products ORDER BY embedding <-> $1 LIMIT 5",
&[WireValue::Vector(vec![0.1, 0.2, 0.3])],
)?;
client.close();
).await?;
client.close().await;
Ok(())
}
```
@@ -112,7 +118,7 @@ Integration tests (requires server on `localhost:9472`):
```bash
# Start server
docker run -d -p 9472:9472 baradb:latest
docker run -d -p 9472:9472 barabadb:latest
# Run all tests
cargo test
@@ -120,19 +126,19 @@ cargo test
## API Reference
### `Client::connect(host, port)`
### `Client::connect(host, port) -> Result<Client>`
Creates a new client connected to the given host and port.
Creates a new async client connected to the given host and port.
### Methods
### Methods (all async)
- `query(sql) -> Result<QueryResult>` — execute SELECT-like query
- `query_params(sql, params) -> Result<QueryResult>` — parameterized query
- `execute(sql) -> Result<usize>` — execute DDL/DML, returns affected rows
- `auth(token) -> Result<()>` — JWT authentication
- `ping() -> Result<bool>` — health check
- `close()` — close connection
- `await client.query(sql) -> Result<QueryResult>` — execute SELECT-like query
- `await client.query_params(sql, params) -> Result<QueryResult>` — parameterized query
- `await client.execute(sql) -> Result<usize>` — execute DDL/DML, returns affected rows
- `await client.auth(token) -> Result<()>` — JWT authentication
- `await client.ping() -> Result<bool>` — health check
- `await client.close()` — close connection
## License
Apache-2.0
Apache-2.0
+59 -49
View File
@@ -3,34 +3,65 @@
use baradb::{Client, QueryBuilder, WireValue};
fn example_connection() -> Result<(), Box<dyn std::error::Error>> {
#[tokio::main]
async fn main() {
println!("BaraDB Rust Client Examples");
println!("Make sure BaraDB is running on localhost:9472");
println!();
if let Err(e) = example_connection().await {
eprintln!("ERROR: {}", e);
}
if let Err(e) = example_simple_query().await {
eprintln!("ERROR: {}", e);
}
if let Err(e) = example_parameterized_query().await {
eprintln!("ERROR: {}", e);
}
if let Err(e) = example_query_builder().await {
eprintln!("ERROR: {}", e);
}
if let Err(e) = example_vector().await {
eprintln!("ERROR: {}", e);
}
if let Err(e) = example_ddl_dml().await {
eprintln!("ERROR: {}", e);
}
}
async fn example_connection() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("=== Connection ===");
let mut client = Client::connect("127.0.0.1", 9472)?;
let mut client = Client::connect("127.0.0.1", 9472).await?;
println!("Connected: {}", client.is_connected());
println!("Ping: {}", client.ping()?);
client.close();
println!("Ping: {}", client.ping().await?);
client.close().await;
println!("Connected after close: {}", client.is_connected());
println!();
Ok(())
}
fn example_simple_query() -> Result<(), Box<dyn std::error::Error>> {
async fn example_simple_query() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("=== Simple Query ===");
let mut client = Client::connect("127.0.0.1", 9472)?;
let result = client.query("SELECT 42 as answer, 'BaraDB' as db")?;
let mut client = Client::connect("127.0.0.1", 9472).await?;
let result = client.query("SELECT 42 as answer, 'BaraDB' as db").await?;
println!("Columns: {:?}", result.columns());
println!("Row count: {}", result.row_count());
for row in result.rows() {
println!(" {:?}", row);
}
client.close();
client.close().await;
println!();
Ok(())
}
fn example_parameterized_query() -> Result<(), Box<dyn std::error::Error>> {
async fn example_parameterized_query() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("=== Parameterized Query ===");
let mut client = Client::connect("127.0.0.1", 9472)?;
let mut client = Client::connect("127.0.0.1", 9472).await?;
let result = client.query_params(
"SELECT $1 as num, $2 as txt, $3 as flag",
&[
@@ -38,18 +69,18 @@ fn example_parameterized_query() -> Result<(), Box<dyn std::error::Error>> {
WireValue::String("hello world".to_string()),
WireValue::Bool(true),
],
)?;
).await?;
for row in result.rows() {
println!(" {:?}", row);
}
client.close();
client.close().await;
println!();
Ok(())
}
fn example_query_builder() -> Result<(), Box<dyn std::error::Error>> {
async fn example_query_builder() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("=== Query Builder ===");
let mut client = Client::connect("127.0.0.1", 9472)?;
let mut client = Client::connect("127.0.0.1", 9472).await?;
let sql = QueryBuilder::new(&mut client)
.select(&["id", "name"])
.from("users")
@@ -58,70 +89,49 @@ fn example_query_builder() -> Result<(), Box<dyn std::error::Error>> {
.limit(5)
.build();
println!("Generated SQL: {}", sql);
client.close();
client.close().await;
println!();
Ok(())
}
fn example_vector() -> Result<(), Box<dyn std::error::Error>> {
async fn example_vector() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("=== Vector Value ===");
let mut client = Client::connect("127.0.0.1", 9472)?;
let mut client = Client::connect("127.0.0.1", 9472).await?;
let result = client.query_params(
"SELECT $1 as embedding",
&[WireValue::Vector(vec![0.1, 0.2, 0.3, 0.4])],
)?;
).await?;
for row in result.rows() {
println!(" {:?}", row);
}
client.close();
client.close().await;
println!();
Ok(())
}
fn example_ddl_dml() -> Result<(), Box<dyn std::error::Error>> {
async fn example_ddl_dml() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("=== DDL & DML ===");
let mut client = Client::connect("127.0.0.1", 9472)?;
let mut client = Client::connect("127.0.0.1", 9472).await?;
let _ = client.execute("DROP TABLE IF EXISTS demo_products");
let _ = client.execute("DROP TABLE IF EXISTS demo_products").await;
client.execute(
"CREATE TABLE demo_products (id INT PRIMARY KEY, name STRING, price FLOAT)",
)?;
).await?;
let affected = client.execute(
"INSERT INTO demo_products (id, name, price) VALUES (1, 'Widget', 9.99)",
)?;
).await?;
println!("Insert affected rows: {}", affected);
let result = client.query("SELECT * FROM demo_products")?;
let result = client.query("SELECT * FROM demo_products").await?;
println!("Select returned {} row(s)", result.row_count());
for row in result.rows() {
println!(" {:?}", row);
}
client.execute("DROP TABLE demo_products")?;
client.execute("DROP TABLE demo_products").await?;
println!("Table dropped");
client.close();
client.close().await;
println!();
Ok(())
}
fn main() {
println!("BaraDB Rust Client Examples");
println!("Make sure BaraDB is running on localhost:9472");
println!();
let examples: Vec<fn() -> Result<(), Box<dyn std::error::Error>>> = vec![
example_connection,
example_simple_query,
example_parameterized_query,
example_query_builder,
example_vector,
example_ddl_dml,
];
for example in examples {
if let Err(e) = example() {
eprintln!("ERROR: {}", e);
}
}
}
}
+5 -4
View File
@@ -1,11 +1,12 @@
use baradb::Client;
fn main() {
let mut client = Client::connect("localhost", 9472).unwrap();
#[tokio::main]
async fn main() {
let mut client = Client::connect("localhost", 9472).await.unwrap();
println!("Connected: {}", client.is_connected());
match client.ping() {
match client.ping().await {
Ok(v) => println!("Ping: {}", v),
Err(e) => println!("Ping error: {}", e),
}
client.close();
client.close().await;
}
+110 -75
View File
@@ -1,36 +1,42 @@
//! BaraDB Rust Client
//! BaraDB Async Rust Client
//!
//! Binary protocol client for BaraDB database.
//! Zero external dependencies — uses only `std`.
//! Async binary protocol client for BaraDB database.
//! Uses Tokio for async I/O operations.
//!
//! # Example
//! ```no_run
//! use baradb::{Client, WireValue};
//!
//! let mut client = Client::connect("localhost", 9472).unwrap();
//! let result = client.query("SELECT name FROM users WHERE age > 18").unwrap();
//! for row in result.rows() {
//! if let Some(WireValue::String(name)) = row.get("name") {
//! println!("{}", name);
//! #[tokio::main]
//! async fn main() {
//! let mut client = Client::connect("localhost", 9472).await.unwrap();
//! let result = client.query("SELECT name FROM users WHERE age > 18").await.unwrap();
//! for row in result.rows() {
//! if let Some(WireValue::String(name)) = row.get("name") {
//! println!("{}", name);
//! }
//! }
//! client.close().await;
//! }
//! client.close();
//! ```
//!
//! # Parameterized Queries
//! ```no_run
//! use baradb::{Client, WireValue};
//!
//! let mut client = Client::connect("localhost", 9472).unwrap();
//! let result = client.query_params(
//! "SELECT * FROM users WHERE age > $1",
//! &[WireValue::Int64(18)],
//! ).unwrap();
//! #[tokio::main]
//! async fn main() {
//! let mut client = Client::connect("localhost", 9472).await.unwrap();
//! let result = client.query_params(
//! "SELECT * FROM users WHERE age > $1",
//! &[WireValue::Int64(18)],
//! ).await.unwrap();
//! }
//! ```
use std::collections::HashMap;
use std::io::{Read, Write};
use std::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
// Client message kinds
const MK_CLIENT_HANDSHAKE: u32 = 0x01;
@@ -221,43 +227,41 @@ impl QueryResult {
}
}
/// BaraDB client
/// BaraDB async client
pub struct Client {
config: Config,
stream: TcpStream,
connected: bool,
request_id: u32,
read_buf: Vec<u8>,
}
impl Client {
pub fn connect(host: &str, port: u16) -> Result<Self, Box<dyn std::error::Error>> {
let config = Config {
host: host.to_string(),
port,
..Default::default()
};
Self::connect_with_config(config)
}
pub fn connect_with_config(config: Config) -> Result<Self, Box<dyn std::error::Error>> {
let addr = format!("{}:{}", config.host, config.port);
let stream = TcpStream::connect(&addr)?;
stream.set_nodelay(true)?;
/// Connect to a BaraDB server
pub async fn connect(host: &str, port: u16) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
let addr = format!("{}:{}", host, port);
let stream = TcpStream::connect(&addr).await?;
Ok(Client {
config,
stream,
connected: true,
request_id: 0,
read_buf: Vec::new(),
})
}
pub fn close(&mut self) {
/// Connect with custom configuration
pub async fn connect_with_config(config: Config) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
Self::connect(&config.host, config.port).await
}
/// Close the connection
pub async fn close(&mut self) {
if self.connected {
let _ = self.send_close();
let _ = self.send_close().await;
}
self.connected = false;
}
/// Check if connected
pub fn is_connected(&self) -> bool {
self.connected
}
@@ -267,27 +271,39 @@ impl Client {
self.request_id
}
fn send_close(&mut self) -> Result<(), Box<dyn std::error::Error>> {
async fn send_close(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let msg = build_message(MK_CLOSE, self.next_id(), &[]);
self.stream.write_all(&msg)?;
self.stream.write_all(&msg).await?;
Ok(())
}
fn read_header(&mut self) -> Result<(u32, u32, u32), Box<dyn std::error::Error>> {
let mut header = [0u8; 12];
self.stream.read_exact(&mut header)?;
async fn read_exact(&mut self, mut n: usize) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
let mut buf = vec![0u8; n];
let mut pos = 0;
while pos < n {
let read = self.stream.read(&mut buf[pos..]).await?;
if read == 0 {
return Err("Connection closed".into());
}
pos += read;
}
Ok(buf)
}
async fn read_header(&mut self) -> Result<(u32, u32, u32), Box<dyn std::error::Error + Send + Sync>> {
let header = self.read_exact(12).await?;
let kind = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
let length = u32::from_be_bytes([header[4], header[5], header[6], header[7]]);
let req_id = u32::from_be_bytes([header[8], header[9], header[10], header[11]]);
Ok((kind, length, req_id))
}
fn read_payload(&mut self, length: u32) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let mut payload = vec![0u8; length as usize];
async fn read_payload(&mut self, length: u32) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
if length > 0 {
self.stream.read_exact(&mut payload)?;
self.read_exact(length as usize).await
} else {
Ok(vec![])
}
Ok(payload)
}
fn read_error_message(payload: &[u8]) -> String {
@@ -302,26 +318,26 @@ impl Client {
"Query error".to_string()
}
fn read_data_response(&mut self, payload: &[u8]) -> Result<QueryResult, Box<dyn std::error::Error>> {
async fn read_data_response(&mut self, payload: &[u8]) -> Result<QueryResult, Box<dyn std::error::Error + Send + Sync>> {
let mut pos = 0usize;
let col_count = read_u32(payload, &mut pos) as usize;
let col_count = read_u32(payload, &mut pos);
let mut columns = Vec::with_capacity(col_count);
let mut columns = Vec::with_capacity(col_count as usize);
for _ in 0..col_count {
columns.push(read_string(payload, &mut pos));
}
let mut col_types = Vec::with_capacity(col_count);
let mut col_types = Vec::with_capacity(col_count as usize);
for _ in 0..col_count {
col_types.push(payload[pos]);
pos += 1;
}
let row_count = read_u32(payload, &mut pos) as usize;
let mut rows = Vec::with_capacity(row_count);
let row_count = read_u32(payload, &mut pos);
let mut rows = Vec::with_capacity(row_count as usize);
for _ in 0..row_count {
let mut row = HashMap::new();
for c in 0..col_count {
for c in 0..col_count as usize {
let val = read_wire_value(payload, &mut pos);
row.insert(columns[c].clone(), val);
}
@@ -329,9 +345,9 @@ impl Client {
}
let mut affected = 0usize;
let (comp_kind, comp_len, _) = self.read_header()?;
let (comp_kind, comp_len, _) = self.read_header().await?;
if comp_kind == MK_COMPLETE {
let comp_payload = self.read_payload(comp_len)?;
let comp_payload = self.read_payload(comp_len).await?;
if comp_payload.len() >= 4 {
affected = u32::from_be_bytes([comp_payload[0], comp_payload[1], comp_payload[2], comp_payload[3]]) as usize;
}
@@ -340,44 +356,47 @@ impl Client {
Ok(QueryResult { columns, column_types: col_types, rows, affected_rows: affected })
}
pub fn auth(&mut self, token: &str) -> Result<(), Box<dyn std::error::Error>> {
/// Authenticate with JWT token
pub async fn auth(&mut self, token: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if !self.connected {
return Err("Not connected".into());
}
let payload = encode_string(token);
let msg = build_message(MK_AUTH, self.next_id(), &payload);
self.stream.write_all(&msg)?;
self.stream.write_all(&msg).await?;
let (kind, length, _) = self.read_header()?;
let (kind, length, _) = self.read_header().await?;
match kind {
MK_AUTH_OK => Ok(()),
MK_ERROR => {
let p = self.read_payload(length)?;
let p = self.read_payload(length).await?;
Err(Self::read_error_message(&p).into())
}
_ => Err(format!("Unexpected auth response: 0x{:02x}", kind).into()),
}
}
pub fn ping(&mut self) -> Result<bool, Box<dyn std::error::Error>> {
/// Ping the server
pub async fn ping(&mut self) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
if !self.connected {
return Err("Not connected".into());
}
let msg = build_message(MK_PING, self.next_id(), &[]);
self.stream.write_all(&msg)?;
self.stream.write_all(&msg).await?;
let (kind, length, _) = self.read_header()?;
let (kind, length, _) = self.read_header().await?;
match kind {
MK_PONG => Ok(true),
MK_ERROR => {
let p = self.read_payload(length)?;
let p = self.read_payload(length).await?;
Err(Self::read_error_message(&p).into())
}
_ => Ok(false),
}
}
pub fn query(&mut self, sql: &str) -> Result<QueryResult, Box<dyn std::error::Error>> {
/// Execute a query
pub async fn query(&mut self, sql: &str) -> Result<QueryResult, Box<dyn std::error::Error + Send + Sync>> {
if !self.connected {
return Err("Not connected".into());
}
@@ -386,14 +405,14 @@ impl Client {
payload.push(0x00); // ResultFormat::BINARY
let msg = build_message(MK_QUERY, self.next_id(), &payload);
self.stream.write_all(&msg)?;
self.stream.write_all(&msg).await?;
let (kind, length, _) = self.read_header()?;
let resp_payload = self.read_payload(length)?;
let (kind, length, _) = self.read_header().await?;
let resp_payload = self.read_payload(length).await?;
match kind {
MK_READY => Ok(QueryResult { columns: vec![], column_types: vec![], rows: vec![], affected_rows: 0 }),
MK_DATA => self.read_data_response(&resp_payload),
MK_DATA => self.read_data_response(&resp_payload).await,
MK_COMPLETE => {
let affected = if resp_payload.len() >= 4 {
u32::from_be_bytes([resp_payload[0], resp_payload[1], resp_payload[2], resp_payload[3]]) as usize
@@ -405,7 +424,8 @@ impl Client {
}
}
pub fn query_params(&mut self, sql: &str, params: &[WireValue]) -> Result<QueryResult, Box<dyn std::error::Error>> {
/// Execute a parameterized query
pub async fn query_params(&mut self, sql: &str, params: &[WireValue]) -> Result<QueryResult, Box<dyn std::error::Error + Send + Sync>> {
if !self.connected {
return Err("Not connected".into());
}
@@ -418,14 +438,14 @@ impl Client {
}
let msg = build_message(MK_QUERY_PARAMS, self.next_id(), &payload);
self.stream.write_all(&msg)?;
self.stream.write_all(&msg).await?;
let (kind, length, _) = self.read_header()?;
let resp_payload = self.read_payload(length)?;
let (kind, length, _) = self.read_header().await?;
let resp_payload = self.read_payload(length).await?;
match kind {
MK_READY => Ok(QueryResult { columns: vec![], column_types: vec![], rows: vec![], affected_rows: 0 }),
MK_DATA => self.read_data_response(&resp_payload),
MK_DATA => self.read_data_response(&resp_payload).await,
MK_COMPLETE => {
let affected = if resp_payload.len() >= 4 {
u32::from_be_bytes([resp_payload[0], resp_payload[1], resp_payload[2], resp_payload[3]]) as usize
@@ -437,12 +457,14 @@ impl Client {
}
}
pub fn execute(&mut self, sql: &str) -> Result<usize, Box<dyn std::error::Error>> {
let result = self.query(sql)?;
/// Execute and return affected rows
pub async fn execute(&mut self, sql: &str) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
let result = self.query(sql).await?;
Ok(result.affected_rows())
}
}
/// Query builder for fluent SQL construction
pub struct QueryBuilder<'a> {
client: &'a mut Client,
select_cols: Vec<String>,
@@ -457,6 +479,7 @@ pub struct QueryBuilder<'a> {
}
impl<'a> QueryBuilder<'a> {
/// Create a new query builder
pub fn new(client: &'a mut Client) -> Self {
QueryBuilder {
client,
@@ -472,56 +495,67 @@ impl<'a> QueryBuilder<'a> {
}
}
/// Add columns to SELECT
pub fn select(mut self, cols: &[&str]) -> Self {
self.select_cols.extend(cols.iter().map(|s| s.to_string()));
self
}
/// Set the FROM table
pub fn from(mut self, table: &str) -> Self {
self.from_table = table.to_string();
self
}
/// Add a WHERE clause
pub fn where_clause(mut self, clause: &str) -> Self {
self.where_clauses.push(clause.to_string());
self
}
/// Add a JOIN clause
pub fn join(mut self, table: &str, on: &str) -> Self {
self.joins.push(format!("JOIN {} ON {}", table, on));
self
}
/// Add a LEFT JOIN clause
pub fn left_join(mut self, table: &str, on: &str) -> Self {
self.joins.push(format!("LEFT JOIN {} ON {}", table, on));
self
}
/// Add GROUP BY columns
pub fn group_by(mut self, cols: &[&str]) -> Self {
self.group_by.extend(cols.iter().map(|s| s.to_string()));
self
}
/// Add HAVING clause
pub fn having(mut self, clause: &str) -> Self {
self.having = clause.to_string();
self
}
/// Add ORDER BY column
pub fn order_by(mut self, col: &str, dir: &str) -> Self {
self.order_by.push(format!("{} {}", col, dir));
self
}
/// Set LIMIT
pub fn limit(mut self, n: usize) -> Self {
self.limit = n;
self
}
/// Set OFFSET
pub fn offset(mut self, n: usize) -> Self {
self.offset = n;
self
}
/// Build the SQL string
pub fn build(&self) -> String {
let mut sql = String::from("SELECT ");
if self.select_cols.is_empty() {
@@ -555,9 +589,10 @@ impl<'a> QueryBuilder<'a> {
sql
}
pub fn exec(self) -> Result<QueryResult, Box<dyn std::error::Error>> {
/// Execute the query
pub async fn exec(self) -> Result<QueryResult, Box<dyn std::error::Error + Send + Sync>> {
let sql = self.build();
self.client.query(&sql)
self.client.query(&sql).await
}
}
@@ -675,4 +710,4 @@ fn read_wire_value(data: &[u8], pos: &mut usize) -> WireValue {
FK_JSON => WireValue::Json(read_string(data, pos)),
_ => WireValue::Null,
}
}
}
+57 -42
View File
@@ -1,104 +1,118 @@
// BaraDB Rust Client — Integration Tests
// Requires a running BaraDB server on localhost:9472.
// Requires a running BaraDB server.
// Set BARADB_HOST / BARADB_PORT env vars to override defaults.
use std::net::TcpStream;
use std::time::Duration;
use baradb::{Client, QueryBuilder, WireValue};
const HOST: &str = "127.0.0.1";
const PORT: u16 = 9472;
fn server_available() -> bool {
TcpStream::connect((HOST, PORT)).is_ok()
fn host() -> String {
std::env::var("BARADB_HOST").unwrap_or_else(|_| "127.0.0.1".to_string())
}
#[test]
fn test_connect_and_close() {
fn port() -> u16 {
std::env::var("BARADB_PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(9472)
}
fn server_available() -> bool {
TcpStream::connect((host().as_str(), port())).is_ok()
}
#[tokio::test]
async fn test_connect_and_close() {
if !server_available() {
return;
}
let mut client = Client::connect(HOST, PORT).unwrap();
let mut client = Client::connect(&host(), port()).await.unwrap();
assert!(client.is_connected());
client.close();
client.close().await;
assert!(!client.is_connected());
}
#[test]
fn test_ping() {
#[tokio::test]
async fn test_ping() {
if !server_available() {
return;
}
let mut client = Client::connect(HOST, PORT).unwrap();
assert!(client.ping().unwrap());
client.close();
let mut client = Client::connect(&host(), port()).await.unwrap();
assert!(client.ping().await.unwrap());
client.close().await;
}
#[test]
fn test_simple_select() {
#[tokio::test]
async fn test_simple_select() {
if !server_available() {
return;
}
let mut client = Client::connect(HOST, PORT).unwrap();
let result = client.query("SELECT 1 as one").unwrap();
let mut client = Client::connect(&host(), port()).await.unwrap();
let result = client.query("SELECT 1 as one").await.unwrap();
assert!(result.row_count() >= 0);
client.close();
client.close().await;
}
#[test]
fn test_parameterized_query() {
#[tokio::test]
async fn test_parameterized_query() {
if !server_available() {
return;
}
let mut client = Client::connect(HOST, PORT).unwrap();
let mut client = Client::connect(&host(), port()).await.unwrap();
let result = client
.query_params(
"SELECT $1 as num, $2 as txt",
&[WireValue::Int64(42), WireValue::String("hello".to_string())],
)
.await
.unwrap();
assert!(result.row_count() >= 0);
client.close();
client.close().await;
}
#[test]
fn test_create_table_insert_select_drop() {
#[tokio::test]
async fn test_create_table_insert_select_drop() {
if !server_available() {
return;
}
let mut client = Client::connect(HOST, PORT).unwrap();
let mut client = Client::connect(&host(), port()).await.unwrap();
let _ = client.execute("DROP TABLE IF EXISTS rust_test_users");
let _ = client.execute("DROP TABLE IF EXISTS rust_test_users").await;
client
.execute("CREATE TABLE rust_test_users (id INT PRIMARY KEY, name STRING, age INT)")
.await
.unwrap();
let affected = client
.execute("INSERT INTO rust_test_users (id, name, age) VALUES (1, 'Alice', 30)")
.await
.unwrap();
assert!(affected >= 0);
let result = client
.query("SELECT name, age FROM rust_test_users WHERE id = 1")
.await
.unwrap();
assert_eq!(result.row_count(), 1);
client.execute("DROP TABLE rust_test_users").unwrap();
client.close();
client.execute("DROP TABLE rust_test_users").await.unwrap();
client.close().await;
}
#[test]
fn test_query_builder_exec() {
#[tokio::test]
async fn test_query_builder_exec() {
if !server_available() {
return;
}
let mut client = Client::connect(HOST, PORT).unwrap();
let mut client = Client::connect(&host(), port()).await.unwrap();
let _ = client.execute("DROP TABLE IF EXISTS rust_test_products");
let _ = client.execute("DROP TABLE IF EXISTS rust_test_products").await;
client
.execute("CREATE TABLE rust_test_products (id INT PRIMARY KEY, name STRING, price FLOAT)")
.await
.unwrap();
client
.execute("INSERT INTO rust_test_products (id, name, price) VALUES (1, 'Widget', 9.99)")
.await
.unwrap();
let result = QueryBuilder::new(&mut client)
@@ -106,21 +120,22 @@ fn test_query_builder_exec() {
.from("rust_test_products")
.where_clause("id = 1")
.exec()
.await
.unwrap();
assert_eq!(result.row_count(), 1);
client.execute("DROP TABLE rust_test_products").unwrap();
client.close();
client.execute("DROP TABLE rust_test_products").await.unwrap();
client.close().await;
}
#[test]
fn test_auth_dummy_token() {
#[tokio::test]
async fn test_auth_dummy_token() {
if !server_available() {
return;
}
let mut client = Client::connect(HOST, PORT).unwrap();
let res = client.auth("dummy-token-for-testing");
let mut client = Client::connect(&host(), port()).await.unwrap();
let res = client.auth("dummy-token-for-testing").await;
// Dev server may accept or reject — both are fine
match res {
Ok(()) => {}
@@ -129,5 +144,5 @@ fn test_auth_dummy_token() {
assert!(msg.contains("Auth") || msg.to_lowercase().contains("error"));
}
}
client.close();
client.close().await;
}
+101
View File
@@ -0,0 +1,101 @@
# BaraDB — Client Integration Test Stack
#
# This compose file starts BaraDB server and runs all client test suites.
# Because test containers run in parallel, do NOT use --abort-on-container-exit
# (the first finished container would kill the others).
#
# Recommended usage:
# ./scripts/test-clients.sh
#
# Or manually run each suite:
# docker compose -f docker-compose.test.yml up -d baradb
# docker compose -f docker-compose.test.yml run --rm test-python
# docker compose -f docker-compose.test.yml run --rm test-javascript
# docker compose -f docker-compose.test.yml run --rm test-nim
# docker compose -f docker-compose.test.yml run --rm test-rust
# docker compose -f docker-compose.test.yml down
services:
baradb:
build:
context: .
dockerfile: Dockerfile
image: baradb:latest
ports:
- "9472:9472"
healthcheck:
test: ["CMD", "sh", "-c", "wget -qO- http://localhost:9912/health >/dev/null 2>&1"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
test-python:
image: python:3.12-slim
depends_on:
baradb:
condition: service_healthy
environment:
- BARADB_HOST=baradb
- BARADB_PORT=9472
volumes:
- ./clients/python:/workspace
working_dir: /workspace
command: >
sh -c "
pip install pytest pytest-asyncio -q &&
pip install -e ".[dev]" -q &&
pytest tests/test_wire_protocol.py tests/test_query_builder.py -v &&
pytest tests/test_integration.py -v
"
test-javascript:
image: node:20-slim
depends_on:
baradb:
condition: service_healthy
environment:
- BARADB_HOST=baradb
- BARADB_PORT=9472
volumes:
- ./clients/javascript:/workspace
working_dir: /workspace
command: >
sh -c "
node --test tests/unit.test.js &&
node --test tests/integration.test.js
"
test-nim:
image: nimlang/nim:2.2.10
depends_on:
baradb:
condition: service_healthy
environment:
- BARADB_HOST=baradb
- BARADB_PORT=9472
volumes:
- ./clients/nim:/workspace
- ./clients/nim/src:/workspace/src
working_dir: /workspace
command: >
sh -c "
nim c --path:src -r tests/test_client.nim &&
nim c --path:src -r tests/test_integration.nim
"
test-rust:
image: rust:1.78
depends_on:
baradb:
condition: service_healthy
environment:
- BARADB_HOST=baradb
- BARADB_PORT=9472
volumes:
- ./clients/rust:/workspace
working_dir: /workspace
command: >
sh -c "
cargo test -- --test-threads=1
"
+6
View File
@@ -90,6 +90,12 @@ The query layer processes BaraQL — a SQL-compatible query language with extens
- **Quantization** (`quant.nim`): Scalar 8-bit/4-bit, product, and binary quantization for compression.
- **SIMD Operations** (`simd.nim`): Unrolled loop distance computations (cosine, Euclidean, dot product, Manhattan).
- **Batch Operations**: batchInsert, batchSearch, batchDistance for high-throughput.
- **SQL Integration** (`query/executor.nim`):
- `VECTOR(n)` column type with dimension validation
- `CREATE INDEX ... USING hnsw` / `USING ivfpq`
- Distance functions: `cosine_distance()`, `euclidean_distance()`, `inner_product()`, `l1_distance()`, `l2_distance()`
- `<->` nearest-neighbor operator
- Automatic index maintenance on INSERT/UPDATE
### Graph Engine (`graph/`)
- **Adjacency List** (`engine.nim`): Edge-weighted directed graph storage with forward/reverse adjacency.
+168 -1
View File
@@ -97,5 +97,172 @@ RETURN friend.name;
```sql
BEGIN;
INSERT users { name := 'Alice', age := 30 };
INSERT orders { user_id := last_insert_id(), total := 100 };
COMMIT;
```
-- مع نقطة حفظ
BEGIN;
INSERT users { name := 'Bob', age := 25 };
SAVEPOINT sp1;
INSERT orders { user_id := last_insert_id(), total := 200 };
-- خطأ، التراجع إلى نقطة الحفظ
ROLLBACK TO sp1;
COMMIT;
```
## البحث النصي الكامل
```sql
-- بحث أساسي
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('database programming');
-- مع درجة الصلة
SELECT title, relevance()
FROM articles
WHERE MATCH(title, body) AGAINST('Nim language')
ORDER BY relevance() DESC;
-- الوضع المنطقي
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('+Nim -Python' IN BOOLEAN MODE);
-- البحث الضبابي
SELECT * FROM articles
WHERE MATCH(title) AGAINST('programing' WITH FUZZINESS 2);
```
## الدوال المحددة من المستخدم
```sql
-- تسجيل UDF
CREATE FUNCTION greet(name str) -> str {
RETURN 'Hello, ' || name || '!';
};
-- استخدامها
SELECT greet(name) FROM users;
-- الدوال المدمجة
SELECT abs(-5), sqrt(16), lower('HELLO'), len('test');
```
## تلميحات الاستعلام
```sql
-- فرض استخدام الفهرس
SELECT /*+ USE_INDEX(idx_users_age) */ * FROM users WHERE age > 18;
-- فرض البحث المتجهي التقريبي
SELECT /*+ APPROXIMATE */ * FROM vectors
ORDER BY cosine_distance(embedding, [...])
LIMIT 10;
-- التنفيذ المتوازي
SELECT /*+ PARALLEL(4) */ * FROM large_table;
```
## دوال النوافذ
```sql
-- دوال الترتيب
SELECT
name,
department,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS r,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dr
FROM employees;
-- دوال القيمة
SELECT
name,
salary,
LAG(salary, 1, 0) OVER (ORDER BY salary) AS prev_salary,
LEAD(salary, 1, 0) OVER (ORDER BY salary) AS next_salary,
FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS cheapest,
LAST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS most_expensive
FROM employees;
-- دوال التوزيع
SELECT name, NTILE(4) OVER (ORDER BY salary) AS quartile FROM employees;
```
### مواصفات الإطار
```sql
-- إطار ROWS
SUM(salary) OVER (
PARTITION BY department
ORDER BY hire_date
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
)
-- إطار RANGE
SUM(salary) OVER (
PARTITION BY department
ORDER BY hire_date
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
```
## ERP متعدد المستأجرين
يدعم BaraDB تشغيل عدة شركات (مستأجرين) في مثيل قاعدة بيانات واحد، باستخدام **أمان مستوى الصف (RLS)** مع **متغيرات الجلسة**.
### متغيرات الجلسة
```sql
SET app.tenant_id = 'company-123';
SELECT current_setting('app.tenant_id') AS tenant;
```
### المستخدم / الدور الحالي
```sql
SELECT current_user AS me, current_role AS my_role;
```
### عزل المستأجر عبر RLS
```sql
-- تمكين RLS على جدول
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
-- إنشاء سياسة تصفية حسب المستأجر
CREATE POLICY tenant_isolation ON invoices
FOR SELECT USING (tenant_id = current_setting('app.tenant_id'));
-- كل جلسة ترى فقط بياناتها
SET app.tenant_id = 'company-a';
SELECT * FROM invoices; -- صفوف company-a فقط
```
### لماذا متعدد المستأجرين؟
- **مثيل واحد، مستأجرون كثيرون** — لا حاجة لتشغيل 100 قاعدة بيانات منفصلة
- **مستندات JSONB** — تخزين مخطط مرن، سهل إضافة حقول لكل مستأجر
- **RLS يضمن العزل** — قاعدة البيانات تفرض حدود المستأجر، وليس فقط التطبيق
## الكلمات المفتاحية المدعومة
| الفئة | الكلمات المفتاحية |
|----------|----------|
| DQL | SELECT, FROM, WHERE, ORDER BY, GROUP BY, HAVING, LIMIT, OFFSET, DISTINCT |
| DML | INSERT, UPDATE, DELETE, SET, VALUES |
| DDL | CREATE TYPE, DROP TYPE, CREATE INDEX, DROP INDEX, ALTER TYPE |
| Join | INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN, CROSS JOIN, ON |
| Set | UNION, UNION ALL, INTERSECT, EXCEPT |
| CTEs | WITH, RECURSIVE, AS |
| Case | CASE, WHEN, THEN, ELSE, END |
| Transaction | BEGIN, COMMIT, ROLLBACK, SAVEPOINT |
| Graph | MATCH, RETURN, WHERE, shortestPath, type |
| FTS | MATCH, AGAINST, relevance, IN BOOLEAN MODE, WITH FUZZINESS |
| Vector | cosine_distance, euclidean_distance, inner_product, l1_distance, l2_distance, <-> |
| JSON | ->, ->> |
| FTS | @@ (تطابق BM25) |
| Recovery | RECOVER TO TIMESTAMP |
| Functions | count, sum, avg, min, max, stddev, variance, abs, sqrt, lower, upper, len, trim, substr, now, last_insert_id, current_setting |
| Session | SET, current_setting, current_user, current_role |
| Window | OVER, PARTITION BY, ROWS, RANGE, UNBOUNDED PRECEDING, CURRENT ROW, FOLLOWING |
| Window Functions | ROW_NUMBER, RANK, DENSE_RANK, LEAD, LAG, FIRST_VALUE, LAST_VALUE, NTILE |
+1 -1
View File
@@ -3,7 +3,7 @@
## البداية السريعة
```bash
git clone https://github.com/katehonz/barabaDB.git
git clone https://codeberg.org/baraba/baradb
cd barabaDB
docker build -t baradb:latest .
+247 -1
View File
@@ -22,38 +22,284 @@
### لينكس
```bash
# المثبت الرسمي
curl https://nim-lang.org/choosenim/init.sh -sSf | sh
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install nim
# Fedora
sudo dnf install nim
# Arch Linux
sudo pacman -S nim
```
### macOS
```bash
# Homebrew
brew install nim
# MacPorts
sudo port install nim
```
### ويندوز
```powershell
# باستخدام choosenim
curl.exe -A "MSYS2_$(uname -m)" -L https://nim-lang.org/choosenim/init.ps1 | powershell -
# باستخدام winget
winget install nim
# باستخدام scoop
scoop install nim
```
### التحقق من التثبيت
```bash
nim --version
# المتوقع: Nim Compiler Version 2.2.0 أو أحدث
```
## تثبيت OpenSSL
### لينكس
```bash
# Ubuntu/Debian
sudo apt-get install libssl-dev
# Fedora
sudo dnf install openssl-devel
# Arch Linux
sudo pacman -S openssl
```
### macOS
OpenSSL مضمن مع النظام. إذا لزم الأمر:
```bash
brew install openssl
```
### ويندوز
OpenSSL مضمن مع توزيع Nim ويندوز. للبناء اليدوي، قم بالتنزيل من [slproweb.com](https://slproweb.com/products/Win32OpenSSL.html).
## بناء BaraDB
### استنساخ المستودع
```bash
git clone https://github.com/katehonz/barabaDB.git
git clone https://codeberg.org/baraba/baradb
cd barabaDB
```
### تثبيت التبعيات
```bash
nimble install -d -y
```
### خيارات البناء
#### بناء التصحيح
```bash
nim c -d:ssl -o:build/baradadb src/baradadb.nim
```
#### بناء الإصدار (موصى به)
```bash
nim c -d:ssl -d:release --opt:speed -o:build/baradadb src/baradadb.nim
```
#### استخدام مهام Nimble
```bash
# بناء التصحيح
nimble build_debug
# بناء الإصدار
nimble build_release
```
#### تقليل حجم الملف الثنائي
```bash
nim c -d:ssl -d:release --opt:size -o:build/baradadb src/baradadb.nim
strip build/baradadb
```
### التحقق من البناء
```bash
./build/baradadb --version
# المتوقع: BaraDB v1.1.0 — Multimodal Database Engine
```
## تشغيل الاختبارات
### جميع الاختبارات
```bash
nim c -d:ssl -r tests/test_all.nim
```
### مجموعات اختبارات محددة
```bash
# اختبارات التخزين
nim c -d:ssl -r tests/test_storage.nim
# اختبارات محرك الاستعلام
nim c -d:ssl -r tests/test_query.nim
# اختبارات البروتوكول
nim c -d:ssl -r tests/test_protocol.nim
```
### المعايير
```bash
nim c -d:ssl -d:release -r benchmarks/bench_all.nim
```
## خيارات التثبيت
### التثبيت على مستوى النظام
```bash
# بناء إصدار RELEASE
nimble build_release
# التثبيت في /usr/local/bin
sudo cp build/baradadb /usr/local/bin/
sudo chmod +x /usr/local/bin/baradadb
# إنشاء دليل البيانات
sudo mkdir -p /var/lib/baradb
sudo chmod 755 /var/lib/baradb
```
### الملف الثنائي المبنى مسبقًا
قم بتنزيل أحدث إصدار لمنصتك:
```bash
# لينكس x86_64
wget https://github.com/katehonz/barabaDB/releases/latest/download/baradadb-linux-amd64
chmod +x baradadb-linux-amd64
mv baradadb-linux-amd64 /usr/local/bin/baradadb
# لينكس ARM64
wget https://github.com/katehonz/barabaDB/releases/latest/download/baradadb-linux-arm64
chmod +x baradadb-linux-arm64
mv baradadb-linux-arm64 /usr/local/bin/baradadb
# macOS
wget https://github.com/katehonz/barabaDB/releases/latest/download/baradadb-darwin-amd64
chmod +x baradadb-darwin-amd64
mv baradadb-darwin-amd64 /usr/local/bin/baradadb
```
### Docker
```bash
# سحب الصورة الرسمية
docker pull barabadb/barabadb:latest
# التشغيل
docker run -d \
-p 9472:9472 \
-p 9470:9470 \
-p 9471:9471 \
-v baradb_data:/data \
barabadb/barabadb
```
### Docker Compose
```bash
docker-compose up -d
```
### الاستخدام المدمج (مشاريع Nim)
أضف إلى ملف `.nimble` الخاص بك:
```nim
requires "barabadb >= 1.1.0"
```
استخدم في الكود:
```nim
import barabadb/storage/lsm
var db = newLSMTree("./data")
db.put("key", cast[seq[byte]]("value"))
let (found, val) = db.get("key")
db.close()
```
## التشغيل الأول
```bash
# بدء الخادم
./build/baradadb
# المخرجات المتوقعة:
# BaraDB v1.1.0 — Multimodal Database Engine
# BaraDB TCP listening on 127.0.0.1:9472
# الاختبار عبر HTTP API
curl http://localhost:9470/health
# الصدفة التفاعلية
./build/baradadb --shell
```
## حل مشكلات التثبيت
### "cannot open file: hunos"
```bash
nimble install -d -y
```
### "BaraDB requires SSL support"
قم دائمًا بالبناء باستخدام `-d:ssl`:
```bash
nim c -d:ssl -o:build/baradadb src/baradadb.nim
```
### البناء البطيء
استخدم البناء المتوازي:
```bash
nim c -d:ssl -d:release --parallelBuild:4 -o:build/baradadb src/baradadb.nim
```
### حجم الملف الثنائي الكبير
استخدم تحسين الحجم:
```bash
nim c -d:ssl -d:release --opt:size --passL:-s -o:build/baradadb src/baradadb.nim
```
## الخطوات التالية
- [دليل البداية السريعة](quickstart.md)
+107
View File
@@ -0,0 +1,107 @@
# API за Бинарен Протокол
Ниско-нивов wire протокол за високопроизводителни клиентски връзки.
## Формат на Съобщенията
Всички съобщения използват big-endian byte order:
```
┌─────────────┬─────────────┬─────────────┬─────────────────────┐
│ Kind │ Length │ RequestId │ Payload │
│ (4 bytes) │ (4 bytes) │ (4 bytes) │ │
│ uint32 BE │ uint32 BE │ uint32 BE │ (Length bytes) │
└─────────────┴─────────────┴─────────────┴─────────────────────┘
```
## Типове Съобщения
### Query (0x01)
```nim
let msg = makeQueryMessage(requestId, "SELECT * FROM users")
```
### QueryParams (0x02)
```nim
let msg = makeQueryParamsMessage(requestId, "SELECT * FROM users WHERE name = ?", params)
```
### Auth (0x07)
```nim
let msg = makeAuthMessage(requestId, token)
```
### Ping (0x09)
```nim
let msg = makePingMessage(requestId)
```
### Close (0x0A)
```nim
let msg = makeCloseMessage(requestId)
```
## Отговорни Съобщения
### Ready (0x05)
```nim
let msg = makeReadyMessage(requestId)
```
### Error (0x06)
```nim
let msg = makeErrorMessage(requestId, code, message)
```
### Data (0x81)
```nim
# Съдържа резултатите от заявката с колони и редове
```
### Complete (0x82)
```nim
# Потвърждава завършване на заявката
```
### Auth_OK (0x83)
```nim
# Потвърждава успешна автентикация
```
### Pong (0x84)
```nim
# Keepalive отговор
```
## Кодове за Грешки
| Код | Име | Описание |
|------|-----|----------|
| 0x00 | OK | Успех |
| 0x01 | ERROR | Обща грешка |
| 0x02 | AUTH_REQUIRED | Изисква се автентикация |
| 0x03 | INVALID_QUERY | Синтактична грешка в заявката |
| 0x04 | NOT_FOUND | Ресурсът не е намерен |
## Сериализация
```nim
import barabadb/protocol/wire
# Сериализиране на стойност
let bytes = serializeValue(Value(kind: vkString, strVal: "test"))
# Десериализиране на стойност
let value = deserializeValue(bytes)
```
+111
View File
@@ -0,0 +1,111 @@
# HTTP/REST API
JSON-базиран REST API за уеб приложения.
## Базов URL
```
http://localhost:9470
```
## Endpoints
### GET /health
Health проверка:
```bash
curl http://localhost:9470/health
```
### GET /ready
Readiness проверка:
```bash
curl http://localhost:9470/ready
```
### POST /query
Изпълнение на BaraQL заявка:
```bash
curl -X POST http://localhost:9470/api/query \
-H "Content-Type: application/json" \
-d '{"query": "SELECT * FROM users WHERE age > 18"}'
```
Отговор:
```json
{
"columns": ["name", "age"],
"rows": [["Alice", 30], ["Bob", 25]],
"row_count": 2,
"duration_ms": 12
}
```
### POST /batch
Групови заявки:
```bash
curl -X POST http://localhost:9470/api/batch \
-H "Content-Type: application/json" \
-d '{"queries": ["INSERT users { name := \"Alice\" }", "INSERT users { name := \"Bob\" }"]}'
```
### GET /schema
Преглед на схемата:
```bash
curl http://localhost:9470/api/schema
```
### GET /metrics
Prometheus метрики:
```bash
curl http://localhost:9470/metrics
```
### POST /explain
Обяснение на план за изпълнение:
```bash
curl -X POST http://localhost:9470/api/explain \
-H "Content-Type: application/json" \
-d '{"query": "SELECT * FROM users WHERE age > 18"}'
```
### POST /backup
Създаване на backup:
```bash
curl -X POST http://localhost:9470/api/backup \
-H "Content-Type: application/json" \
-d '{"destination": "/backup/snapshot.db"}'
```
## Грешки
```json
{
"error": {
"code": "INVALID_QUERY",
"message": "Грешка в синтаксиса"
}
}
```
## Автентикация
```bash
curl -H "Authorization: Bearer <token>" \
http://localhost:9470/api/users
```
+127
View File
@@ -0,0 +1,127 @@
# WebSocket API
Full-duplex стрийминг за данни в реално време и push известия.
## Свързване
```
ws://localhost:9471
```
## Клиентски Пример
```javascript
const ws = new WebSocket('ws://localhost:9471');
ws.onopen = () => {
console.log('Свързан');
ws.send(JSON.stringify({
type: 'query',
query: 'SELECT * FROM users'
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Получено:', data);
};
```
## Формат на Съобщенията
```json
{
"type": "query",
"id": "1",
"query": "SELECT * FROM users"
}
```
## Типове Съобщения
### Заявка (query)
```json
{
"type": "query",
"id": "1",
"query": "SELECT * FROM users"
}
```
### Резултат (result)
```json
{
"type": "result",
"id": "1",
"columns": ["id", "name"],
"rows": [["1", "Alice"], ["2", "Bob"]]
}
```
### Грешка (error)
```json
{
"type": "error",
"id": "1",
"code": "INVALID_QUERY",
"message": "Синтактична грешка"
}
```
### Абониране (subscribe)
Абониране за промени в таблица:
```json
{
"type": "subscribe",
"id": "sub1",
"table": "users"
}
```
### Известие (notification)
Push известие от сървъра:
```json
{
"type": "notification",
"table": "users",
"operation": "insert",
"data": {"id": 3, "name": "Charlie"}
}
```
### Ping/Pong (keepalive)
```json
{"type": "ping", "id": "ping1"}
```
Отговор:
```json
{"type": "pong", "id": "ping1"}
```
## JavaScript Клиент
```javascript
class BaraDBClient {
constructor(url) {
this.ws = new WebSocket(url);
this.pending = new Map();
}
query(sql) {
return new Promise((resolve, reject) => {
const id = crypto.randomUUID();
this.pending.set(id, { resolve, reject });
this.ws.send(JSON.stringify({ type: 'query', id, query: sql }));
});
}
}
```
+69 -94
View File
@@ -2,150 +2,125 @@
## Преглед
BaraDB е **мултимодална база данни** написана на Nim, която комбинира документно (KV), графично, векторно, колонно и пълнотекстово търсене в един двигател с обединен език за заявки наречен **BaraQL**.
BaraDB е **мултимодален database engine**, написан на Nim, който комбинира документно (KV), графово, векторно, колонково и пълнотекстово съхранение в един engine с унифициран език за заявки наречен **BaraQL**.
## Слоеста Архитектура
## Слоеве на Архитектурата
```
┌─────────────────────────────────────────────────────────┐
│ 1. СЛОЙ ЗА КЛИЕНТИ
Binary Protocol │ HTTP/REST │ WebSocket │ Embedded
│ 1. КЛИЕНТСКИ СЛОЙ │
Бинарен Протокол │ HTTP/REST │ WebSocket │ Embedded │
├─────────────────────────────────────────────────────────┤
│ 2. ЗАЯВКИ СЛОЙ (BaraQL) │
│ 2. QUERY СЛОЙ (BaraQL)
│ Lexer → Parser → AST → IR → Optimizer → Codegen │
├─────────────────────────────────────────────────────────┤
│ 3. ИЗПЪЛНИТЕЛЕН ДВИГАТЕЛ
Document │ Graph │ Vector │ Columnar │ FTS
│ 3. ИЗПЪЛНИТЕЛЕН ENGINE
Документен │ Графов │ Векторен │ Колонков │ FTS │
├─────────────────────────────────────────────────────────┤
│ 4. СЪХРАНЕНИЕ
│ 4. СЪХРАНЕНИЕ │
│ LSM-Tree │ B-Tree │ WAL │ Bloom │ Compaction │ Cache │
├─────────────────────────────────────────────────────────┤
│ 5. РАЗПРЕДЕЛЕНО
│ Raft Consensus │ Sharding │ Replication │ Gossip │
│ 5. РАЗПРЕДЕЛЕНИ
│ Raft Консенсус │ Шардиране │ Репликация │ Gossip │
└─────────────────────────────────────────────────────────┘
```
## Слой 1: Клиентски Слой
Множество протоколи за комуникация:
Множество комуникационни протоколи:
- **Binary Protocol** (`protocol/wire.nim`): Ефективен big-endian бинарен протокол с 16 типа съобщения
- **HTTP/REST** (`core/httpserver.nim`): JSON REST API с мулти-трединг
- **WebSocket** (`core/websocket.nim`): Пълен дуплекс стрийминг
- **Бинарен Протокол** (`protocol/wire.nim`): Ефективен big-endian бинарен протокол с 16 типа съобщения
- **HTTP/REST** (`core/httpserver.nim`): JSON-базиран REST API с multi-threading
- **WebSocket** (`core/websocket.nim`): Full-duplex стрийминг
- **Embedded** (`storage/lsm.nim`): Директен in-process достъп
### Управление на Връзките
### Управление на Връзки
- **Connection Pool** (`protocol/pool.nim`): Мин/макс лимити на връзки
- **Rate Limiting** (`protocol/ratelimit.nim`): Token-bucket лимитиране
- **Authentication** (`protocol/auth.nim`): JWT с HMAC-SHA256
- **TLS/SSL** (`protocol/ssl.nim`): TLS 1.3 с авто-генерирани сертификати
- **Connection Pool** (`protocol/pool.nim`): Min/max лимити на връзки с idle timeout
- **Rate Limiting** (`protocol/ratelimit.nim`): Token-bucket глобални и per-client лимити
- **Автентикация** (`protocol/auth.nim`): JWT с HMAC-SHA256 и достъп на база роли
- **TLS/SSL** (`protocol/ssl.nim`): TLS 1.3 с автоматично генерирани сертификати
## Слой 2: Заявки (BaraQL)
## Слой 2: Query Слой (BaraQL)
Pipeline-а на BaraQL:
BaraQL конвейрът:
1. **Lexer** (`query/lexer.nim`): Токенизира входа в 80+ типа токени
2. **Parser** (`query/parser.nim`): Рекурсивен descent парсър произвеждащ AST
2. **Parser** (`query/parser.nim`): Recursive descent parser генериращ AST
3. **AST** (`query/ast.nim`): 300+ реда покриващи 25+ вида възли
4. **IR** (`query/ir.nim`): Междинно представяне за планове за изпълнение
5. **Optimizer** (`query/adaptive.nim`): Адаптивен крос-модален оптимизатор
6. **Codegen** (`query/codegen.nim`): Транслира IR към операции върху съхранение
4. **IR** (`query/ir.nim`): Intermediate representation за планове за изпълнение
5. **Optimizer** (`query/adaptive.nim`): Adaptive cross-modal оптимизация на заявки
6. **Codegen** (`query/codegen.nim`): Превежда IR към storage операции
7. **Executor** (`query/executor.nim`): Изпълнява планове с паралелизация
### Крос-Модално Планиране
## Слой 3: Изпълнителен Engine
Оптимизаторът определя реда на изпълнение между двигателите:
### Документен/KV Engine
- **LSM-Tree** (`storage/lsm.nim`): Write-оптимизирано съхранение с MemTable, WAL, SSTables
- **B-Tree Индекс** (`storage/btree.nim`): Подреден индекс за range сканиране с COW
```
1. Оценка на селективност за всеки предикат
2. Най-селективният предикат се изпълнява първи
3. Bloom филтри за KV търсения
4. Паралелизация на независими клонове
```
### Vector Engine (`vector/`)
- **HNSW Индекс** (`vector/engine.nim`): Hierarchical Navigable Small World граф
- **IVF-PQ Индекс** (`vector/engine.nim`): Inverted File Index с Product Quantization
- **SIMD Операции** (`vector/simd.nim`): AVX2-оптимизирани изчисления на разстояние
- **Квантуване** (`vector/quant.nim`): Скаларно, продуктово и бинарно квантуване
## Слой 3: Изпълнителен Двигател
### Document/KV Двигател
- **LSM-Tree** (`storage/lsm.nim`): Оптимизиран за запис с MemTable, WAL, SSTables
- **B-Tree Index** (`storage/btree.nim`): Подреден индекс за диапазони с COW
### Vector Engine
- **HNSW** (`vector/engine.nim`): Иерархичен навигируем малък свят
- **IVF-PQ** (`vector/engine.nim`): Инвертиран файл с продуктово квантуване
- **SIMD** (`vector/simd.nim`): AVX2-оптимизирани изчисления на разстояния
- **Quantization** (`vector/quant.nim`): Скаларно, продуктово и бинарно квантуване
### Graph Engine
- **Списък със съседи** (`graph/engine.nim`): Насочен граф с тегла
### Graph Engine (`graph/`)
- **Adjacency List** (`graph/engine.nim`): Насочен граф с тегла на ребрата
- **Алгоритми** (`graph/engine.nim`): BFS, DFS, Dijkstra, PageRank
- **Community Detection** (`graph/community.nim`): Louvain алгоритъм
- **Pattern Matching** (`graph/community.nim`): Subgraph isomorphism
- **Cypher Parser** (`graph/cypher.nim`): Cypher-подобни заявки
- **Pattern Matching** (`graph/community.nim`): Subgraph изоморфизъм
- **Cypher Parser** (`graph/cypher.nim`): Cypher-подобни графови заявки
### FTS
- **Инвертиран индекс** (`fts/engine.nim`): Термин-документ индекс
- **Ранжиране** (`fts/engine.nim`): BM25 и TF-IDF
- **Fuzzy Search** (`fts/engine.nim`): Levenshtein разстояние
- **Многоезичен** (`fts/multilang.nim`): Токенизация за EN, BG, DE, FR, RU
### Full-Text Search (`fts/`)
- **Inverted Index** (`fts/engine.nim`): Термин-документен индекс
- **Ранжиране** (`fts/engine.nim`): BM25 и TF-IDF оценяване
- **Fuzzy Търсене** (`fts/engine.nim`): Съвпадение с Levenshtein разстояние
- **Многоезичност** (`fts/multilang.nim`): Токенизатори за EN, BG, DE, FR, RU
### Columnar Engine
- **Колонно съхранение** (`core/columnar.nim`): Аналитични заявки
- **Компресия**: RLE и dictionary encoding
- **SIMD агрегати**: Ускорени агрегатни функции
### Columnar Engine (`core/columnar.nim`)
- Колонково съхранение за аналитични заявки
- RLE и dictionary encoding
- SIMD-ускорени агрегати
## Слой 4: Съхранение
- **LSM-Tree** (`storage/lsm.nim`): MemTable, WAL, SSTable, Bloom Filter, Compaction
- **Page Cache** (`storage/compaction.nim`): LRU кеш
- **Memory-mapped I/O** (`storage/mmap.nim`): mmap-базиран достъп
- **Recovery** (`storage/recovery.nim`): WAL replay и възстановяване
- **Page Cache** (`storage/compaction.nim`): LRU кеш с проследяване на hit rate
- **Memory-mapped I/O** (`storage/mmap.nim`): mmap-базиран достъп до файлове
- **Recovery** (`storage/recovery.nim`): WAL replay и crash recovery
### Път на Запис
## Слой 5: Разпределение
```
Client → Protocol → Auth → Parser → AST → IR → Codegen
→ StorageOp → MVCC Txn → WAL Write → MemTable → Commit
```
- **Raft Консенсус** (`core/raft.nim`): Leader election, log репликация
- **Шардиране** (`core/sharding.nim`): Hash, range и consistent hashing
- **Репликация** (`core/replication.nim`): Sync, async, semi-sync режими
- **Gossip Протокол** (`core/gossip.nim`): SWIM-подобно управление на членство
- **Разпределени Транзакции** (`core/disttxn.nim`): Two-phase commit
### Път на Четене
```
Client → Protocol → Auth → Parser → AST → IR → Codegen
→ StorageOp → MVCC Snapshot → MemTable → SSTable → Result
```
## Слой 5: Разпределено
- **Raft Consensus** (`core/raft.nim`): Лидерско избиране, репликация на логове
- **Sharding** (`core/sharding.nim`): Hash, range и консистентно хеширане
- **Replication** (`core/replication.nim`): Sync, async, semi-sync режими
- **Gossip Protocol** (`core/gossip.nim`): SWIM-подобно управление на членство
- **Distributed Transactions** (`core/disttxn.nim`): Two-phase commit
## Ключови Дизайн Решения
## Ключови Дизайнерски Решения
1. **Чист Nim**: Без Cython, Python или Rust зависимости
2. **Обединено Съхранение**: Един двигател за KV, граф, вектор, FTS и колонно
3. **Вграден Режим**: Работи като библиотека или сървър
4. **Бинарен Протокол**: Ефективен wire протокол
2. **Унифицирано Съхранение**: Един engine обработва KV, graph, vector, FTS и columnar
3. **Embedded Режим**: Може да работи като библиотека или сървър
4. **Бинарен Протокол**: Персонализиран ефективен wire протокол
5. **MVCC**: Multi-version concurrency control
6. **Schema-First**: Строго типизирана система с наследяване
7. **Крос-Модал**: Един език за заявки през всички модели данни
8. **Формално Верифициран**: Разпределените алгоритми са специфицирани в TLA+ и проверени с TLC
6. **Schema-First**: Силно типизирана система от схеми с наследяване
7. **Cross-Modal**: Един език за заявки за всички модели на данни
8. **Формално Верифициран**: Основните разпределени алгоритми са специфицирани в TLA+ и проверени с TLC
## Статистика на Модулите
| Категория | Модули | Редове Код | Предназначение |
|-----------|--------|------------|----------------|
| Core | 16 | ~4,200 | Сървър, протоколи, транзакции, разпределено |
| Core | 16 | ~4,200 | Сървър, протоколи, транзакции, разпределени |
| Storage | 7 | ~3,100 | LSM, B-Tree, WAL, bloom, compaction, mmap |
| Query | 7 | ~2,800 | Lexer, parser, AST, IR, оптимизатор, codegen, executor |
| Query | 7 | ~2,800 | Lexer, parser, AST, IR, optimizer, codegen, executor |
| Vector | 3 | ~1,200 | HNSW, IVF-PQ, квантуване, SIMD |
| Graph | 3 | ~1,000 | Списък със съседи, алгоритми, community detection |
| FTS | 2 | ~900 | Инвертиран индекс, BM25, fuzzy, многоезичен |
| Graph | 3 | ~1,000 | Adjacency list, алгоритми, community detection |
| FTS | 2 | ~900 | Inverted index, BM25, fuzzy, многоезичност |
| Protocol | 7 | ~2,400 | Wire, HTTP, WebSocket, pool, auth, rate limit, SSL |
| Schema | 1 | ~600 | Типове, връзки, наследяване, миграции |
| Client | 2 | ~800 | Nim binary client, file helpers |
| CLI | 1 | ~400 | Интерактивна BaraQL конзола |
| Client | 2 | ~800 | Nim бинарен клиент, файлови помощници |
| CLI | 1 | ~400 | Интерактивна BaraQL обвивка |
| **Общо** | **49** | **~14,100** | |
+198
View File
@@ -0,0 +1,198 @@
# Backup и Възстановяване
## Online Snapshots
BaraDB поддържа online snapshots без спиране на сървъра. Snapshot-ът заснема консистентен изглед към момент във времето чрез MVCC.
### Създаване на Snapshot
```nim
import barabadb/core/backup
var bm = newBackupManager()
bm.createSnapshot("/backup/baradb_2025-01-15")
```
### Чрез CLI
```bash
./build/baradadb --snapshot --output=/backup/snapshot.db
```
### Чрез HTTP API
```bash
curl -X POST http://localhost:9470/api/backup \
-H "Content-Type: application/json" \
-d '{"destination": "/backup/snapshot.db"}'
```
### Автоматизирани Backups
Използвайте cron за планирани backups:
```bash
# Ежедневен snapshot в 2 сутринта
0 2 * * * /usr/local/bin/baradadb --snapshot --output=/backup/baradb_$(date +\%Y\%m\%d).db
# Запазване на последните 7 дни
find /backup -name "baradb_*.db" -mtime +7 -delete
```
## Point-in-Time Recovery (PITR)
BaraDB използва Write-Ahead Log (WAL) за възстановяване до момент във времето.
### WAL Архивиране
Включете непрекъснато WAL архивиране:
```bash
BARADB_WAL_ARCHIVE_DIR=/backup/wal \
BARADB_WAL_ARCHIVE_INTERVAL_MS=60000 \
./build/baradadb
```
### Възстановяване от Checkpoint + WAL
```bash
# Възстановяване от snapshot
./build/baradadb --recover \
--checkpoint=/backup/snapshot.db \
--wal-dir=/backup/wal
# Възстановяване до конкретен LSN
./build/baradadb --recover \
--checkpoint=/backup/snapshot.db \
--wal-dir=/backup/wal \
--target-lsn=15420
# Възстановяване до конкретно време
./build/baradadb --recover \
--checkpoint=/backup/snapshot.db \
--wal-dir=/backup/wal \
--target-time="2025-01-15T10:30:00Z"
```
### Възстановяване чрез SQL
Можете също да възстановявате директно чрез BaraQL:
```sql
RECOVER TO TIMESTAMP '2026-05-07T12:00:00';
```
### Инкрементални Backups
Инкременталните backups копират само променени SSTables:
```bash
./build/baradadb --backup-incremental \
--last-backup=/backup/previous \
--output=/backup/incremental_$(date +%Y%m%d)
```
## Репликация като Backup
За непрекъсната защита използвайте streaming репликация:
### Primary
```bash
BARADB_REPLICATION_ENABLED=true \
BARADB_REPLICATION_MODE=async \
./build/baradadb
```
### Replica
```bash
BARADB_REPLICATION_ENABLED=true \
BARADB_REPLICATION_PRIMARY=primary:9472 \
./build/baradadb
```
## Disaster Recovery
### Процедури за Възстановяване
#### Сценарий 1: Повреда на Единичен Файл
```bash
# Идентифициране на повреден SSTable от логовете
# Възстановяване на конкретен SSTable от backup
cp /backup/sstables/000012.sst ./data/sstables/
# Възстановяване на индекса
./build/baradadb --rebuild-index
```
#### Сценарий 2: Пълна Загуба на Данни
```bash
# 1. Възстановяване на последния snapshot
cp /backup/snapshot.db ./data/
# 2. Преиграване на WAL
./build/baradadb --recover --wal-dir=/backup/wal
# 3. Проверка
curl http://localhost:9470/health
```
#### Сценарий 3: Отказ на Възел в Клъстер
```bash
# За Raft клъстери, просто стартирайте нов възел
BARADB_RAFT_NODE_ID=newnode \
BARADB_RAFT_PEERS=node1:9001,node2:9001 \
./build/baradadb
# Новият възел ще навакса чрез Raft log репликация
```
## Верификация на Backup
Винаги проверявайте backups:
```bash
# Възстановяване във временна директория
./build/baradadb --recover \
--checkpoint=/backup/snapshot.db \
--data-dir=/tmp/verify_data
# Проверка на консистентност
curl http://localhost:9470/api/admin/check
```
## Изисквания за Съхранение
| Тип Backup | Размер | Честота | Задържане |
|------------|--------|---------|-----------|
| Пълен snapshot | ~1× размер на данните | Ежедневно | 7 дни |
| Инкрементален | ~0.1× размер на данните | На всеки час | 24 часа |
| WAL архив | ~0.05× размер на данните / ден | Непрекъснато | 30 дни |
## Най-добри Практики
1. **Тествайте възстановяването редовно** — Backup, който не може да бъде възстановен, е безполезен
2. **Съхранявайте backups извън локацията** — Използвайте S3, GCS или Azure Blob
3. **Криптирайте backups** — Използвайте `gpg` или криптиране на ниво ОС
4. **Мониторирайте backup задачите** — Алармирайте при неуспешни backups
5. **Документирайте RTO/RPO** — Знайте целите си за време и точка на възстановяване
### Качване на Backup в Облак
```bash
# Качване в S3
aws s3 cp /backup/snapshot.db s3://my-bucket/baradb/
# Качване в GCS
gsutil cp /backup/snapshot.db gs://my-bucket/baradb/
# Качване в Azure
az storage blob upload \
--container-name backups \
--file /backup/snapshot.db \
--name baradb/snapshot.db
```
+559 -32
View File
@@ -2,39 +2,160 @@
BaraQL е SQL-съвместим език за заявки с разширения за графи, вектори и документи.
## Типове Данни
| Тип | Описание | Пример |
|------|----------|--------|
| `null` | Null стойност | `null` |
| `bool` | Булев | `true`, `false` |
| `int8` | 8-битов signed integer | `127` |
| `int16` | 16-битов signed integer | `32767` |
| `int32` | 32-битов signed integer | `2147483647` |
| `int64` | 64-битов signed integer | `9223372036854775807` |
| `float32` | 32-битов float | `3.14` |
| `float64` | 64-битов float | `3.14159265359` |
| `str` | UTF-8 низ | `'hello'` |
| `bytes` | Сурови байтове | `0xDEADBEEF` |
| `array<T>` | Хомогенен масив | `[1, 2, 3]` |
| `vector` | Float32 вектор | `[0.1, 0.2, 0.3]` |
| `vector(n)` | Float32 вектор с фиксирана размерност (SQL) | `VECTOR(768)` |
| `object` | Ключ-стойност обект | `{"a": 1}` |
| `datetime` | ISO 8601 времеви печат | `'2025-01-15T10:30:00Z'` |
| `uuid` | UUID v4 | `'550e8400-e29b-41d4-a716-446655440000'` |
| `json` | JSON документ | `{"key": "value"}` |
| `jsonb` | Бинарен JSON (валидиран) | `{"key": "value"}` |
## Основни Заявки
### SELECT
```sql
SELECT name, age FROM users WHERE age > 18 ORDER BY name LIMIT 10;
-- Всички колони
SELECT * FROM users;
-- Конкретни колони
SELECT name, age FROM users;
-- Псевдоними
SELECT name AS full_name, age AS years FROM users;
-- DISTINCT
SELECT DISTINCT department FROM employees;
-- LIMIT и OFFSET
SELECT * FROM users LIMIT 10 OFFSET 20;
```
### WHERE
```sql
-- Оператори за сравнение
SELECT * FROM users WHERE age > 18;
SELECT * FROM users WHERE age >= 18 AND age <= 65;
SELECT * FROM users WHERE name = 'Alice';
SELECT * FROM users WHERE name != 'Bob';
-- Диапазон
SELECT * FROM users WHERE age BETWEEN 18 AND 65;
-- Принадлежност към множество
SELECT * FROM users WHERE department IN ('Engineering', 'Sales');
-- Търсене по шаблон
SELECT * FROM users WHERE name LIKE 'A%';
SELECT * FROM users WHERE name ILIKE 'alice'; -- Case-insensitive
-- NULL проверки
SELECT * FROM users WHERE email IS NOT NULL;
-- Логически оператори
SELECT * FROM users WHERE age > 18 AND (department = 'Engineering' OR department = 'Sales');
```
### ORDER BY
```sql
-- Възходящ (по подразбиране)
SELECT * FROM users ORDER BY age;
-- Низходящ
SELECT * FROM users ORDER BY age DESC;
-- Множество колони
SELECT * FROM users ORDER BY department ASC, age DESC;
```
### INSERT
```sql
-- Един ред
INSERT users { name := 'Alice', age := 30 };
-- С явен тип
INSERT User { name := 'Alice', age := 30 };
-- Множество редове
INSERT users {
{ name := 'Alice', age := 30 },
{ name := 'Bob', age := 25 }
};
```
### UPDATE
```sql
-- Обнови всички редове
UPDATE users SET status = 'active';
-- Условно обновяване
UPDATE users SET age = 31 WHERE name = 'Alice';
-- Обновяване на няколко колони
UPDATE users SET age = 32, status = 'premium' WHERE name = 'Alice';
```
### DELETE
```sql
DELETE FROM users WHERE name = 'Bob';
-- Изтрий всички редове
DELETE FROM users;
-- Условно изтриване
DELETE FROM users WHERE age < 18;
```
## Агрегати и Групиране
### Агрегатни Функции
| Функция | Описание |
|----------|-----------|
| `count(*)` | Брой на всички редове |
| `count(column)` | Брой на не-NULL стойности |
| `sum(column)` | Сума на стойностите |
| `avg(column)` | Средно аритметично |
| `min(column)` | Минимална стойност |
| `max(column)` | Максимална стойност |
| `stddev(column)` | Стандартно отклонение |
| `variance(column)` | Дисперсия |
### GROUP BY
```sql
SELECT department, count(*), avg(salary)
SELECT department, count(*) as emp_count, avg(salary) as avg_salary
FROM employees
GROUP BY department;
-- С HAVING
SELECT department, count(*) as emp_count
FROM employees
GROUP BY department
HAVING count(*) > 5;
-- Множествено групиране
SELECT department, role, count(*), avg(salary)
FROM employees
GROUP BY department, role;
```
## JOINs
@@ -49,15 +170,85 @@ INNER JOIN orders o ON u.id = o.user_id;
SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
-- RIGHT JOIN
SELECT u.name, o.total
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
-- FULL JOIN
SELECT u.name, o.total
FROM users u
FULL JOIN orders o ON u.id = o.user_id;
-- CROSS JOIN
SELECT u.name, p.name
FROM users u
CROSS JOIN products p;
-- Множество JOINs
SELECT u.name, o.id, p.name
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN products p ON o.product_id = p.id;
-- Self JOIN
SELECT e.name, m.name as manager
FROM employees e
JOIN employees m ON e.manager_id = m.id;
```
## CTEs (Common Table Expressions)
```sql
-- Единичен CTE
WITH active_users AS (
SELECT * FROM users WHERE active = true
)
SELECT * FROM active_users;
-- Множество CTEs
WITH
recent AS (
SELECT * FROM orders WHERE date > '2025-01-01'
),
totals AS (
SELECT user_id, sum(amount) as total FROM recent GROUP BY user_id
)
SELECT u.name, t.total
FROM users u
JOIN totals t ON u.id = t.user_id;
-- Рекурсивен CTE
WITH RECURSIVE subordinates AS (
SELECT id, name, manager_id FROM employees WHERE name = 'CEO'
UNION ALL
SELECT e.id, e.name, e.manager_id
FROM employees e
JOIN subordinates s ON e.manager_id = s.id
)
SELECT * FROM subordinates;
```
## Подзаявки
```sql
-- Подзаявка в SELECT
SELECT name, (SELECT count(*) FROM orders WHERE user_id = u.id) as order_count
FROM users u;
-- Подзаявка в FROM
SELECT * FROM (SELECT id, name FROM users WHERE active = true) AS active;
-- Подзаявка в WHERE (IN)
SELECT name FROM users WHERE id IN (SELECT user_id FROM orders);
-- Подзаявка в WHERE (EXISTS)
SELECT name FROM users WHERE EXISTS (SELECT 1 FROM orders WHERE orders.user_id = users.id);
-- Корелирана подзаявка
SELECT name FROM users u
WHERE age > (SELECT avg(age) FROM users WHERE department = u.department);
```
## CASE Изрази
@@ -65,54 +256,114 @@ SELECT * FROM active_users;
```sql
SELECT name,
CASE
WHEN age < 18 THEN 'minor'
WHEN age < 13 THEN 'child'
WHEN age < 20 THEN 'teenager'
WHEN age < 65 THEN 'adult'
ELSE 'senior'
END AS category
FROM users;
-- Прост CASE
SELECT name,
CASE department
WHEN 'Engineering' THEN 'Tech'
WHEN 'Sales' THEN 'Revenue'
ELSE 'Other'
END AS division
FROM employees;
```
## Схема
## Set Операции
```sql
-- UNION (различни)
SELECT name FROM customers
UNION
SELECT name FROM suppliers;
-- UNION ALL (с дубликати)
SELECT name FROM customers
UNION ALL
SELECT name FROM suppliers;
-- INTERSECT
SELECT name FROM customers
INTERSECT
SELECT name FROM suppliers;
-- EXCEPT
SELECT name FROM customers
EXCEPT
SELECT name FROM suppliers;
```
## Дефиниране на Схема
### CREATE TYPE
```sql
CREATE TYPE Person {
name: str,
age: int32
};
```
## Векторно Търсене
```sql
INSERT articles {
title := 'Nim Programming',
embedding := [0.1, 0.2, 0.3, ...]
-- Със задължителни полета
CREATE TYPE User {
email: str REQUIRED,
name: str,
age: int32,
created_at: datetime DEFAULT now()
};
SELECT title FROM articles
ORDER BY cosine_distance(embedding, [0.1, 0.2, 0.3, ...])
LIMIT 5;
-- С връзки
CREATE TYPE Movie {
title: str,
year: int32,
director: Person
};
-- С изчислими свойства
CREATE TYPE Employee {
name: str,
base_salary: float64,
bonus: float64,
total_compensation: float64 COMPUTED (base_salary + bonus)
};
```
## Графични Шаблони
### Наследяване
```sql
MATCH (p:Person)-[:KNOWS]->(friend:Person)
WHERE p.name = 'Alice'
RETURN friend.name;
CREATE TYPE Animal {
name: str
};
CREATE TYPE Dog EXTENDING Animal {
breed: str
};
CREATE TYPE Cat EXTENDING Animal {
indoor: bool
};
```
## Пълнотекстово Търсене
### Индекси
```sql
-- Създаване на FTS индекс
CREATE INDEX idx_fts ON articles(body) USING FTS;
-- Търсене с BM25 ранжиране
SELECT * FROM articles WHERE body @@ 'database programming';
CREATE INDEX idx_users_name ON users(name);
CREATE UNIQUE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_age ON users(age) USING btree;
CREATE INDEX idx_vectors ON items(embedding) USING hnsw;
```
## JSON Оператори
### DROP
```sql
DROP TYPE User;
DROP INDEX idx_users_name;
```
### JSON Оператори за Път
```sql
-- Извличане на JSON поле като JSON
@@ -122,16 +373,292 @@ SELECT data->'name' FROM users;
SELECT data->>'name' FROM users;
```
## Set Операции
### Пълнотекстово Търсене (SQL)
```sql
SELECT name FROM customers
UNION ALL
SELECT name FROM suppliers;
-- Създаване на FTS индекс с BM25
CREATE INDEX idx_fts ON articles(body) USING FTS;
-- Търсене с BM25 ранжиране
SELECT * FROM articles WHERE body @@ 'machine learning';
```
## Възстановяване до Момент във Времето
### Възстановяване до Момент във Времето
```sql
RECOVER TO TIMESTAMP '2026-05-07T12:00:00';
```
```
## Векторно Търсене (SQL)
### Създаване на Векторни Колони
```sql
CREATE TABLE items (
id INT PRIMARY KEY,
embedding VECTOR(768)
);
```
### Вмъкване на Вектори
```sql
INSERT INTO items (id, embedding) VALUES (1, '[0.1, 0.2, 0.3, 0.4]');
```
### Функции за Разстояние
```sql
-- Косинусово разстояние (0 = идентични, 2 = противоположни)
SELECT id, cosine_distance(embedding, '[0.1, 0.2, 0.3, 0.4]') AS dist
FROM items;
-- Евклидово / L2 разстояние
SELECT id, euclidean_distance(embedding, '[0.1, 0.2, 0.3, 0.4]') AS dist
FROM items;
-- L2 разстояние с <-> оператор
SELECT id, embedding <-> '[0.1, 0.2, 0.3, 0.4]' AS dist
FROM items;
-- Скаларно произведение (отрицателно dot product)
SELECT id, inner_product(embedding, '[0.1, 0.2, 0.3, 0.4]') AS dist
FROM items;
-- Манхатън / L1 разстояние
SELECT id, l1_distance(embedding, '[0.1, 0.2, 0.3, 0.4]') AS dist
FROM items;
```
### Търсене на Най-близки Съседи
```sql
-- Топ-10 най-близки съседи по косинусово разстояние
SELECT id FROM items
ORDER BY cosine_distance(embedding, '[0.1, 0.2, 0.3, 0.4]') ASC
LIMIT 10;
-- Топ-5 най-близки съседи по евклидово разстояние
SELECT id FROM items
ORDER BY embedding <-> '[0.1, 0.2, 0.3, 0.4]'
LIMIT 5;
-- С филтър по метаданни
SELECT id FROM items
WHERE category = 'tech'
ORDER BY cosine_distance(embedding, '[0.1, 0.2, 0.3, 0.4]')
LIMIT 5;
```
### Векторни Индекси
```sql
-- Създаване на HNSW индекс за приблизително търсене на най-близки съседи
CREATE INDEX idx_items_vec ON items(embedding) USING hnsw;
-- Поддържани индекс методи: hnsw, ivfpq
```
## Графични Шаблони
```sql
-- Намиране на приятели на Alice
MATCH (p:Person)-[:KNOWS]->(friend:Person)
WHERE p.name = 'Alice'
RETURN friend.name;
-- Намиране на най-кратък път
MATCH path = shortestPath((a:Person)-[:KNOWS*1..5]->(b:Person))
WHERE a.name = 'Alice' AND b.name = 'Bob'
RETURN path;
-- Намиране на всички връзки
MATCH (p:Person)-[r]->(other)
WHERE p.name = 'Alice'
RETURN type(r), other.name;
-- Множество преходи
MATCH (a:Person)-[:KNOWS]->(b:Person)-[:KNOWS]->(c:Person)
WHERE a.name = 'Alice'
RETURN c.name;
-- С агрегати
MATCH (p:Person)-[:KNOWS]->(friend)
RETURN p.name, count(friend) as friend_count
ORDER BY friend_count DESC;
```
## Пълнотекстово Търсене
```sql
-- Основно търсене
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('database programming');
-- С релевантност
SELECT title, relevance()
FROM articles
WHERE MATCH(title, body) AGAINST('Nim language')
ORDER BY relevance() DESC;
-- Булев режим
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('+Nim -Python' IN BOOLEAN MODE);
-- Fuzzy търсене
SELECT * FROM articles
WHERE MATCH(title) AGAINST('programing' WITH FUZZINESS 2);
```
## Транзакции
```sql
BEGIN;
INSERT users { name := 'Alice', age := 30 };
INSERT orders { user_id := last_insert_id(), total := 100 };
COMMIT;
-- С savepoint
BEGIN;
INSERT users { name := 'Bob', age := 25 };
SAVEPOINT sp1;
INSERT orders { user_id := last_insert_id(), total := 200 };
-- Грешка, връщане до savepoint
ROLLBACK TO sp1;
COMMIT;
```
## Потребителски Функции (UDF)
```sql
-- Регистриране на UDF
CREATE FUNCTION greet(name str) -> str {
RETURN 'Hello, ' || name || '!';
};
-- Използване
SELECT greet(name) FROM users;
-- Вградени функции
SELECT abs(-5), sqrt(16), lower('HELLO'), len('test');
```
## Подсказки за Заявки (Query Hints)
```sql
-- Форсиране на индекс
SELECT /*+ USE_INDEX(idx_users_age) */ * FROM users WHERE age > 18;
-- Форсиране на приблизително векторно търсене
SELECT /*+ APPROXIMATE */ * FROM vectors
ORDER BY cosine_distance(embedding, [...])
LIMIT 10;
-- Паралелно изпълнение
SELECT /*+ PARALLEL(4) */ * FROM large_table;
```
## Window Функции
```sql
-- Функции за ранжиране
SELECT
name,
department,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS r,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dr
FROM employees;
-- Стойностни функции
SELECT
name,
salary,
LAG(salary, 1, 0) OVER (ORDER BY salary) AS prev_salary,
LEAD(salary, 1, 0) OVER (ORDER BY salary) AS next_salary,
FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS cheapest,
LAST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS most_expensive
FROM employees;
-- Функции за разпределение
SELECT name, NTILE(4) OVER (ORDER BY salary) AS quartile FROM employees;
```
### Рамкови Спецификации
```sql
-- ROWS рамка
SUM(salary) OVER (
PARTITION BY department
ORDER BY hire_date
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
)
-- RANGE рамка
SUM(salary) OVER (
PARTITION BY department
ORDER BY hire_date
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
```
## Multi-Tenant ERP
BaraDB поддържа множество компании (тенанти) в една инстанция чрез **Row-Level Security (RLS)** и **сесийни променливи**.
### Сесийни Променливи
```sql
SET app.tenant_id = 'company-123';
SELECT current_setting('app.tenant_id') AS tenant;
```
### Текущ Потребител / Роля
```sql
SELECT current_user AS me, current_role AS my_role;
```
### RLS Изолация на Тенанти
```sql
-- Включване на RLS за таблица
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
-- Създаване на политика за филтриране по тенант
CREATE POLICY tenant_isolation ON invoices
FOR SELECT USING (tenant_id = current_setting('app.tenant_id'));
-- Всяка сесия вижда само своите данни
SET app.tenant_id = 'company-a';
SELECT * FROM invoices; -- само редове на company-a
```
### Защо Multi-Tenant?
- **Една инстанция, много тенанти** — няма нужда от 100 отделни бази данни
- **JSONB документи** — гъвкаво съхранение без схема, лесно добавяне на полета за всеки тенант
- **RLS гарантира изолация** — базата данни налага границите между тенанти, не само приложението
## Поддържани Ключови Думи
| Категория | Ключови думи |
|-----------|-------------|
| DQL | SELECT, FROM, WHERE, ORDER BY, GROUP BY, HAVING, LIMIT, OFFSET, DISTINCT |
| DML | INSERT, UPDATE, DELETE, SET, VALUES |
| DDL | CREATE TYPE, DROP TYPE, CREATE INDEX, DROP INDEX, ALTER TYPE |
| Join | INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN, CROSS JOIN, ON |
| Set | UNION, UNION ALL, INTERSECT, EXCEPT |
| CTEs | WITH, RECURSIVE, AS |
| Case | CASE, WHEN, THEN, ELSE, END |
| Транзакции | BEGIN, COMMIT, ROLLBACK, SAVEPOINT |
| Графи | MATCH, RETURN, WHERE, shortestPath, type |
| FTS | MATCH, AGAINST, relevance, IN BOOLEAN MODE, WITH FUZZINESS |
| Вектори | cosine_distance, euclidean_distance, inner_product, l1_distance, l2_distance, <-> |
| JSON | ->, ->> |
| FTS | @@ (BM25 съвпадение) |
| Recovery | RECOVER TO TIMESTAMP |
| Функции | count, sum, avg, min, max, stddev, variance, abs, sqrt, lower, upper, len, trim, substr, now, last_insert_id, current_setting |
| Сесийни | SET, current_setting, current_user, current_role |
| Window | OVER, PARTITION BY, ROWS, RANGE, UNBOUNDED PRECEDING, CURRENT ROW, FOLLOWING |
| Window Функции | ROW_NUMBER, RANK, DENSE_RANK, LEAD, LAG, FIRST_VALUE, LAST_VALUE, NTILE |
+223
View File
@@ -0,0 +1,223 @@
# Списък с Промени (Changelog)
Всички забележителни промени в BaraDB са документирани в този файл.
## [Unreleased] — SQL:2023 Стабилизация
### Поправки
- **GROUPING SETS изпълнение** — `lowerSelect` вече създава `irpkGroupBy` когато `selGroupingSetsKind != gskNone`, дори ако `selGroupBy` е празен. Преди това заявки като `GROUP BY GROUPING SETS ((dept), ())` напълно заобикаляха grouping executor-a.
- **FTS CREATE INDEX docId несъответствие** — `CREATE INDEX ... USING FTS` вече изчислява `docId` като хеш на `tableName.$key`, консистентно с DML операциите (`INSERT`/`UPDATE`/`DELETE`). Преди това създаването на индекс използваше последователни ID-та (0, 1, 2...), което причиняваше `@@` заявките никога да не намират индексирани документи.
- **Тестова изолация (всички сюити)** — Всички извиквания на `newLSMTree("")` са заменени с уникални временни директории за всеки сюит. Елиминира проблеми с натрупване на WAL и нестабилни тестове от споделено състояние между тестове.
- **Window frame parser** — `parseFrameBoundary` вече не консумира `tkRow` след `tkCurrent` неправилно (използваше `tkRows`). Също така е поправен конфликт на ключовата дума `tkRow` с парсването на `ENABLE ROW LEVEL SECURITY`.
- **ORDER BY + SELECT проекция** — `lowerSelect` вече поставя `irpkSort` преди `irpkProject`, което позволява `ORDER BY` по колони, които не присъстват в `SELECT` списъка.
- **UNPIVOT изпълнение** — Проверено и поправено липсващо тестово покритие за UNPIVOT трансформация.
### Добавки
- **JSON оператори** — `@>` (съдържа), `<@` (съдържа се в), `?` (има ключ), `?|` (има някой от), `?&` (има всички) вече се поддържат в lexer, parser и executor.
- **Window frame изпълнение** — `ROWS BETWEEN X PRECEDING AND Y FOLLOWING` / `CURRENT ROW` граници на рамката вече се спазват от `FIRST_VALUE` и `LAST_VALUE`.
- **Сесийни променливи** — `SET var_name = value` и `current_setting('var_name')` за ключ/стойност съхранение на ниво връзка.
- **Текущ потребител/роля** — `current_user` и `current_role` SQL ключови думи връщат потребителя и ролята на автентикираната сесия.
- **Auth-executor мост** — Сървърът и HTTP сървърът вече попълват `ExecutionContext.currentUser` и `ExecutionContext.currentRole` след JWT/SCRAM автентикация.
- **Multi-tenant RLS** — Row-Level Security политиките вече могат да реферират `current_user`, `current_role` и `current_setting('app.tenant_id')` за изолация на данни по тенант.
## [1.1.0] — 2026-05-13
### Добавки
- **Client SDKs v1.1.0** — Пълнофункционални клиенти за всички езици:
- JavaScript: TypeScript дефиниции, package.json, примери, unit и integration тестове
- Python: Преструктуриран като пакет (`baradb/` с `__init__.py` и `core.py`), pyproject.toml, примери, тестове (query builder, wire protocol, integration)
- Nim: Примери, integration тестове, README
- Rust: Примери, integration тестове, подобрен Cargo.toml
- **SCRAM-SHA-256 Автентикация** — RFC 7677 съвместима автентикация с PBKDF2 + HMAC + SHA-256 + nonce/salt генериране
- **HTTP SCRAM Endpoints** — `/auth/scram/start` + `/auth/scram/finish` в HTTP сървъра
- **Docker Compose Тестова Конфигурация** — `docker-compose.test.yml` за тестови среди
- **CI/CD Clients Pipeline** — `.github/workflows/clients-ci.yml` за автоматизирано тестване на клиенти
### Поправки
- **Query Executor** — Унарен минус (`irNeg`) вече работи коректно в SELECT и WHERE клаузи
- **Distributed Transactions** — Rollback след commit опит вече не нарушава атомарността
- **Sharding** — Протокол за миграция на данни с TCP + `scanAll` на LSM
- **Raft** — Поправено изчисление на мнозинство за четен брой нодове
- **MVCC** — Прекъснатите транзакции вече не стават видими
- **LSM-Tree** — Поправена загуба на данни при презаписване на immutable memtable; поправено сортиране на SSTable търсене
- **Auth** — JWT подписът е променен на HMAC-SHA256 (вече не е тривиално forgeable); валидация на токен изтичане (`exp`/`nbf`/`iat`); сравнението на подписи вече е constant-time
- **Recovery** — `summary()` вече не мутира базата данни
- **Wire Protocol** — 64MB лимит + bounds проверки + max дълбочина за предотвратяване на OOM/DoS
- **SQL Injection** — `exprToSql` вече escape-ва единични кавички
- **ReDoS** — `irLike`/`irILike` вече escape-ват regex метасимволи
- **Graph** — `addEdge` вече проверява съществуването на възел
- **Vector** — Валидация на несъответствие на размерности + HNSW заключване
- **FTS** — UTF-8 токенизацията вече използва runes вместо байтове
- **Build** — `nim.cfg` добавя `-d:ssl`, така че `nimble build` работи без флагове; `--threads:on` добавен към всички CI команди
### Промени
- **Версията е вдигната до 1.1.0** във всички компоненти (сървър, Docker изображения, клиенти, CLI)
- **README** — Версионният badge е обновен; всички feature таблици вече реферират v1.1.0
- **TLA+ Формална Верификация** — Добавени `crossmodal.tla`, `backup.tla`, `recovery.tla`; symmetry reduction във всички 9 спецификации
- **Чист build** — 0 компилаторни предупреждения на Nim 2.2.10
## [0.1.0] — 2025-01-15
### Добавки
- **Ядро за Съхранение**
- LSM-Tree с MemTable, WAL, SSTables и size-tiered compaction
- B-Tree подреден индекс с range сканиране и MVCC copy-on-write
- Bloom филтри за ефективно пропускане на SSTable
- Memory-mapped I/O за SSTable четене
- LRU page cache с проследяване на hit rate
- **Query Engine (BaraQL)**
- SQL-съвместим lexer с 80+ типа токени
- Recursive descent parser генериращ AST с 25+ вида възли
- Intermediate representation (IR) за планове за изпълнение
- Code generator превеждащ IR към storage операции
- Adaptive query optimizer с cross-modal планиране
- Query executor с паралелизация
- **BaraQL Езикови Възможности**
- SELECT, INSERT, UPDATE, DELETE
- WHERE, ORDER BY, LIMIT, OFFSET
- GROUP BY, HAVING, агрегатни функции (count, sum, avg, min, max)
- INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN, CROSS JOIN
- CTEs (Common Table Expressions) с WITH
- Подзаявки (EXISTS, IN, корелирани)
- CASE изрази
- UNION, INTERSECT, EXCEPT
- Дефиниране на схема: CREATE TYPE, DROP TYPE
- **Vector Engine**
- HNSW индекс за приблизително търсене на най-близки съседи
- IVF-PQ индекс за мащабно векторно търсене
- SIMD-оптимизирани функции за разстояние (cosine, L2, dot product, Manhattan)
- Квантуване: scalar 8-bit/4-bit, product quantization, binary
- Филтриране по метаданни при векторно търсене
- **Graph Engine**
- Adjacency list съхранение за насочени графи с тегла
- BFS и DFS обхождане
- Dijkstra най-кратък път
- PageRank важност на възли
- Louvain community detection
- Subgraph pattern matching
- Cypher-подобен graph query parser
- **Full-Text Search**
- Inverted index с term-document mapping
- BM25 алгоритъм за ранжиране
- TF-IDF оценяване
- Fuzzy търсене с Levenshtein разстояние
- Wildcard/regex търсене
- Многоезични токенизатори (английски, български, немски, френски, руски)
- **Columnar Storage**
- Колонково съхранение за аналитични заявки
- RLE (Run-Length Encoding) компресия
- Dictionary encoding за колони с ниска кардиналност
- SIMD-ускорени агрегати
- **Транзакции**
- MVCC (Multi-Version Concurrency Control) със snapshot изолация
- Deadlock детекция чрез wait-for граф
- Write-ahead log за устойчивост
- Savepoints и частичен rollback
- **Протоколен Слой**
- Бинарен wire протокол с 16 типа съобщения
- HTTP/REST JSON API
- WebSocket стрийминг
- Connection pooling
- JWT-базирана автентикация
- Token-bucket rate limiting
- TLS/SSL с автоматично генерирани сертификати
- **Система за Схеми**
- Силна типова система с 17 нативни типа
- Наследяване на типове с multi-base поддръжка
- Property links между типове
- Schema diffing и миграции
- Изчислими свойства
- **Разпределени Системи**
- Raft консенсус (leader election, log replication)
- Hash, range и consistent-hash шардиране
- Sync/async/semi-sync репликация
- Gossip протокол за управление на членство
- Two-phase commit за разпределени транзакции
- **Cross-Modal Заявки**
- Унифициран език за заявки през всички storage двигатели
- Cross-engine predicate pushdown
- Оптимизирани планове за изпълнение за multi-modal заявки
- **Backup & Recovery**
- Online snapshots без прекъсване
- Point-in-time recovery чрез WAL replay
- Инкрементални backups
- **Client SDKs**
- JavaScript/TypeScript клиент с бинарен протокол
- Python клиент със sync и async API
- Nim embedded режим и клиентска библиотека
- Rust клиент (async)
- **Операции**
- Интерактивен CLI shell (BaraQL REPL)
- Структурирано логване (JSON и текстови формати)
- Prometheus-съвместим metrics endpoint
- Health и readiness проби
- CPU/memory profiling endpoints
- **Docker Поддръжка**
- Multi-stage Dockerfile (Alpine Linux)
- Docker Compose конфигурация
- Health checks
### Производителност
- LSM-Tree: 580K записа/s, 720K четения/s
- B-Tree: 1.2M вмъквания/s, 1.5M търсения/s
- Vector SIMD: 850K косинусови разстояния/s (dim=768)
- FTS: 320K документи/s индексиране, 28K заявки/s BM25
- Graph: 2.5M възела/s вмъкване, 12K BFS обхождания/s
- Бинарен протокол: 380K заявки/s (100 конкурентни връзки)
### Тестове
- 262 теста в 56 тестови сюита
- 100% успеваемост
## [Unreleased]
### Добавки
- **Vector SQL Integration** — Пълна поддръжка на векторно търсене на SQL ниво:
- `VECTOR(n)` тип колона в `CREATE TABLE` с валидация на размерност
- `CREATE INDEX ... USING hnsw` / `USING ivfpq` за приблизителни nearest neighbor индекси
- SQL функции за разстояние: `cosine_distance()`, `euclidean_distance()`, `inner_product()`, `l1_distance()`, `l2_distance()`
- `<->` nearest-neighbor оператор (евклидово разстояние)
- `ORDER BY` поддръжка за изрази с векторно разстояние, включително колони извън `SELECT`
- Автоматична поддръжка на HNSW индекс при `INSERT` и `UPDATE`
- **Advanced SQL Engine** — Window функции, MERGE/UPSERT, LATERAL JOIN, PIVOT/UNPIVOT, SQL/PGQ Property Graph, Разширени агрегати (ARRAY_AGG, STRING_AGG, FILTER, GROUPING SETS/ROLLUP/CUBE)
- **JavaScript Client — TCP Request Queue** — Вътрешна `_requestQueue` + `_requestLock` за безопасни конкурентни заявки. Множество паралелни извиквания на `query()` / `execute()` / `ping()` вече не размесват бинарни frame-ове по връзката.
### Поправки
- **Query Executor — Ескейпване на Стойности** — `execInsert` вече правилно ескейпва запетаи и знаци за равенство в стойностите на колоните, поправяйки корупция на съхранението за векторни литерали като `[1.0, 2.0, 3.0]`
- **Query Planner — ORDER BY Проекция** — `irpkSort` вече се поставя преди `irpkProject` в IR плана, позволявайки на `ORDER BY` да реферира колони, които не са селектирани
- **Wire Protocol — Big-Endian Float Сериализация** — `FLOAT32`/`FLOAT64` и float стойностите във вектори вече се сериализират в big-endian byte order, съвпадайки с `readFloatBE()` / `readDoubleBE()` на клиента и осигурявайки междуплатформена числова точност.
- **Gossip Protocol — Async UDP Socket** — Заменен синхронният `newSocket` + блокиращ `recvFrom` с `newAsyncSocket` + `await recvFrom`, предотвратявайки замръзване на async event loop-а до пристигане на UDP пакет.
### Планирани
- Query plan caching
- Materialized views
- Геопространствен индекс
- Time-series оптимизации
- CDC (Change Data Capture) стрийминг
- Федеративни заявки между BaraDB инстанции
+285
View File
@@ -0,0 +1,285 @@
# Client SDKs
BaraDB предоставя официални клиентски библиотеки за JavaScript/TypeScript, Python, Nim и Rust.
## JavaScript / TypeScript
### Инсталация
```bash
npm install baradb
# или
yarn add baradb
```
### Основна Употреба
```typescript
import { Client } from 'baradb';
const client = new Client('localhost', 9472);
await client.connect();
// Проста заявка
const result = await client.query('SELECT name, age FROM users WHERE age > 18');
console.log(result.rows);
// Параметризирана заявка
const result2 = await client.query(
'SELECT * FROM users WHERE name = ?',
['Alice']
);
// Batch вмъкване
await client.batch([
"INSERT users { name := 'Alice', age := 30 }",
"INSERT users { name := 'Bob', age := 25 }",
]);
// Транзакции
await client.begin();
await client.query("INSERT orders { total := 100 }");
await client.query("UPDATE users SET balance = balance - 100 WHERE name = 'Alice'");
await client.commit();
await client.close();
```
### Конкурентни Заявки
JavaScript клиентът автоматично сериализира конкурентни заявки през една TCP връзка чрез вътрешна опашка. Можете безопасно да изпращате множество паралелни операции — техните бинарни frame-ове няма да се размесят:
```typescript
const [users, orders, stats] = await Promise.all([
client.query('SELECT * FROM users'),
client.query('SELECT * FROM orders'),
client.query('SELECT count(*) FROM visits')
]);
```
### WebSocket Стрийминг
```typescript
import { WebSocketClient } from 'baradb/ws';
const ws = new WebSocketClient('ws://localhost:9471');
ws.onMessage = (data) => console.log(data);
await ws.connect();
await ws.send('SUBSCRIBE updates');
```
## Python
### Инсталация
```bash
pip install baradb
```
### Основна Употреба
```python
from baradb import Client
client = Client("localhost", 9472)
client.connect()
# Проста заявка
result = client.query("SELECT name, age FROM users WHERE age > 18")
for row in result:
print(row["name"], row["age"])
# Параметризирана заявка
result = client.query(
"SELECT * FROM users WHERE name = ?",
["Alice"]
)
# Batch операции
client.batch([
"INSERT users { name := 'Alice', age := 30 }",
"INSERT users { name := 'Bob', age := 25 }",
])
# Context manager (автоматично затваряне)
with Client("localhost", 9472) as c:
result = c.query("SELECT count(*) FROM users")
print(result[0]["count"])
```
### Async Клиент
```python
import asyncio
from baradb import AsyncClient
async def main():
client = AsyncClient("localhost", 9472)
await client.connect()
result = await client.query("SELECT * FROM users")
print(result.rows)
await client.close()
asyncio.run(main())
```
## Nim (Вграден Режим)
### Добавяне на Зависимост
```nim
# Във вашия .nimble файл
requires "barabadb >= 1.1.0"
```
### Вградена Употреба
```nim
import barabadb/storage/lsm
import barabadb/storage/btree
import barabadb/vector/engine
import barabadb/graph/engine
# Key-Value store
var db = newLSMTree("./data")
db.put("user:1", cast[seq[byte]]("Alice"))
let (found, value) = db.get("user:1")
db.close()
# B-Tree индекс
var btree = newBTreeIndex[string, int]()
btree.insert("Alice", 30)
let ages = btree.get("Alice")
# Векторно търсене
var idx = newHNSWIndex(dimensions = 128)
idx.insert(1, @[0.1'f32, 0.2, 0.3], {"category": "A"}.toTable)
let results = idx.search(@[0.1'f32, 0.2, 0.3], k = 10)
# Графи
var g = newGraph()
let alice = g.addNode("Person", {"name": "Alice"}.toTable)
let bob = g.addNode("Person", {"name": "Bob"}.toTable)
discard g.addEdge(alice, bob, "knows")
let path = g.shortestPath(alice, bob)
```
### Клиентска Библиотека
```nim
import barabadb/client/client
var c = newBaraClient("localhost", 9472)
c.connect()
let result = c.query("SELECT name FROM users")
for row in result.rows:
echo row["name"]
c.close()
```
## Rust
### Добавяне на Зависимост
```toml
[dependencies]
baradb = "0.1"
tokio = { version = "1", features = ["full"] }
```
### Основна Употреба
```rust
use baradb::Client;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::connect("localhost:9472").await?;
let result = client
.query("SELECT name, age FROM users WHERE age > 18")
.await?;
for row in result.rows {
println!("{} is {} years old", row["name"], row["age"]);
}
client.close().await?;
Ok(())
}
```
## HTTP/REST (Езиково Независим)
Всички езици могат да използват HTTP/REST API директно:
```bash
# Заявка
curl -X POST http://localhost:9470/api/query \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query": "SELECT * FROM users WHERE age > 18"}'
# Вмъкване
curl -X POST http://localhost:9470/api/query \
-H "Content-Type: application/json" \
-d '{"query": "INSERT users { name := \"Alice\", age := 30 }"}'
# Схема
curl http://localhost:9470/api/schema
# Health
curl http://localhost:9470/health
# Метрики
curl http://localhost:9470/metrics
```
## Connection Pooling
Всички официални клиенти поддържат connection pooling:
### JavaScript
```typescript
import { Pool } from 'baradb';
const pool = new Pool({
host: 'localhost',
port: 9472,
min: 5,
max: 50,
idleTimeout: 30000,
});
const client = await pool.acquire();
try {
const result = await client.query('SELECT 1');
} finally {
pool.release(client);
}
```
### Python
```python
from baradb import Pool
pool = Pool("localhost", 9472, min_size=5, max_size=50)
with pool.connection() as conn:
result = conn.query("SELECT 1")
```
## Съответствие на Типове Данни
| BaraDB Тип | JavaScript | Python | Nim | Rust |
|------------|------------|--------|-----|------|
| `null` | `null` | `None` | `nil` | `Option::None` |
| `bool` | `boolean` | `bool` | `bool` | `bool` |
| `int8/16/32/64` | `number` | `int` | `int` | `i8/i16/i32/i64` |
| `float32/64` | `number` | `float` | `float32/float64` | `f32/f64` |
| `str` | `string` | `str` | `string` | `String` |
| `bytes` | `Uint8Array` | `bytes` | `seq[byte]` | `Vec<u8>` |
| `array` | `Array` | `list` | `seq` | `Vec` |
| `object` | `Object` | `dict` | `Table` | `HashMap` |
| `vector` | `Float32Array` | `list[float]` | `seq[float32]` | `Vec<f32>` |
+58
View File
@@ -0,0 +1,58 @@
# Колонково Съхранение (Columnar)
Колонково-ориентирано съхранение за аналитични заявки и агрегации.
## Употреба
```nim
import barabadb/core/columnar
var batch = newColumnBatch()
var ageCol = batch.addInt64Col("age")
var nameCol = batch.addStringCol("name")
ageCol.appendInt64(25)
nameCol.appendString("Alice")
```
## Агрегации
```nim
echo ageCol.sumInt64()
echo ageCol.avgInt64()
echo ageCol.minInt64()
echo ageCol.maxInt64()
echo ageCol.count()
```
## Кодиране
### RLE (Run-Length Encoding)
```nim
let rle = rleEncode(@[1'i64, 1, 1, 2, 2, 3])
```
### Dictionary Encoding
```nim
let dict = dictEncode(@["apple", "banana", "apple"])
```
## Типове Колони
| Тип | Описание |
|------|----------|
| `int32` | 32-битов integer |
| `int64` | 64-битов integer |
| `float32` | 32-битов float |
| `float64` | 64-битов float |
| `string` | Низ с променлива дължина |
| `bool` | Булев |
## Случаи на Употреба
- OLAP натоварвания
- Мащабни агрегации
- Data warehousing
- Анализ на времеви редове
+218
View File
@@ -0,0 +1,218 @@
# Конфигурационна Референция
BaraDB може да се конфигурира чрез **променливи на средата**, **конфигурационен файл** или **командно-редови флагове**.
## Ред на Приоритет
1. Командно-редови флагове (най-висок приоритет)
2. Променливи на средата
3. Конфигурационен файл (`baradb.conf` или `baradb.json`)
4. Вградени стойности по подразбиране (най-нисък приоритет)
## Променливи на Средата
### Мрежа
| Променлива | По подр. | Описание |
|------------|----------|----------|
| `BARADB_ADDRESS` | `127.0.0.1` | Адрес за свързване |
| `BARADB_PORT` | `9472` | TCP бинарен протокол порт |
| `BARADB_HTTP_PORT` | `9470` | HTTP/REST API порт |
| `BARADB_WS_PORT` | `9471` | WebSocket порт |
### Съхранение
| Променлива | По подр. | Описание |
|------------|----------|----------|
| `BARADB_DATA_DIR` | `./data` | Път до директория за данни |
| `BARADB_MEMTABLE_SIZE_MB` | `64` | Размер на MemTable в MB |
| `BARADB_CACHE_SIZE_MB` | `256` | Размер на page cache в MB |
| `BARADB_WAL_SYNC_INTERVAL_MS` | `0` | Интервал за WAL fsync (0 = всеки запис) |
| `BARADB_COMPACTION_INTERVAL_MS` | `60000` | Интервал за фонов compaction |
| `BARADB_BLOOM_BITS_PER_KEY` | `10` | Bloom филтър битове за ключ |
### TLS/SSL
| Променлива | По подр. | Описание |
|------------|----------|----------|
| `BARADB_TLS_ENABLED` | `false` | Включване на TLS |
| `BARADB_CERT_FILE` | — | Път до TLS сертификат |
| `BARADB_KEY_FILE` | — | Път до TLS частен ключ |
### Сигурност
| Променлива | По подр. | Описание |
|------------|----------|----------|
| `BARADB_AUTH_ENABLED` | `false` | Включване на автентикация |
| `BARADB_JWT_SECRET` | — | JWT подписващ secret |
| `BARADB_RATE_LIMIT_GLOBAL` | `10000` | Глобални заявки в секунда |
| `BARADB_RATE_LIMIT_PER_CLIENT` | `1000` | Заявки в секунда за клиент |
### Логване
| Променлива | По подр. | Описание |
|------------|----------|----------|
| `BARADB_LOG_LEVEL` | `info` | Ниво на логване: debug, info, warn, error |
| `BARADB_LOG_FILE` | — | Път до лог файл (stdout ако е празен) |
| `BARADB_LOG_FORMAT` | `json` | Формат на лога: json, text |
### Vector Engine
| Променлива | По подр. | Описание |
|------------|----------|----------|
| `BARADB_VECTOR_M` | `16` | HNSW `M` параметър |
| `BARADB_VECTOR_EF_CONSTRUCTION` | `200` | HNSW `efConstruction` |
| `BARADB_VECTOR_EF_SEARCH` | `64` | HNSW `efSearch` |
### Graph Engine
| Променлива | По подр. | Описание |
|------------|----------|----------|
| `BARADB_GRAPH_PAGE_RANK_ITERATIONS` | `20` | Брой итерации на PageRank |
| `BARADB_GRAPH_PAGE_RANK_DAMPING` | `0.85` | PageRank damping фактор |
| `BARADB_GRAPH_LOUVAIN_RESOLUTION` | `1.0` | Louvain резолюционен параметър |
### Разпределени
| Променлива | По подр. | Описание |
|------------|----------|----------|
| `BARADB_RAFT_NODE_ID` | — | Уникално ID на възел в клъстер |
| `BARADB_RAFT_PEERS` | — | Списък с адреси на peer-ове, разделени със запетая |
| `BARADB_RAFT_PORT` | `9001` | Raft вътрешен комуникационен порт |
| `BARADB_SHARD_COUNT` | `1` | Брой шардове |
| `BARADB_REPLICATION_FACTOR` | `1` | Фактор на репликация |
| `BARADB_SEED_NODES` | — | Gossip seed възли (host:port, разделени със запетая) |
## Конфигурационен Файл
### baradb.conf (INI-подобен)
```ini
[server]
address = "0.0.0.0"
port = 9472
http_port = 9470
ws_port = 9471
[storage]
data_dir = "/var/lib/baradb"
memtable_size_mb = 256
cache_size_mb = 512
wal_sync_interval_ms = 10
compaction_interval_ms = 30000
[tls]
enabled = true
cert_file = "/etc/baradb/server.crt"
key_file = "/etc/baradb/server.key"
[auth]
enabled = true
jwt_secret = "change-me-in-production"
rate_limit_global = 10000
rate_limit_per_client = 1000
[logging]
level = "info"
format = "json"
file = "/var/log/baradb/baradb.log"
[vector]
m = 16
ef_construction = 200
ef_search = 64
[cluster]
raft_node_id = "node1"
raft_peers = "node2:9001,node3:9001"
```
### baradb.json
```json
{
"server": {
"address": "0.0.0.0",
"port": 9472,
"http_port": 9470,
"ws_port": 9471
},
"storage": {
"data_dir": "/var/lib/baradb",
"memtable_size_mb": 256,
"cache_size_mb": 512
},
"tls": {
"enabled": true,
"cert_file": "/etc/baradb/server.crt",
"key_file": "/etc/baradb/server.key"
}
}
```
## Командно-редови Флагове
```bash
./build/baradadb --help
```
```
BaraDB v1.1.0 — Multimodal Database Engine
Употреба:
baradadb [опции]
Опции:
-c, --config <файл> Път до конфигурационен файл
-p, --port <порт> TCP бинарен порт (по подр.: 9472)
--http-port <порт> HTTP порт (по подр.: 9470)
--ws-port <порт> WebSocket порт (по подр.: 9471)
-d, --data-dir <дир> Директория за данни (по подр.: ./data)
--tls-cert <файл> TLS сертификатен файл
--tls-key <файл> TLS файл с частен ключ
--log-level <ниво> Ниво на логване: debug, info, warn, error
--log-file <файл> Път до лог файл
--shell Стартиране на интерактивна обвивка
--version Показване на версия
--recover Изпълнение на WAL възстановяване
--checkpoint <файл> Checkpoint за възстановяване
-h, --help Показване на тази помощ
```
## Примерни Конфигурации
### Разработка
```bash
./build/baradadb \
--log-level debug \
--data-dir ./dev_data
```
### Продукционен Единичен Възел
```bash
BARADB_TLS_ENABLED=true \
BARADB_CERT_FILE=/etc/baradb/server.crt \
BARADB_KEY_FILE=/etc/baradb/server.key \
BARADB_AUTH_ENABLED=true \
BARADB_JWT_SECRET="$(openssl rand -hex 32)" \
BARADB_LOG_LEVEL=warn \
BARADB_LOG_FILE=/var/log/baradb/baradb.log \
BARADB_MEMTABLE_SIZE_MB=256 \
BARADB_CACHE_SIZE_MB=1024 \
./build/baradadb
```
### Продукционен Клъстер (3 възела)
```bash
# Възел 1
BARADB_ADDRESS=0.0.0.0 \
BARADB_PORT=9472 \
BARADB_RAFT_NODE_ID=node1 \
BARADB_RAFT_PEERS=node2:9001,node3:9001 \
BARADB_SHARD_COUNT=4 \
BARADB_REPLICATION_FACTOR=2 \
./build/baradadb
```
+101
View File
@@ -0,0 +1,101 @@
# Cross-Modal Заявки
Уникалната способност на BaraDB да изпълнява заявки, обхващащи множество storage двигатели в една унифицирана BaraQL заявка.
## Преглед
Традиционните бази данни изискват отделни заявки и join-ове на ниво приложение при работа с различни модели на данни. Cross-modal query planner-ът на BaraDB оптимизира изпълнението през:
- **Документи/KV** (LSM-Tree) — структурирани записи
- **Графи** (Adjacency List) — връзки
- **Вектори** (HNSW/IVF-PQ) — търсене на прилика
- **Пълен текст** (Inverted Index) — текстово търсене
- **Колонково** — аналитични агрегати
## Примери за Заявки
### Векторно + Пълнотекстово (Семантично + Ключово Търсене)
```sql
SELECT title, score
FROM articles
WHERE MATCH(body) AGAINST('machine learning')
ORDER BY cosine_distance(embedding, [0.1, 0.2, 0.3, ...])
LIMIT 10;
```
### Графово + Векторно (Социални Препоръки)
```sql
MATCH (u:User)-[:KNOWS]->(friend:User)
WHERE u.name = 'Alice'
ORDER BY cosine_distance(friend.taste_vector, u.taste_vector)
RETURN friend.name, friend.age;
```
### Документно + Графово (Обогатяване на Същности)
```sql
SELECT o.id, o.total, c.name,
(SELECT count(*) FROM orders WHERE customer_id = c.id) as order_count
FROM orders o
MATCH (c:Customer)-[:PLACED]->(o)
WHERE o.date > '2025-01-01';
```
## Cross-Modal Engine API
```nim
import barabadb/core/crossmodal
var engine = newCrossModalEngine("/tmp/baradb")
# Документни операции
engine.put("key1", cast[seq[byte]]("value1"))
let (found, val) = engine.get("key1")
# Векторни операции
engine.insertVector(1, @[1.0'f32, 0.0'f32, ...], {"cat": "A"}.toTable)
let results = engine.searchVector(@[1.0'f32, 0.1'f32, ...], 2)
# Графови операции
let n1 = engine.addNode("Person")
let n2 = engine.addNode("Person")
discard engine.addEdge(n1, n2, "knows")
let traversal = engine.traverseGraph(n1, "bfs")
# FTS операции
engine.indexText(1, "Nim programming language")
let ftsResults = engine.searchText("programming")
# Хибридно търсене
var query = newCrossModalQuery(qmHybrid)
query.vector = @[1.0'f32, 0.0'f32]
query.searchQuery = "fast"
query.vecWeight = 1.0
query.ftsWeight = 1.0
let hybridResult = engine.hybridSearch(query)
```
## 2PC Транзакции
Cross-modal engine-ът поддържа two-phase commit за атомарни операции през множество storage системи:
```nim
var txn = newTPCTransaction(1)
txn.addParticipant("storage")
txn.addParticipant("vector")
txn.addParticipant("graph")
txn.prepare() # Всички участници потвърждават, че могат да комитнат
txn.commit() # Атомарен commit през всички участници
```
## Формална Верификация
Cross-modal консистентността е формално специфицирана в TLA+:
- **Спецификация:** `formal-verification/crossmodal.tla`
- **Проверени свойства:**
- `MetadataVectorConsistency` — insertVector обновява метаданни за филтрирано търсене
- `CrossModalAtomicity` — всички участници комитват или всички абортират
+250
View File
@@ -0,0 +1,250 @@
# Ръководство за Внедряване (Deployment)
## Docker
За пълно ръководство за Docker deployment вижте [Docker Guide](docker.md).
### Бърз старт
```bash
docker build -t baradb:latest .
docker compose up -d
```
### Docker Compose файлове
| Файл | Назначение |
|------|-----------|
| `docker-compose.yml` | Development |
| `docker-compose.prod.yml` | Production |
| `docker-compose.override.yml` | Dev override (автоматично) |
| `docker-compose.test.yml` | Тестова среда |
### Production
```bash
docker compose -f docker-compose.prod.yml up -d
```
### Docker Swarm
```bash
docker stack deploy -c docker-compose.prod.yml baradb
```
## systemd Услуга
Създайте `/etc/systemd/system/baradb.service`:
```ini
[Unit]
Description=BaraDB Multimodal Database
After=network.target
[Service]
Type=simple
User=baradb
Group=baradb
WorkingDirectory=/var/lib/baradb
ExecStart=/usr/local/bin/baradadb
Restart=always
RestartSec=5
Environment=BARADB_PORT=9472
Environment=BARADB_HTTP_PORT=9470
Environment=BARADB_DATA_DIR=/var/lib/baradb/data
Environment=BARADB_LOG_LEVEL=info
# Подсилване на сигурността
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/baradb/data
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
[Install]
WantedBy=multi-user.target
```
Активиране и стартиране:
```bash
sudo useradd -r -s /bin/false baradb
sudo mkdir -p /var/lib/baradb/data
sudo chown -R baradb:baradb /var/lib/baradb
sudo cp build/baradadb /usr/local/bin/
sudo systemctl daemon-reload
sudo systemctl enable --now baradb
```
## Kubernetes
### StatefulSet
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: baradb
spec:
serviceName: baradb
replicas: 3
selector:
matchLabels:
app: baradb
template:
metadata:
labels:
app: baradb
spec:
containers:
- name: baradb
image: baradb:latest
ports:
- containerPort: 9472
name: binary
- containerPort: 9470
name: http
- containerPort: 9471
name: websocket
env:
- name: BARADB_DATA_DIR
value: /data
- name: BARADB_RAFT_NODE_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
---
apiVersion: v1
kind: Service
metadata:
name: baradb
spec:
selector:
app: baradb
ports:
- port: 9472
name: binary
- port: 9470
name: http
- port: 9471
name: websocket
clusterIP: None
```
## Reverse Proxy (nginx)
```nginx
upstream baradb_http {
server 127.0.0.1:9470;
}
upstream baradb_ws {
server 127.0.0.1:9471;
}
server {
listen 80;
server_name db.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name db.example.com;
ssl_certificate /etc/letsencrypt/live/db.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/db.example.com/privkey.pem;
location /api/ {
proxy_pass http://baradb_http/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /ws/ {
proxy_pass http://baradb_ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
```
## Висока Достъпност (High Availability)
### 3-Възел Raft Клъстер
```bash
# Възел 1
BARADB_RAFT_NODE_ID=node1 \
BARADB_RAFT_PEERS=node2:9001,node3:9001 \
./build/baradadb
# Възел 2
BARADB_RAFT_NODE_ID=node2 \
BARADB_RAFT_PEERS=node1:9001,node3:9001 \
./build/baradadb
# Възел 3
BARADB_RAFT_NODE_ID=node3 \
BARADB_RAFT_PEERS=node1:9001,node2:9001 \
./build/baradadb
```
## Облачно Внедряване
### AWS EC2
Препоръчителна инстанция: `m6i.2xlarge` (8 vCPU, 32 GB RAM)
```bash
# User data скрипт
#!/bin/bash
apt-get update
apt-get install -y nim
wget https://github.com/katehonz/barabaDB/releases/latest/download/baradadb-linux-amd64
chmod +x baradadb-linux-amd64
mv baradadb-linux-amd64 /usr/local/bin/baradadb
mkdir -p /data/baradb
cat > /etc/systemd/system/baradb.service << 'EOF'
[Unit]
Description=BaraDB
After=network.target
[Service]
ExecStart=/usr/local/bin/baradadb
Environment=BARADB_DATA_DIR=/data/baradb
Restart=always
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now baradb
```
### GCP Cloud Run (само HTTP)
```bash
gcloud run deploy baradb \
--image gcr.io/PROJECT/baradb \
--port 9470 \
--memory 4Gi \
--cpu 2 \
--max-instances 10
```
+58 -5
View File
@@ -1,9 +1,11 @@
# Разпределена Система
Поддръжка за разпределено внедряване с Raft консенсус, шардиране и репликация.
BaraDB поддържа разпределено внедряване с Raft консенсус, шардиране, репликация и gossip протокол.
## Raft Консенсус
Leader election и log репликация:
```nim
import barabadb/core/raft
@@ -13,19 +15,35 @@ cluster.addNode("node2")
cluster.addNode("node3")
let n1 = cluster.nodes["n1"]
n1.becomeCandidate()
n1.becomeLeader()
let entry = n1.appendLog("SET key1 value1")
```
## Шардиране
Разпределение на данни между възли:
```nim
import barabadb/core/sharding
var router = newShardRouter(ShardConfig(numShards: 4, replicas: 2))
var router = newShardRouter(ShardConfig(
numShards: 4,
replicas: 2,
strategy: ssHash
))
router.rebalance(@["node1", "node2", "node3"])
let shard = router.getShard("user_123")
```
### Стратегии за Шардиране
| Стратегия | Описание |
|-----------|----------|
| `ssHash` | Хеш-базирано шардиране |
| `ssRange` | Range-базирано шардиране |
| `ssConsistent` | Consistent hashing |
## Репликация
```nim
@@ -34,11 +52,46 @@ import barabadb/core/replication
var rm = newReplicationManager(rmSync)
rm.addReplica(newReplica("r1", "10.0.0.1", 9472))
rm.connectReplica("r1")
let lsn = rm.writeLsn(@[1'u8, 2, 3])
rm.ackLsn("r1", lsn)
```
### Режими на Репликация
| Режим | Описание |
|--------|----------|
| `rmSync` | Синхронна репликация |
| `rmAsync` | Асинхронна репликация |
| `rmSemiSync` | Полу-синхронна репликация |
## Gossip Протокол
Управление на членство и детекция на откази:
```nim
import barabadb/core/gossip
var g = newGossipProtocol("node1", "localhost", 9472, gossipPort = 9572)
g.join(newGossipNode("node2", "10.0.0.2", 9472))
```
## Разпределени Транзакции
Two-phase commit между възли:
```nim
import barabadb/core/disttxn
var tm = newDistTxnManager()
let txn = tm.beginTransaction("node1")
txn.addParticipant("node2", "10.0.0.2", 9472)
txn.prepare()
txn.commit()
```
## Формална Верификация
Разпределените алгоритми са формално специфицирани в TLA+ и проверени с TLC:
Основните разпределени алгоритми са формално специфицирани в TLA+ и проверени с TLC:
- **Raft Консенсус** — `formal-verification/raft.tla`
- Проверено: ElectionSafety, StateMachineSafety
@@ -47,11 +100,11 @@ rm.connectReplica("r1")
- **Репликация** — `formal-verification/replication.tla`
- Проверено: MonotonicLsn, AcksRemovePending
Пускане на TLC:
Пускане на TLC локално:
```bash
cd formal-verification
java -cp tla2tools.jar tlc2.TLC -config models/raft.cfg raft.tla
java -cp tla2tools.jar tlc2.TLC -config models/twopc.cfg twopc.tla
java -cp tla2tools.jar tlc2.TLC -config models/replication.cfg replication.tla
```
```
+183
View File
@@ -0,0 +1,183 @@
# Docker Deployment Ръководство
Това ръководство описва как да използвате BaraDB с Docker и Docker Compose.
## Бърз старт
```bash
# Клониране на репото
git clone https://codeberg.org/baraba/baradb
cd barabaDB
# Build на образа
docker build -t baradb:latest .
# Стартиране с Docker Compose
docker compose up -d
# Проверка на статуса
docker compose ps
docker compose logs -f
```
## Файлове
| Файл | Описание |
|------|----------|
| `Dockerfile` | Мулти-stage production build |
| `docker-compose.yml` | Development конфигурация |
| `docker-compose.prod.yml` | Production конфигурация |
| `docker-compose.override.yml` | Development override (автоматично се зарежда) |
| `docker-compose.test.yml` | Тестова конфигурация |
| `docker-entrypoint.sh` | Entrypoint скрипт за инициализация |
| `.dockerignore` | Файлове, които да не се копират в образа |
## Създаване на образ
```bash
# Стандартен build
docker build -t baradb:latest .
# С конкретна версия
docker build -t baradb:1.1.0 .
```
## Стартиране
### Development (docker compose)
```bash
# Стартиране на заден план
docker compose up -d
# Спиране
docker compose down
# Спиране и изтриване на volumes (ВНИМАНИЕ — изтрива данните!)
docker compose down -v
# Преглед на логове
docker compose logs -f
```
### Production (docker compose)
```bash
# Стартиране с production конфигурация
docker compose -f docker-compose.prod.yml up -d
# Проверка на healthcheck
docker compose -f docker-compose.prod.yml ps
```
### Ръчно (docker run)
```bash
docker run -d \
--name baradb \
-p 9472:9472 \
-p 9470:9470 \
-p 9471:9471 \
-v baradb_data:/data \
-e BARADB_LOG_LEVEL=info \
baradb:latest
```
## Портове
| Порт | Описание |
|------|----------|
| `9472` | Binary wire протокол |
| `9470` | HTTP/REST API |
| `9471` | WebSocket |
## Променливи на Средата
| Променлива | Стойност по подразбиране | Описание |
|------------|--------------------------|----------|
| `BARADB_ADDRESS` | `0.0.0.0` | Адрес за слушане |
| `BARADB_PORT` | `9472` | Binary протокол порт |
| `BARADB_HTTP_PORT` | `9470` | HTTP порт |
| `BARADB_WS_PORT` | `9471` | WebSocket порт |
| `BARADB_DATA_DIR` | `/data` | Директория за данни |
| `BARADB_LOG_LEVEL` | `info` | Ниво на логове |
## Volumes
| Път в контейнера | Описание |
|------------------|----------|
| `/data` | Основна директория за базата данни |
| `/data/server/wal` | Write-ahead log |
| `/data/server/sstables` | SSTable файлове |
## Production Checklist
- [ ] Създайте TLS сертификати в `./certs/`
- [ ] Задайте силен `BARADB_JWT_SECRET`
- [ ] Настройте firewall правила
- [ ] Конфигурирайте регулярни backups
- [ ] Проверете resource limits
- [ ] Настройте мониторинг (healthcheck, logs)
## TLS в Docker
1. Създайте сертификати:
```bash
mkdir -p certs
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout certs/server.key -out certs/server.crt
```
2. Активирайте в `docker-compose.prod.yml`:
```yaml
environment:
- BARADB_TLS_ENABLED=true
- BARADB_CERT_FILE=/certs/server.crt
- BARADB_KEY_FILE=/certs/server.key
volumes:
- ./certs:/certs:ro
```
## Backup в Docker
```bash
# Ръчен backup
docker exec baradb baradadb --snapshot --output=/backup/snapshot.db
# Списък на backups
ls /backup/
# Възстановяване
docker exec baradb baradadb --recover --checkpoint=/backup/snapshot.db
```
## Troubleshooting
### Контейнерът не стартира
```bash
# Проверка на логове
docker compose logs -f baradb
# Проверка на статус
docker compose ps
```
### Няма връзка с базата
```bash
# Проверка дали портовете са експозвани
docker port baradb
# Проверка отвътре
docker exec baradb wget -qO- http://localhost:9470/health
```
### Permission denied на /data
Entrypoint скриптът автоматично създава директориите и задава правилните permissions. Ако имате проблем:
```bash
docker exec baradb ls -la /data
docker exec baradb chown -R baradb:baradb /data
```
+67 -15
View File
@@ -1,6 +1,6 @@
# Пълнотекстово Търсене
# Full-Text Search Engine
Инвертиран индекс с BM25 и TF-IDF ранжиране.
Inverted индекс с BM25 и TF-IDF ранжиране за текстово търсене.
## Употреба
@@ -8,28 +8,80 @@
import barabadb/fts/engine
var idx = newInvertedIndex()
idx.addDocument(1, "Nim е бърз език за програмиране")
idx.addDocument(2, "Python е популярен за data science")
idx.addDocument(1, "Nim is a fast programming language")
idx.addDocument(2, "Python is popular for data science")
let results = idx.search("език програмиране")
let tfidf = idx.searchTfidf("език")
let fuzzy = idx.fuzzySearch("програмиране", maxDistance = 2)
# BM25 търсене
let results = idx.search("programming language")
# TF-IDF търсене
let tfidf = idx.searchTfidf("programming language")
# Fuzzy търсене (толеранс на печатни грешки)
let fuzzy = idx.fuzzySearch("programing", maxDistance = 2)
# Wildcard търсене
let wild = idx.regexSearch("prog*")
```
## Методи за Ранжиране
### BM25
Най-добрият алгоритъм за съвпадение
Best matching алгоритъм за ранжиране:
```nim
let bm25 = idx.searchBM25("query terms")
```
### TF-IDF
Term Frequency-Inverse Document Frequency
Term Frequency-Inverse Document Frequency:
## Търсене
```nim
let tfidf = idx.searchTfidf("query terms")
```
| Тип | Описание |
|-----|----------|
| Fuzzy | Толерантност към правописни грешки |
| Wildcard | Префикс, суфикс, и инфикс заместващи символи |
| Regex | Регулярни изрази |
## Функции за Търсене
| Функция | Описание |
|---------|----------|
| Fuzzy търсене | Levenshtein distance толеранс |
| Wildcard | Префиксни, суфиксни и инфиксни wildcards |
| Regex | Регулярни изрази |
| Фразово търсене | Точно съвпадение на фраза |
| Булево | AND, OR, NOT оператори |
## SQL Интерфейс
Пълнотекстовото търсене е достъпно и директно в BaraQL:
```sql
-- Създаване на таблица с текстова колона
CREATE TABLE articles (id INT PRIMARY KEY, title TEXT, body TEXT);
-- Създаване на FTS индекс
CREATE INDEX idx_fts ON articles(body) USING FTS;
-- Търсене с оператора @@ (BM25 ранжиране)
SELECT * FROM articles WHERE body @@ 'machine learning';
-- Търсене с множество термини
SELECT * FROM articles WHERE body @@ 'quick brown fox';
```
## Многоезична Поддръжка
```nim
import barabadb/fts/multilang
# Поддържани езици: EN, BG, DE, FR, RU
var tokenizer = newTokenizer("bg") # Български
let tokens = tokenizer.tokenize("Търсене в пълен текст")
```
Функции за всеки език:
- Токенизация
- Stop думи
- Стеминг
- Детекция на език
+118 -21
View File
@@ -15,6 +15,7 @@
| macOS | x86_64 | ✅ Пълна поддръжка |
| macOS | ARM64 (Apple Silicon) | ✅ Пълна поддръжка |
| Windows | x86_64 | ✅ Поддръжка |
| FreeBSD | x86_64 | 🟡 Тествано от общността |
## Инсталиране на Nim
@@ -48,10 +49,13 @@ sudo port install nim
### Windows
```powershell
# Winget
# С choosenim
curl.exe -A "MSYS2_$(uname -m)" -L https://nim-lang.org/choosenim/init.ps1 | powershell -
# С winget
winget install nim
# Scoop
# С scoop
scoop install nim
```
@@ -79,16 +83,22 @@ sudo pacman -S openssl
### macOS
OpenSSL е включен в системата. Ако е необходимо:
```bash
brew install openssl
```
### Windows
OpenSSL е включен в Nim Windows дистрибуцията. За ръчни компилации, изтеглете от [slproweb.com](https://slproweb.com/products/Win32OpenSSL.html).
## Компилиране на BaraDB
### Клониране на Репозиторито
```bash
git clone https://github.com/katehonz/barabaDB.git
git clone https://codeberg.org/baraba/baradb
cd barabaDB
```
@@ -103,26 +113,29 @@ nimble install -d -y
#### Debug Компилация
```bash
nim c -d:ssl -o:build/baradadb src/baradadb.nim
nim c -d:ssl --threads:on -o:build/baradadb src/baradadb.nim
```
#### Release Компилация (Препоръчителна)
```bash
nim c -d:ssl -d:release --opt:speed -o:build/baradadb src/baradadb.nim
nim c -d:ssl --threads:on -d:release --opt:speed -o:build/baradadb src/baradadb.nim
```
#### Използване на Nimble
#### Използване на Nimble Tasks
```bash
# Debug компилация
nimble build_debug
# Release компилация
nimble build_release
```
#### Минимален Размер
```bash
nim c -d:ssl -d:release --opt:size -o:build/baradadb src/baradadb.nim
nim c -d:ssl --threads:on -d:release --opt:size -o:build/baradadb src/baradadb.nim
strip build/baradadb
```
@@ -130,17 +143,31 @@ strip build/baradadb
```bash
./build/baradadb --version
# Очакван резултат: BaraDB v0.1.0 — Multimodal Database Engine
# Очакван резултат: BaraDB v1.1.0 — Multimodal Database Engine
```
## Стартиране на Тестове
```bash
# Всички тестове (262 теста, 56 сюита)
nim c -d:ssl -r tests/test_all.nim
### Всички Тестове
# Бенчмаркове
nim c -d:ssl -d:release -r benchmarks/bench_all.nim
```bash
nim c --path:src -d:ssl --threads:on -r tests/test_all.nim
```
### Специфични Тестови Сюити
```bash
# Storage тестове
nim c --path:src -d:ssl --threads:on -r tests/test_all.nim
# Stress тестове
nim c --path:src -d:ssl --threads:on -d:release -r tests/stress_test.nim
```
### Бенчмаркове
```bash
nim c --path:src -d:ssl --threads:on -d:release -r benchmarks/bench_all.nim
```
## Опции за Инсталация
@@ -148,16 +175,46 @@ nim c -d:ssl -d:release -r benchmarks/bench_all.nim
### Системна Инсталация
```bash
# Компилиране на release binary
nimble build_release
# Инсталиране в /usr/local/bin
sudo cp build/baradadb /usr/local/bin/
sudo chmod +x /usr/local/bin/baradadb
# Създаване на директория за данни
sudo mkdir -p /var/lib/baradb
sudo chmod 755 /var/lib/baradb
```
### Предварително Компилиран Binary
Изтеглете най-новата версия за вашата платформа:
```bash
# Linux x86_64
wget https://github.com/katehonz/barabaDB/releases/latest/download/baradadb-linux-amd64
chmod +x baradadb-linux-amd64
mv baradadb-linux-amd64 /usr/local/bin/baradadb
# Linux ARM64
wget https://github.com/katehonz/barabaDB/releases/latest/download/baradadb-linux-arm64
chmod +x baradadb-linux-arm64
mv baradadb-linux-arm64 /usr/local/bin/baradadb
# macOS
wget https://github.com/katehonz/barabaDB/releases/latest/download/baradadb-darwin-amd64
chmod +x baradadb-darwin-amd64
mv baradadb-darwin-amd64 /usr/local/bin/baradadb
```
### Docker
```bash
# Изтегляне на официален образ
docker pull barabadb/barabadb:latest
# Стартиране
docker run -d \
-p 9472:9472 \
-p 9470:9470 \
@@ -172,12 +229,16 @@ docker run -d \
docker-compose up -d
```
### Вградено Използване
### Вградено Използване (Nim Проекти)
Добавете към вашия `.nimble` файл:
```nim
requires "barabadb >= 0.1.0"
requires "barabadb >= 1.1.0"
```
Използване в кода:
```nim
import barabadb/storage/lsm
@@ -193,16 +254,52 @@ db.close()
# Стартиране на сървъра
./build/baradadb
# Тестване на HTTP API
# Очакван изход:
# BaraDB v1.1.0 — Multimodal Database Engine
# BaraDB TCP listening on 127.0.0.1:9472
# Тестване с HTTP API
curl http://localhost:9470/health
# Интерактивна конзола
# Интерактивна обвивка
./build/baradadb --shell
```
## Отстраняване на Проблеми с Инсталацията
### "cannot open file: hunos"
```bash
nimble install -d -y
```
### "BaraDB requires SSL support"
Винаги компилирайте с `-d:ssl`:
```bash
nim c -d:ssl --threads:on -o:build/baradadb src/baradadb.nim
```
### Бавна компилация
Използвайте паралелна компилация:
```bash
nim c -d:ssl --threads:on -d:release --parallelBuild:4 -o:build/baradadb src/baradadb.nim
```
### Голям размер на binary-то
Използвайте оптимизация на размера:
```bash
nim c -d:ssl --threads:on -d:release --opt:size --passL:-s -o:build/baradadb src/baradadb.nim
```
## Следващи Стъпки
- [Бързо Стартиране](bg/quickstart.md)
- [Конфигурация](en/configuration.md)
- [Архитектура](bg/architecture.md)
- [BaraQL Заявки](bg/baraql.md)
- [Бърз Старт](quickstart.md)
- [Конфигурационна Референция](configuration.md)
- [Преглед на Архитектурата](architecture.md)
- [BaraQL Език за Заявки](baraql.md)
+48 -9
View File
@@ -1,6 +1,29 @@
# LSM-Tree Съхранение
# LSM-Tree Storage Engine
Основният двигател за съхранение използващ Log-Structured Merge-Tree архитектура.
Основният storage engine в BaraDB, използващ Log-Structured Merge-Tree архитектура.
## Архитектура
```
┌─────────────────────────────────────────────┐
│ Записи │
│ (добавяне към WAL + MemTable) │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ MemTable │
│ (в-памет сортиран буфер) │
└─────────────────────────────────────────────┘
(при запълване, flush към SSTable)
┌─────────────────────────────────────────────┐
│ SSTable │
│ (сортирана string table на диска) │
└─────────────────────────────────────────────┘
```
## Употреба
@@ -9,17 +32,33 @@ import barabadb/storage/lsm
var db = newLSMTree("./data")
# Запис
db.put("key1", cast[seq[byte]]("value1"))
# Четене
let (found, value) = db.get("key1")
# Изтриване
db.delete("key1")
db.close()
```
## Компоненти
## Възможности
- **MemTable**: Сортиран буфер в паметта
- **WAL**: Write-ahead log за трайност
- **SSTable**: Сортирани таблици на диска
- **Bloom Filter**: Бързи негативни проверки
- **Compaction**: Сливане на SSTables
- **Page Cache**: LRU кеш
- **Write-оптимизиран**: Append-only лог структура
- **Устойчивост**: Write-ahead log (WAL) осигурява crash recovery
- **Bloom Филтър**: Бързи негативни проверки
- **Compaction**: Size-tiered стратегия слива SSTables
- **Page Cache**: LRU кеш за често достъпвани страници
## Конфигурация
```nim
var db = newLSMTree(
path = "./data",
memTableSize = 64 * 1024 * 1024, # 64MB
walEnabled = true,
bloomFpRate = 0.01
)
```
+202
View File
@@ -0,0 +1,202 @@
# Мониторинг и Наблюдаемост
## Health Checks
### HTTP Health Endpoint
```bash
curl http://localhost:9470/health
```
Отговор:
```json
{
"status": "healthy",
"version": "1.1.0",
"uptime_seconds": 86400,
"checks": {
"storage": "ok",
"memory": "ok",
"connections": "ok"
}
}
```
### Readiness Probe
```bash
curl http://localhost:9470/ready
```
Връща `200 OK` когато сървърът е готов да приема трафик, `503` по време на стартиране.
## Метрики
### Prometheus-Съвместими Метрики
```bash
curl http://localhost:9470/metrics
```
Примерен изход:
```
# HELP baradb_queries_total Общ брой изпълнени заявки
# TYPE baradb_queries_total counter
baradb_queries_total 152340
# HELP baradb_queries_duration_seconds Хистограма на времетраене на заявки
# TYPE baradb_queries_duration_seconds histogram
baradb_queries_duration_seconds_bucket{le="0.001"} 45000
baradb_queries_duration_seconds_bucket{le="0.01"} 120000
baradb_queries_duration_seconds_bucket{le="0.1"} 148000
# HELP baradb_storage_lsm_size_bytes Общ размер на LSM-Tree
# TYPE baradb_storage_lsm_size_bytes gauge
baradb_storage_lsm_size_bytes 2147483648
# HELP baradb_cache_hit_rate Page cache hit rate
# TYPE baradb_cache_hit_rate gauge
baradb_cache_hit_rate 0.94
# HELP baradb_active_connections Активни клиентски връзки
# TYPE baradb_active_connections gauge
baradb_active_connections 42
```
### JSON Метрики
```bash
curl http://localhost:9470/metrics?format=json
```
## Логване
### Нива на Логване
| Ниво | Описание |
|------|----------|
| `debug` | Детайлни вътрешни операции |
| `info` | Нормални операции |
| `warn` | Възстановими проблеми |
| `error` | Грешки, изискващи внимание |
### Структурирани JSON Логове
```bash
BARADB_LOG_LEVEL=info \
BARADB_LOG_FORMAT=json \
BARADB_LOG_FILE=/var/log/baradb/baradb.log \
./build/baradadb
```
Примерен лог запис:
```json
{
"timestamp": "2025-01-15T10:30:00.123Z",
"level": "info",
"component": "server",
"message": "Query executed",
"query": "SELECT * FROM users",
"duration_ms": 12,
"client_ip": "10.0.0.15"
}
```
### Текстов Формат
```bash
BARADB_LOG_FORMAT=text ./build/baradadb
```
## Правила за Алармиране
### Prometheus AlertManager
```yaml
groups:
- name: baradb
rules:
- alert: BaraDBHighErrorRate
expr: rate(baradb_errors_total[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "Висок процент грешки в BaraDB"
- alert: BaraDBLowCacheHitRate
expr: baradb_cache_hit_rate < 0.8
for: 10m
labels:
severity: warning
annotations:
summary: "Cache hit rate под 80%"
- alert: BaraDBHighConnections
expr: baradb_active_connections > 800
for: 5m
labels:
severity: warning
annotations:
summary: "Голям брой връзки към BaraDB"
- alert: BaraDBDown
expr: up{job="baradb"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "BaraDB инстанцията не работи"
```
## Разпределен Мониторинг
### Клъстерни Метрики
За Raft клъстери, мониторирайте:
```bash
curl http://node1:9470/metrics/cluster
```
```json
{
"cluster_id": "baradb-cluster-1",
"nodes": [
{"id": "node1", "role": "leader", "health": "healthy"},
{"id": "node2", "role": "follower", "health": "healthy"},
{"id": "node3", "role": "follower", "health": "healthy"}
],
"raft_log_index": 15420,
"raft_commit_index": 15420,
"shards": 4,
"replication_lag_ms": 5
}
```
## Профилиране на Производителност
### Вграден CPU Profiler
```bash
curl -X POST http://localhost:9470/debug/pprof/cpu?seconds=30 > cpu.prof
```
### Memory Profiler
```bash
curl http://localhost:9470/debug/pprof/heap > heap.prof
```
## Отстраняване на Проблеми с Метрики
| Симптом | Метрика | Действие |
|---------|--------|----------|
| Бавни заявки | `baradb_queries_duration_seconds` | Проверете cache hit rate, добавете индекси |
| Висока памет | `process_resident_memory_bytes` | Намалете memtable/cache размери |
| Растящо съхранение | `baradb_storage_lsm_size_bytes` | Пуснете ръчен compaction |
| Грешки при връзка | `baradb_active_connections` | Увеличете connection pool или добавете възли |
| Репликационно закъснение | `baradb_replication_lag_ms` | Проверете мрежата, увеличете ресурсите |
+125
View File
@@ -0,0 +1,125 @@
# Ръководство за Производителност
## Методология на Бенчмарковете
Всички бенчмаркове са проведени с:
- **Компилатор**: Nim 2.2.0 с `-d:release --opt:speed`
- **CPU**: AMD Ryzen 9 5900X (12 ядра / 24 нишки)
- **Памет**: 64 GB DDR4-3600
- **Диск**: Samsung 980 Pro NVMe SSD
- **ОС**: Ubuntu 24.04 LTS
Пускане на пълния бенчмарк сюит:
```bash
nim c -d:ssl -d:release -r benchmarks/bench_all.nim
```
## Storage Engine Бенчмаркове
### LSM-Tree Key-Value
| Метрика | Стойност |
|---------|----------|
| Пропускателна способност при запис | ~580,000 ops/s |
| Пропускателна способност при четене | ~720,000 ops/s |
| Средна латентност при запис | 1.7 µs |
| Средна латентност при четене | 1.4 µs |
LSM-Tree използва 64MB MemTable, WAL fsync при всеки запис и size-tiered compaction с 6 нива.
### B-Tree Индекс
| Метрика | Стойност |
|---------|----------|
| Пропускателна способност при вмъкване | ~1,200,000 ops/s |
| Пропускателна способност при точково търсене | ~1,500,000 ops/s |
| Range scan (1000 ключа) | ~0.3 ms |
## Vector Engine Бенчмаркове
### HNSW Индекс
| Метрика | Стойност |
|---------|----------|
| Вмъкване (dim=128) | ~45,000 вектора/s |
| Търсене топ-10 (dim=128, n=10K) | ~2 ms |
| Търсене топ-10 (dim=128, n=100K) | ~8 ms |
### SIMD Функции за Разстояние
| Операция | dim=128 | dim=768 | dim=1536 |
|----------|---------|---------|----------|
| Косинусово разстояние | 4.2M/s | 850K/s | 420K/s |
| L2 (Евклидово) | 4.5M/s | 920K/s | 450K/s |
| Скаларно произведение | 4.8M/s | 980K/s | 480K/s |
## Full-Text Search Бенчмаркове
| Метрика | Стойност |
|---------|----------|
| Индексиране | ~320,000 документа/s |
| BM25 търсене | ~28,000 заявки/s |
| Fuzzy търсене (distance=2) | ~850 заявки/s |
## Graph Engine Бенчмаркове
| Операция | Пропускателна способност |
|----------|--------------------------|
| Добавяне на възел | ~2.5M ops/s |
| Добавяне на ребро | ~1.8M ops/s |
| BFS (1K възела, 5K ребра) | ~12K обхождания/s |
| Dijkstra най-кратък път | ~120 µs |
| PageRank (10 итерации) | ~450 графа/s |
## Протоколни Бенчмаркове
| Протокол | Връзки | Заявки/s | Латентност p99 |
|----------|--------|----------|----------------|
| Бинарен (localhost) | 1 | 45,000 | 0.4 ms |
| Бинарен (localhost) | 100 | 380,000 | 1.2 ms |
| HTTP/REST | 1 | 12,000 | 2.1 ms |
| HTTP/REST | 100 | 95,000 | 5.8 ms |
| WebSocket | 1 | 18,000 | 1.8 ms |
## Вертикален Мащабинг
| Ядра | LSM Запис | LSM Четене | Векторно Търсене |
|------|-----------|------------|-------------------|
| 1 | 580K | 720K | 2.0 ms |
| 4 | 1.9M | 2.6M | 1.1 ms |
| 8 | 3.4M | 4.8M | 0.7 ms |
| 16 | 5.8M | 7.2M | 0.5 ms |
## Ръководство за Настройка
### За Write-Heavy Натоварвания
```bash
export BARADB_MEMTABLE_SIZE_MB=256
export BARADB_WAL_SYNC_INTERVAL_MS=10
export BARADB_COMPACTION_INTERVAL_MS=30000
```
### За Read-Heavy Натоварвания
```bash
export BARADB_CACHE_SIZE_MB=1024
export BARADB_BLOOM_BITS_PER_KEY=10
export BARADB_COMPACTION_INTERVAL_MS=120000
```
### За Векторно Търсене
```bash
export BARADB_VECTOR_EF_CONSTRUCTION=200
export BARADB_VECTOR_EF_SEARCH=128
export BARADB_VECTOR_M=32
```
### За Графова Аналитика
```bash
export BARADB_GRAPH_PAGE_RANK_ITERATIONS=20
export BARADB_GRAPH_LOUVAIN_RESOLUTION=1.0
```
+373
View File
@@ -0,0 +1,373 @@
# Протоколна Референция
BaraDB поддържа множество протоколи за клиентска комуникация:
- **Бинарен Wire Протокол** — високопроизводителен, ниска латентност
- **HTTP/REST API** — езиково-независим, лесен за дебъгване
- **WebSocket** — стрийминг и pub/sub
---
## Бинарен Wire Протокол
Бинарният протокол използва big-endian кодиране за всички многобайтови стойности.
### Жизнен Цикъл на Връзката
```
Клиент Сървър
| |
|─── TCP свързване ───────────>|
|<── TLS ръкостискане (опц.) ──|
|─── Auth съобщение ──────────>|
|<── Auth_OK / Грешка ─────────|
|─── Query съобщение ─────────>|
|<── Data / Complete / Error ──|
|─── Close съобщение ─────────>|
|<── TCP затваряне ────────────|
```
### Формат на Съобщенията
Всяко съобщение започва с 12-байтов хедър:
```
┌─────────────┬─────────────┬─────────────┬─────────────────────┐
│ Kind │ Length │ RequestId │ Payload │
│ (4 bytes) │ (4 bytes) │ (4 bytes) │ │
│ uint32 BE │ uint32 BE │ uint32 BE │ (Length bytes) │
└─────────────┴─────────────┴─────────────┴─────────────────────┘
```
### Типове Съобщения
| Тип | ID | Посока | Описание |
|------|----|--------|----------|
| Query | 0x01 | К→С | Изпълни заявка |
| QueryParams | 0x02 | К→С | Параметризирана заявка |
| Auth | 0x07 | К→С | Заявка за автентикация |
| Ping | 0x09 | К→С | Keepalive ping |
| Close | 0x0A | К→С | Затваряне на връзка |
| Data | 0x81 | С→К | Резултат от заявка |
| Complete | 0x82 | С→К | Заявката е завършена |
| Auth_OK | 0x83 | С→К | Успешна автентикация |
| Pong | 0x84 | С→К | Keepalive отговор |
| Error | 0x06 | С→К | Грешка |
### Query Съобщение Payload
```
┌───────────────────┬────────────────────────────┐
│ Query String │ │
│ (променлива) │ │
│ UTF-8 │ │
└───────────────────┴────────────────────────────┘
```
### Data Съобщение Payload
```
┌──────────────┬─────────────────────────────────────────────┐
│ Брой Колони │ Дефиниции на Колони + Данни от Редове │
│ (4 bytes) │ │
│ uint32 BE │ │
└──────────────┴─────────────────────────────────────────────┘
```
### Типове Полета
| Тип | ID | Размер | Описание |
|------|----|--------|----------|
| NULL | 0x00 | 0 | NULL стойност |
| BOOL | 0x01 | 1 | true/false |
| INT8 | 0x02 | 1 | Signed 8-bit integer |
| INT16 | 0x03 | 2 | Signed 16-bit integer |
| INT32 | 0x04 | 4 | Signed 32-bit integer |
| INT64 | 0x05 | 8 | Signed 64-bit integer |
| FLOAT32 | 0x06 | 4 | IEEE 754 единична точност (big-endian) |
| FLOAT64 | 0x07 | 8 | IEEE 754 двойна точност (big-endian) |
| STRING | 0x08 | променлив | UTF-8 низ (4-байтов префикс за дължина) |
| BYTES | 0x09 | променлив | Сурови байтове (4-байтов префикс за дължина) |
| ARRAY | 0x0A | променлив | Масив от стойности |
| OBJECT | 0x0B | променлив | Ключ-стойност обект |
| VECTOR | 0x0C | променлив | Float32 масив (4-байтов префикс, big-endian floats) |
### Error Съобщение Payload
```
┌──────────────┬──────────────┬────────────────────────────┐
│ Код Грешка │ Дълж. Съобщ. │ Съобщение за Грешка │
│ (4 bytes) │ (4 bytes) │ (Дълж. Съобщ. bytes) │
│ uint32 BE │ uint32 BE │ UTF-8 │
└──────────────┴──────────────┴────────────────────────────┘
```
---
## HTTP/REST API
Базов URL: `http://localhost:9470/api/v1`
### Endpoints
#### Health
```http
GET /health
```
Отговор:
```json
{
"status": "healthy",
"version": "1.1.0",
"uptime_seconds": 86400
}
```
#### Ready
```http
GET /ready
```
Връща `200` когато е готов, `503` при стартиране.
#### Query
```http
POST /query
Content-Type: application/json
Authorization: Bearer <token>
{
"query": "SELECT name, age FROM users WHERE age > 18",
"params": [],
"format": "json"
}
```
Отговор:
```json
{
"columns": ["name", "age"],
"rows": [
["Alice", 30],
["Bob", 25]
],
"row_count": 2,
"duration_ms": 12
}
```
#### Batch
```http
POST /batch
Content-Type: application/json
{
"queries": [
"INSERT users { name := 'Alice', age := 30 }",
"INSERT users { name := 'Bob', age := 25 }"
]
}
```
Отговор:
```json
{
"results": [
{"status": "ok", "affected_rows": 1},
{"status": "ok", "affected_rows": 1}
]
}
```
#### Schema
```http
GET /schema
```
#### Metrics
```http
GET /metrics
```
Prometheus-съвместими метрики. Виж [Ръководство за Мониторинг](monitoring.md).
#### Explain
```http
POST /explain
Content-Type: application/json
{
"query": "SELECT * FROM users WHERE age > 18"
}
```
Отговор:
```json
{
"plan": "IndexScan",
"index": "idx_users_age",
"estimated_rows": 42,
"cost": 120
}
```
#### Backup
```http
POST /backup
Content-Type: application/json
{
"destination": "/backup/snapshot.db"
}
```
#### Административни Операции
```http
POST /admin/compact
POST /admin/rebalance
POST /admin/check
```
### HTTP Статус Кодове
| Код | Значение |
|------|----------|
| 200 | Успех |
| 400 | Лоша заявка (синтактична грешка) |
| 401 | Неоторизиран (изисква се auth) |
| 403 | Забранен (недостатъчни права) |
| 404 | Не е намерен (таблица/тип не съществува) |
| 429 | Твърде много заявки (rate limited) |
| 500 | Вътрешна сървърна грешка |
| 503 | Услугата е недостъпна (стартира) |
---
## WebSocket Протокол
URL: `ws://localhost:9471`
### Формат на Frame
WebSocket текстови frame-ове съдържат JSON съобщения:
```json
{
"id": 1,
"type": "query",
"query": "SELECT * FROM users"
}
```
### Типове Съобщения
| Тип | Посока | Описание |
|------|--------|----------|
| `query` | К→С | Изпълни заявка |
| `subscribe` | К→С | Абониране за промени |
| `unsubscribe` | К→С | Отписване |
| `ping` | К→С | Keepalive |
| `result` | С→К | Резултат от заявка |
| `notification` | С→К | Известие за промяна |
| `error` | С→К | Грешка |
| `pong` | С→К | Keepalive отговор |
### Pub/Sub Пример
```javascript
const ws = new WebSocket('ws://localhost:9471');
ws.onopen = () => {
// Абониране за промени в таблица
ws.send(JSON.stringify({
id: 1,
type: 'subscribe',
table: 'users'
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'notification') {
console.log('Промяна:', msg.operation, msg.data);
}
};
```
### Стрийминг Заявки
```javascript
ws.send(JSON.stringify({
id: 2,
type: 'query',
query: 'SELECT * FROM logs ORDER BY timestamp',
streaming: true
}));
// Сървърът изпраща множество result frame-ове
// Последният frame има {"complete": true}
```
---
## Nim API Примери
### Бинарен Протокол
```nim
import barabadb/protocol/wire
let msg = makeQueryMessage(1, "SELECT * FROM users")
let ready = makeReadyMessage(1)
let error = makeErrorMessage(1, 42, "Syntax error")
```
### Connection Pool
```nim
import barabadb/protocol/pool
var pool = newConnectionPool(
minConnections = 5,
maxConnections = 100,
idleTimeout = 30000
)
let conn = pool.acquire()
# Използване на връзка...
pool.release(conn)
```
### Автентикация
```nim
import barabadb/protocol/auth
var am = newAuthManager("secret-key")
let token = am.createToken(JWTClaims(sub: "user1", role: "admin"))
let result = am.validateCredentials(
AuthCredentials(authMethod: amToken, payload: token)
)
```
### Rate Limiting
```nim
import barabadb/protocol/ratelimit
var rl = newRateLimiter(
rlaTokenBucket,
globalRate = 10000,
perClientRate = 1000
)
if rl.allowRequest("client-123"):
echo "Заявката е разрешена"
```
+210
View File
@@ -0,0 +1,210 @@
# Ръководство за Сигурност
## TLS/SSL Криптиране
BaraDB поддържа TLS 1.3 за всички протоколи (бинарен, HTTP, WebSocket). Ако не е предоставен сертификат, сървърът автоматично генерира self-signed сертификат при стартиране за криптиране без конфигурация.
### Използване на Персонализирани Сертификати
```bash
# Предоставяне на съществуващи сертификати
BARADB_TLS_ENABLED=true \
BARADB_CERT_FILE=/etc/baradb/server.crt \
BARADB_KEY_FILE=/etc/baradb/server.key \
./build/baradadb
```
### Генериране на Self-Signed Сертификати
```bash
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt \
-days 365 -nodes -subj "/CN=localhost"
```
### Let's Encrypt (Продукция)
Използвайте certbot и насочете BaraDB към генерираните файлове:
```bash
sudo certbot certonly --standalone -d db.example.com
BARADB_CERT_FILE=/etc/letsencrypt/live/db.example.com/fullchain.pem \
BARADB_KEY_FILE=/etc/letsencrypt/live/db.example.com/privkey.pem \
./build/baradadb
```
### TLS от Страна на Клиента
```python
from baradb import Client
client = Client("localhost", 9472, tls=True, tls_verify=True)
client.connect()
```
## Автентикация
### JWT-Базирана Автентикация
BaraDB използва JWT (JSON Web Tokens) с HMAC-SHA256 подписване.
#### Включване на Автентикация
```bash
BARADB_AUTH_ENABLED=true \
BARADB_JWT_SECRET="$(openssl rand -hex 32)" \
./build/baradadb
```
#### Създаване на Токени
```nim
import barabadb/protocol/auth
var am = newAuthManager("your-secret-key")
let token = am.createToken(JWTClaims(
sub: "user1",
role: "admin",
exp: getTime() + 24.hours
))
```
#### Контрол на Достъп на База Роли
| Роля | Права |
|------|-------|
| `admin` | Пълен достъп |
| `write` | Четене + запис |
| `read` | Само четене |
| `monitor` | Само метрики и health |
#### Използване на Токени
```bash
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:9470/api/query \
-d '{"query": "SELECT * FROM users"}'
```
```python
from baradb import Client
client = Client("localhost", 9472)
client.connect()
client.authenticate("eyJhbGciOiJIUzI1NiIs...")
```
### Многофакторна Автентикация (MFA)
```nim
import barabadb/protocol/auth
var am = newAuthManager("secret-key")
# TOTP-базирана MFA
let mfaCode = am.generateTOTP("user1")
let valid = am.validateTOTP("user1", mfaCode)
```
## Rate Limiting
Token-bucket rate limiting предотвратява злоупотреби:
```nim
import barabadb/protocol/ratelimit
var rl = newRateLimiter(
rlaTokenBucket,
globalRate = 10000, # 10K заявки/s глобално
perClientRate = 1000, # 1K заявки/s на IP/токен
burstSize = 100 # Разрешаване на 100 заявки burst
)
if not rl.allowRequest("client-ip"):
return error("Лимитът на заявки е надвишен")
```
## Мрежова Сигурност
### Адрес за Свързване
По подразбиране BaraDB се свързва към `127.0.0.1` (само localhost). За продукция:
```bash
# Свързване към всички интерфейси (зад защитна стена или reverse proxy)
BARADB_ADDRESS=0.0.0.0 ./build/baradadb
# Свързване към конкретен вътрешен интерфейс
BARADB_ADDRESS=10.0.0.5 ./build/baradadb
```
### Правила на Защитната Стена
```bash
# Разрешаване само на сървъри на приложения
sudo ufw allow from 10.0.0.0/8 to any port 9472
sudo ufw allow from 10.0.0.0/8 to any port 9470
# Блокиране на външен достъп до портове за управление
sudo ufw deny 9471 # WebSocket (само вътрешна употреба)
```
## Криптиране на Данни в Покой
### Криптиране на Ниво ОС
Използвайте LUKS за пълно дисково криптиране:
```bash
cryptsetup luksFormat /dev/nvme0n1p2
cryptsetup open /dev/nvme0n1p2 baradb-crypt
mkfs.ext4 /dev/mapper/baradb-crypt
mount /dev/mapper/baradb-crypt /var/lib/baradb
```
### Криптиране на Ниво Приложение
BaraDB поддържа прозрачно криптиране на SSTable файлове:
```bash
BARADB_STORAGE_ENCRYPTION_KEY="$(openssl rand -hex 32)" \
./build/baradadb
```
## Одитно Логване
Всички заявки и административни действия се логват:
```json
{
"timestamp": "2025-01-15T10:30:00Z",
"level": "info",
"event": "query_executed",
"client_ip": "10.0.0.15",
"user": "app_user",
"query": "SELECT * FROM users WHERE id = ?",
"duration_ms": 12,
"rows_returned": 1
}
```
Включване на одитно логване:
```bash
BARADB_LOG_LEVEL=info \
BARADB_LOG_FORMAT=json \
BARADB_LOG_FILE=/var/log/baradb/audit.log \
./build/baradadb
```
## Чеклист за Сигурност
- [ ] Сменете JWT secret по подразбиране
- [ ] Включете TLS с валидни сертификати
- [ ] Свържете се към конкретни интерфейси
- [ ] Включете автентикация в продукция
- [ ] Конфигурирайте rate limiting
- [ ] Включете одитно логване
- [ ] Криптирайте данните в покой (LUKS или на ниво приложение)
- [ ] Стартирайте BaraDB като non-root потребител
- [ ] Поддържайте рестриктивни правила на защитната стена
- [ ] Ротирайте JWT secret-и редовно
+78
View File
@@ -0,0 +1,78 @@
# Storage Engines
BaraDB предоставя множество storage двигатели, оптимизирани за различни модели на достъп.
## LSM-Tree (Key-Value)
Основният storage engine с write-оптимизирана append-only лог структура.
### Употреба
```nim
import barabadb/storage/lsm
var db = newLSMTree("./data")
db.put("key1", cast[seq[byte]]("value1"))
let (found, value) = db.get("key1")
db.close()
```
### Компоненти
- **MemTable**: В-памет сортиран буфер
- **WAL**: Write-ahead log за устойчивост
- **SSTable**: Сортирани string таблици на диска
- **Bloom Filter**: Вероятностна проверка за принадлежност
- **Compaction**: Size-tiered стратегия с управление на нива
- **Page Cache**: LRU кеш с проследяване на hit rate
## B-Tree Индекс
Подреден индекс за range сканиране и точково търсене.
### Употреба
```nim
import barabadb/storage/btree
var btree = newBTreeIndex[string, string]()
btree.insert("key1", "value1")
let values = btree.get("key1")
let range = btree.scan("key_a", "key_z")
```
## Write-Ahead Log (WAL)
Осигурява устойчивост на операциите за запис.
```nim
import barabadb/storage/wal
var wal = newWAL("./wal")
wal.append("txn1", "SET key1 value1")
wal.flush()
```
## Bloom Filter
Вероятностна структура от данни за бързи негативни проверки.
```nim
import barabadb/storage/bloom
var filter = newBloomFilter(10000, 0.01)
filter.add("key1")
if filter.mightContain("key1"):
echo "евентуално съществува"
```
## Memory-mapped I/O
Ефективен достъп до файлове чрез mmap.
```nim
import barabadb/storage/mmap
var mapped = mmapFile("./data/file.dat")
let data = mapped.read(0, 100)
```
+46 -12
View File
@@ -1,6 +1,6 @@
# Транзакции & MVCC
# Транзакции и MVCC
Multi-Version Concurrency Control със snapshot изолация.
MVCC (Multi-Version Concurrency Control) със snapshot изолация и deadlock детекция.
## Употреба
@@ -10,21 +10,55 @@ import barabadb/core/mvcc
var tm = newTxnManager()
let txn = tm.beginTxn()
# Операции за запис
discard tm.write(txn, "key1", cast[seq[byte]]("value1"))
discard tm.write(txn, "key2", cast[seq[byte]]("value2"))
# Savepoint
tm.savepoint(txn)
discard tm.rollbackToSavepoint(txn)
discard tm.write(txn, "key3", cast[seq[byte]]("value3"))
discard tm.rollbackToSavepoint(txn) # отмяна на key3
# Commit
discard tm.commit(txn)
```
## Изолация
## Изолация на Транзакции
BaraDB използва **snapshot isolation**:
- Читателите не блокират писатели
- Писателите не блокират читатели
- Всяка транзакция вижда консистентен моментна снимка
BaraDB използва **snapshot изолация**:
- Четящите не блокират пишещите
- Пишещите не блокират четящите
- Всяка транзакция вижда консистентен snapshot
## Deadlock Детекция
```nim
import barabadb/core/deadlock
var detector = newDeadlockDetector()
if detector.detectCycle(txn1, txn2):
echo "Открит deadlock!"
```
## Write-Ahead Log
```nim
import barabadb/storage/wal
var wal = newWAL("./wal")
wal.append(txnId, "SET key value")
wal.flush()
```
## Savepoints
Вложени savepoints на транзакции:
```nim
tm.savepoint(txn, "sp1")
# ... операции ...
tm.rollbackToSavepoint(txn, "sp1")
```
## Формална Верификация
@@ -32,13 +66,13 @@ MVCC / Snapshot Isolation протоколът е формално специф
- **Спецификация:** `formal-verification/mvcc.tla`
- **Проверени свойства:**
- `NoDirtyReads` — транзакциите никога не четат неcommit-нати данни
- `NoDirtyReads` — транзакциите никога не четат некомитнати данни
- `ReadOwnWrites` — транзакциите винаги виждат собствените си записи
- `WriteWriteConflict` — first-committer-wins
- `WriteWriteConflict` — first-committer-wins (няма две комитнати транзакции да пишат един и същ ключ)
Пускане на TLC:
Пускане на TLC локално:
```bash
cd formal-verification
java -cp tla2tools.jar tlc2.TLC -config models/mvcc.cfg mvcc.tla
```
```
+164
View File
@@ -0,0 +1,164 @@
# Ръководство за Отстраняване на Проблеми
## Проблеми с Инсталацията
### Nim не е намерен
```
nim: command not found
```
**Решение:**
```bash
# Linux/macOS
curl https://nim-lang.org/choosenim/init.sh -sSf | sh
# Добавяне към PATH
echo 'export PATH=$HOME/.nimble/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
```
### SSL Грешка при Компилация
```
Error: BaraDB requires SSL support. Compile with -d:ssl
```
**Решение:** Винаги компилирайте с `-d:ssl`:
```bash
nim c -d:ssl -d:release -o:build/baradadb src/baradadb.nim
```
### Липсващи Зависимости
```
Error: cannot open file: hunos
```
**Решение:**
```bash
nimble install -d -y
```
## Проблеми по Време на Изпълнение
### Портът е Зает
```
Error: unhandled exception: Address already in use [OSError]
```
**Решение 1:** Сменете порта:
```bash
BARADB_PORT=5433 ./build/baradadb
```
**Решение 2:** Убийте съществуващия процес:
```bash
lsof -ti:9472 | xargs kill -9
```
### Permission Denied на Директория с Данни
```
Error: Permission denied: ./data
```
**Решение:**
```bash
mkdir -p ./data
chmod 755 ./data
# или използвайте друга директория
BARADB_DATA_DIR=/tmp/baradb ./build/baradadb
```
## Storage Проблеми
### Бавни Заявки
1. Проверете cache hit rate:
```bash
curl http://localhost:9470/metrics | grep cache_hit_rate
```
2. Пуснете ръчен compaction:
```bash
curl -X POST http://localhost:9470/admin/compact
```
3. Проверете броя на SSTables:
```bash
curl http://localhost:9470/metrics | grep sstables
```
### Растящо Дисково Пространство
```bash
# Проверете размера на директорията с данни
du -sh ./data
# Проверете WAL размера
du -sh ./data/server/wal
# Пуснете ръчен compaction за освобождаване на място
curl -X POST http://localhost:9470/admin/compact
```
## Проблеми с Автентикация
### Грешка при Верификация на JWT
```json
{"error": {"code": "AUTH_REQUIRED", "message": "Authentication required"}}
```
**Решение:** Уверете се, че изпращате правилния токен:
```bash
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:9470/api/query \
-d '{"query": "SELECT 1"}'
```
## Разпределени Проблеми
### Възел не се Присъединява към Клъстер
1. Проверете мрежовата свързаност между възлите
2. Проверете gossip порта (raft порт + 100)
3. Проверете логовете за грешки при gossip
### Репликационно Закъснение
```bash
# Проверете replication lag
curl http://localhost:9470/metrics/cluster | jq .replication_lag_ms
```
## Често Задавани Въпроси
### Как да нулирам напълно базата данни?
```bash
# Спрете сървъра
# Изтрийте директорията с данни
rm -rf ./data
# Стартирайте отново — нова празна база ще бъде създадена
./build/baradadb
```
### Как да мигрирам от друга база данни?
BaraDB поддържа импорт чрез JSON и CSV:
```bash
curl -X POST http://localhost:9470/api/import \
-H "Content-Type: application/json" \
-d '{"table": "users", "format": "json", "data": [...]}'
```
+55
View File
@@ -0,0 +1,55 @@
# Потребителски Функции (UDF)
Разширете BaraQL с персонализирани функции.
## Употреба
```nim
import barabadb/query/udf
var reg = newUDFRegistry()
# Регистриране на стандартна библиотека
reg.registerStdlib() # abs, sqrt, pow, lower, upper, len, trim, substr, toString, toInt
# Персонализирана функция
reg.register("greet", @[UDFParam(name: "name", typeName: "str")],
"str", proc(args: seq[Value]): Value =
return Value(kind: vkString, strVal: "Hello, " & args[0].strVal & "!"))
```
## Функции от Стандартната Библиотека
| Функция | Описание | Пример |
|----------|----------|--------|
| `abs(n)` | Абсолютна стойност | `abs(-5)` → 5 |
| `sqrt(n)` | Квадратен корен | `sqrt(16)` → 4 |
| `pow(n, e)` | Степенуване | `pow(2, 3)` → 8 |
| `lower(s)` | Малки букви | `lower('ABC')` → 'abc' |
| `upper(s)` | Главни букви | `upper('abc')` → 'ABC' |
| `len(s)` | Дължина | `len('hello')` → 5 |
| `trim(s)` | Премахване на интервали | `trim(' hello ')` → 'hello' |
| `substr(s, start, len)` | Подниз | `substr('hello', 0, 3)` → 'hel' |
| `toString(n)` | Конвертиране в низ | `toString(123)` → '123' |
| `toInt(s)` | Конвертиране в integer | `toInt('123')` → 123 |
## Регистриране на Функции
```nim
reg.register(
name: "my_function",
params: @[
UDFParam(name: "arg1", typeName: "str"),
UDFParam(name: "arg2", typeName: "int32")
],
returnType: "str",
body: proc(args: seq[Value]): Value =
result = Value(kind: vkString, strVal: "")
)
```
## Използване на UDF в Заявки
```sql
SELECT greet(name) FROM users;
```
+130 -14
View File
@@ -1,8 +1,89 @@
# Vector Търсене
# Vector Search Engine
HNSW и IVF-PQ индекси за търсене на прилика.
Нативни HNSW и IVF-PQ индекси за търсене на прилика с пълна SQL интеграция.
## Употреба
## SQL Употреба
### Създаване на Векторни Колони
```sql
CREATE TABLE items (
id INT PRIMARY KEY,
embedding VECTOR(768)
);
```
Типът `VECTOR(n)` съхранява float32 масиви с фиксирана размерност `n`.
### Вмъкване на Вектори
```sql
INSERT INTO items (id, embedding) VALUES (1, '[0.1, 0.2, 0.3, ...]');
```
### Функции за Векторно Разстояние
```sql
-- Косинусово разстояние (0 = идентични, 1 = ортогонални)
SELECT id, cosine_distance(embedding, '[0.1, 0.2, 0.3]') AS dist
FROM items;
-- Евклидово / L2 разстояние
SELECT id, euclidean_distance(embedding, '[0.1, 0.2, 0.3]') AS dist
FROM items;
-- L2 разстояние с <-> оператор
SELECT id, embedding <-> '[0.1, 0.2, 0.3]' AS dist
FROM items;
-- Скаларно произведение (отрицателно dot product за минимизация)
SELECT id, inner_product(embedding, '[0.1, 0.2, 0.3]') AS dist
FROM items;
-- Манхатън / L1 разстояние
SELECT id, l1_distance(embedding, '[0.1, 0.2, 0.3]') AS dist
FROM items;
```
### Търсене на Най-близки Съседи
```sql
-- Топ-10 най-близки съседи по косинусово разстояние
SELECT id FROM items
ORDER BY cosine_distance(embedding, '[0.1, 0.2, 0.3]') ASC
LIMIT 10;
-- Топ-5 най-близки съседи по евклидово разстояние
SELECT id FROM items
ORDER BY embedding <-> '[0.1, 0.2, 0.3]'
LIMIT 5;
```
### Векторни Индекси
```sql
-- Създаване на HNSW индекс за приблизително търсене на най-близки съседи
CREATE INDEX idx_items_vec ON items(embedding) USING hnsw;
-- Индексът се поддържа автоматично при INSERT и UPDATE
```
Поддържани индекс методи:
- `USING hnsw` — Hierarchical Navigable Small World (по подразбиране: косинусова метрика)
- `USING ivfpq` — Inverted File с Product Quantization
### Валидация на Размерност
BaraDB валидира размерностите на векторите при вмъкване:
```sql
-- Това ще даде грешка: очаквани 768 размерности, но подадени 3
INSERT INTO items (id, embedding) VALUES (2, '[1.0, 2.0, 3.0]');
```
## Нативно Nim API
За вградена или високопроизводителна употреба използвайте нативното Nim API директно:
```nim
import barabadb/vector/engine
@@ -10,32 +91,67 @@ import barabadb/vector/engine
var idx = newHNSWIndex(dimensions = 128)
idx.insert(1, @[1.0'f32, 0.0'f32, ...], {"category": "A"}.toTable)
# Търсене
let results = idx.search(queryVector, k = 10)
# С филтриране по метаданни
let filtered = idx.searchWithFilter(queryVector, k = 10,
filter = proc(meta: Table[string, string]): bool =
return meta.getOrDefault("category") == "A")
```
## Индекс Типове
## Типове Индекси
### HNSW
Иерархичен навигируем малък свят:
Иерархичен навигируем малък свят за приблизително търсене на най-близки съседи.
```nim
var hnsw = newHNSWIndex(dimensions = 128, m = 16)
var hnsw = newHNSWIndex(
dimensions = 128,
m = 16, # връзки на слой
efConstruction = 200, # ширина на търсене при изграждане
efSearch = 100 # ширина на търсене при заявка
)
```
### IVF-PQ
Инвертиран файл с продуктово квантуване:
Inverted File Index с продуктово квантуване за компресия.
```nim
var ivfpq = newIVFPQIndex(dimensions = 128, numCentroids = 256)
var ivfpq = newIVFPQIndex(
dimensions = 128,
numCentroids = 256,
subQuantizers = 8
)
```
## Метрики за Разстояние
| Метрика | Описание |
|---------|----------|
| `cosine` | Косинусова прилика |
| `euclidean` | L2 разстояние |
| `dotproduct` | Скаларно произведение |
| `manhattan` | L1 разстояние |
| Метрика | SQL Функция | Описание |
|---------|-------------|----------|
| `cosine` | `cosine_distance(a, b)` | Косинусова dissimilarity (1 - similarity) |
| `euclidean` | `euclidean_distance(a, b)` / `<->` | L2 разстояние |
| `dotproduct` | `inner_product(a, b)` | Отрицателно скаларно произведение |
| `manhattan` | `l1_distance(a, b)` | L1 разстояние |
## Квантуване
```nim
import barabadb/vector/quant
# Скаларно квантуване
let scalar = scalarQuantize(data, bits = 8)
# Продуктово квантуване
let pq = productQuantize(data, subVectors = 8, bits = 8)
```
## SIMD Ускорение
```nim
import barabadb/vector/simd
let dist = simdCosineDistance(vec1, vec2)
```
+147 -21
View File
@@ -18,6 +18,7 @@ BaraQL is a SQL-compatible query language with extensions for graph, vector, and
| `bytes` | Raw bytes | `0xDEADBEEF` |
| `array<T>` | Homogeneous array | `[1, 2, 3]` |
| `vector` | Float32 vector | `[0.1, 0.2, 0.3]` |
| `vector(n)` | Fixed-dimension float32 vector (SQL) | `VECTOR(768)` |
| `object` | Key-value object | `{"a": 1}` |
| `datetime` | ISO 8601 timestamp | `'2025-01-15T10:30:00Z'` |
| `uuid` | UUID v4 | `'550e8400-e29b-41d4-a716-446655440000'` |
@@ -352,6 +353,7 @@ CREATE TYPE Cat EXTENDING Animal {
CREATE INDEX idx_users_name ON users(name);
CREATE UNIQUE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_age ON users(age) USING btree;
CREATE INDEX idx_vectors ON items(embedding) USING hnsw;
```
### DROP
@@ -387,37 +389,76 @@ SELECT * FROM articles WHERE body @@ 'machine learning';
RECOVER TO TIMESTAMP '2026-05-07T12:00:00';
```
## Vector Search
## Vector Search (SQL)
### Creating Vector Columns
```sql
-- Insert with vector
INSERT articles {
title := 'Nim Programming',
embedding := [0.1, 0.2, 0.3, 0.4]
};
CREATE TABLE items (
id INT PRIMARY KEY,
embedding VECTOR(768)
);
```
-- Similarity search (cosine distance)
SELECT title FROM articles
ORDER BY cosine_distance(embedding, [0.1, 0.2, 0.3, 0.4])
LIMIT 5;
### Inserting Vectors
-- Euclidean distance
SELECT title FROM articles
ORDER BY l2_distance(embedding, [0.1, 0.2, 0.3, 0.4])
LIMIT 5;
```sql
INSERT INTO items (id, embedding) VALUES (1, '[0.1, 0.2, 0.3, 0.4]');
```
-- Dot product
SELECT title FROM articles
ORDER BY dot_product(embedding, [0.1, 0.2, 0.3, 0.4]) DESC
### Distance Functions
```sql
-- Cosine distance (0 = identical, 2 = opposite)
SELECT id, cosine_distance(embedding, '[0.1, 0.2, 0.3, 0.4]') AS dist
FROM items;
-- Euclidean / L2 distance
SELECT id, euclidean_distance(embedding, '[0.1, 0.2, 0.3, 0.4]') AS dist
FROM items;
-- L2 distance with <-> operator
SELECT id, embedding <-> '[0.1, 0.2, 0.3, 0.4]' AS dist
FROM items;
-- Inner product (negative dot product)
SELECT id, inner_product(embedding, '[0.1, 0.2, 0.3, 0.4]') AS dist
FROM items;
-- Manhattan / L1 distance
SELECT id, l1_distance(embedding, '[0.1, 0.2, 0.3, 0.4]') AS dist
FROM items;
```
### Nearest Neighbor Search
```sql
-- Top-10 nearest neighbors by cosine distance
SELECT id FROM items
ORDER BY cosine_distance(embedding, '[0.1, 0.2, 0.3, 0.4]') ASC
LIMIT 10;
-- Top-5 nearest neighbors by Euclidean distance
SELECT id FROM items
ORDER BY embedding <-> '[0.1, 0.2, 0.3, 0.4]'
LIMIT 5;
-- With metadata filter
SELECT title FROM articles
SELECT id FROM items
WHERE category = 'tech'
ORDER BY cosine_distance(embedding, [0.1, 0.2, 0.3, 0.4])
ORDER BY cosine_distance(embedding, '[0.1, 0.2, 0.3, 0.4]')
LIMIT 5;
```
### Vector Indexes
```sql
-- Create HNSW index for approximate nearest neighbor search
CREATE INDEX idx_items_vec ON items(embedding) USING hnsw;
-- Supported index methods: hnsw, ivfpq
```
## Graph Patterns
```sql
@@ -517,6 +558,88 @@ LIMIT 10;
SELECT /*+ PARALLEL(4) */ * FROM large_table;
```
## Window Functions
```sql
-- Ranking functions
SELECT
name,
department,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS r,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dr
FROM employees;
-- Value functions
SELECT
name,
salary,
LAG(salary, 1, 0) OVER (ORDER BY salary) AS prev_salary,
LEAD(salary, 1, 0) OVER (ORDER BY salary) AS next_salary,
FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS cheapest,
LAST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS most_expensive
FROM employees;
-- Distribution functions
SELECT name, NTILE(4) OVER (ORDER BY salary) AS quartile FROM employees;
```
### Frame Specifications
```sql
-- ROWS frame
SUM(salary) OVER (
PARTITION BY department
ORDER BY hire_date
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
)
-- RANGE frame
SUM(salary) OVER (
PARTITION BY department
ORDER BY hire_date
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
```
## Multi-Tenant ERP
BaraDB supports running multiple companies (tenants) in a single database instance, using **Row-Level Security (RLS)** combined with **session variables**.
### Session Variables
```sql
SET app.tenant_id = 'company-123';
SELECT current_setting('app.tenant_id') AS tenant;
```
### Current User / Role
```sql
SELECT current_user AS me, current_role AS my_role;
```
### RLS Tenant Isolation
```sql
-- Enable RLS on a table
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
-- Create a policy that filters by tenant
CREATE POLICY tenant_isolation ON invoices
FOR SELECT USING (tenant_id = current_setting('app.tenant_id'));
-- Each session only sees its own data
SET app.tenant_id = 'company-a';
SELECT * FROM invoices; -- only company-a rows
```
### Why Multi-Tenant?
- **One instance, many tenants** — no need to run 100 separate databases
- **JSONB documents** — schema-flexible storage, easy to add fields per tenant
- **RLS guarantees isolation** — the database enforces tenant boundaries, not just the application
## Supported Keywords
| Category | Keywords |
@@ -531,8 +654,11 @@ SELECT /*+ PARALLEL(4) */ * FROM large_table;
| Transaction | BEGIN, COMMIT, ROLLBACK, SAVEPOINT |
| Graph | MATCH, RETURN, WHERE, shortestPath, type |
| FTS | MATCH, AGAINST, relevance, IN BOOLEAN MODE, WITH FUZZINESS |
| Vector | cosine_distance, l2_distance, dot_product, manhattan_distance |
| Vector | cosine_distance, euclidean_distance, inner_product, l1_distance, l2_distance, <-> |
| JSON | ->, ->> |
| FTS | @@ (BM25 match) |
| Recovery | RECOVER TO TIMESTAMP |
| Functions | count, sum, avg, min, max, stddev, variance, abs, sqrt, lower, upper, len, trim, substr, now, last_insert_id |
| Functions | count, sum, avg, min, max, stddev, variance, abs, sqrt, lower, upper, len, trim, substr, now, last_insert_id, current_setting |
| Session | SET, current_setting, current_user, current_role |
| Window | OVER, PARTITION BY, ROWS, RANGE, UNBOUNDED PRECEDING, CURRENT ROW, FOLLOWING |
| Window Functions | ROW_NUMBER, RANK, DENSE_RANK, LEAD, LAG, FIRST_VALUE, LAST_VALUE, NTILE |
+39
View File
@@ -2,6 +2,26 @@
All notable changes to BaraDB are documented in this file.
## [Unreleased] — SQL:2023 Stabilization
### Fixed
- **GROUPING SETS execution** — `lowerSelect` now creates `irpkGroupBy` when `selGroupingSetsKind != gskNone`, even if `selGroupBy` is empty. Previously, queries like `GROUP BY GROUPING SETS ((dept), ())` bypassed the grouping executor entirely.
- **FTS CREATE INDEX docId mismatch** — `CREATE INDEX ... USING FTS` now computes `docId` as a hash of `tableName.$key`, consistent with DML operations (`INSERT`/`UPDATE`/`DELETE`). Previously, index creation used sequential IDs (0, 1, 2...), causing `@@` queries to never match indexed documents.
- **Test isolation (all suites)** — All `newLSMTree("")` calls replaced with unique temporary directories per suite. Eliminates WAL accumulation issues and flaky tests caused by shared database state between test runs.
- **Window frame parser** — `parseFrameBoundary` no longer consumes `tkRow` after `tkCurrent` incorrectly (was using `tkRows`). Also fixed `tkRow` keyword conflict with `ENABLE ROW LEVEL SECURITY` parsing.
- **ORDER BY + SELECT projection** — `lowerSelect` now places `irpkSort` before `irpkProject`, enabling `ORDER BY` on columns not present in the `SELECT` list.
- **UNPIVOT execution** — Verified and fixed missing test coverage for UNPIVOT transformation.
### Added
- **JSON operators** — `@>` (contains), `<@` (contained by), `?` (has key), `?|` (has any), `?&` (has all) now supported in lexer, parser, and executor.
- **Window frame execution** — `ROWS BETWEEN X PRECEDING AND Y FOLLOWING` / `CURRENT ROW` frame boundaries now respected by `FIRST_VALUE` and `LAST_VALUE`.
- **Session variables** — `SET var_name = value` and `current_setting('var_name')` for connection-scoped key/value storage.
- **Current user/role** — `current_user` and `current_role` SQL keywords evaluate to the authenticated session's user and role.
- **Auth-executor bridge** — Wire server and HTTP server now populate `ExecutionContext.currentUser` and `ExecutionContext.currentRole` after JWT/SCRAM authentication.
- **Multi-tenant RLS** — Row-Level Security policies can now reference `current_user`, `current_role`, and `current_setting('app.tenant_id')` for per-tenant data isolation.
## [1.1.0] — 2026-05-13
### Added
@@ -174,6 +194,25 @@ All notable changes to BaraDB are documented in this file.
## [Unreleased]
### Added
- **Vector SQL Integration** — Full SQL-level vector search support:
- `VECTOR(n)` column type in `CREATE TABLE` with dimension validation
- `CREATE INDEX ... USING hnsw` / `USING ivfpq` for approximate nearest neighbor indexes
- SQL distance functions: `cosine_distance()`, `euclidean_distance()`, `inner_product()`, `l1_distance()`, `l2_distance()`
- `<->` nearest-neighbor operator (Euclidean distance)
- `ORDER BY` support for vector distance expressions, including columns not in `SELECT`
- Automatic HNSW index maintenance on `INSERT` and `UPDATE`
- **Advanced SQL Engine** — Window functions, MERGE/UPSERT, LATERAL JOIN, PIVOT/UNPIVOT, SQL/PGQ Property Graph, Advanced Aggregates (ARRAY_AGG, STRING_AGG, FILTER, GROUPING SETS/ROLLUP/CUBE)
- **JavaScript Client — TCP Request Queue** — Internal `_requestQueue` + `_requestLock` for safe concurrent queries. Multiple parallel `query()` / `execute()` / `ping()` calls no longer interleave binary frames on the wire.
### Fixed
- **Query Executor — Row Value Escaping** — `execInsert` now properly escapes commas and equals signs in column values, fixing storage corruption for vector literals like `[1.0, 2.0, 3.0]`
- **Query Planner — ORDER BY Projection** — `irpkSort` is now placed before `irpkProject` in the IR plan, allowing `ORDER BY` to reference columns that are not selected
- **Wire Protocol — Big-Endian Float Serialization** — `FLOAT32`/`FLOAT64` and vector float values are now serialized in big-endian byte order, matching the client's `readFloatBE()` / `readDoubleBE()` and ensuring cross-platform numeric accuracy.
- **Gossip Protocol — Async UDP Socket** — Replaced synchronous `newSocket` + blocking `recvFrom` with `newAsyncSocket` + `await recvFrom`, preventing the async event loop from freezing until a UDP packet arrives.
### Planned
- Query plan caching
+12
View File
@@ -45,6 +45,18 @@ await client.commit();
await client.close();
```
### Concurrent Queries
The JavaScript client automatically serializes concurrent requests over a single TCP connection via an internal request queue. You can safely fire multiple parallel operations — their binary frames will not interleave on the wire:
```typescript
const [users, orders, stats] = await Promise.all([
client.query('SELECT * FROM users'),
client.query('SELECT * FROM orders'),
client.query('SELECT count(*) FROM visits')
]);
```
### WebSocket Streaming
```typescript
+1 -1
View File
@@ -156,7 +156,7 @@ raft_peers = "node2:9001,node3:9001"
```
```
BaraDB v0.1.0 — Multimodal Database Engine
BaraDB v1.1.0 — Multimodal Database Engine
Usage:
baradadb [options]
+1 -1
View File
@@ -6,7 +6,7 @@
```bash
# Клониране на репото
git clone https://github.com/katehonz/barabaDB.git
git clone https://codeberg.org/baraba/baradb
cd barabaDB
# Build на образа
+3 -3
View File
@@ -99,7 +99,7 @@ download from [slproweb.com](https://slproweb.com/products/Win32OpenSSL.html).
### Clone the Repository
```bash
git clone https://github.com/katehonz/barabaDB.git
git clone https://codeberg.org/baraba/baradb
cd barabaDB
```
@@ -144,7 +144,7 @@ strip build/baradadb
```bash
./build/baradadb --version
# Expected: BaraDB v0.1.0 — Multimodal Database Engine
# Expected: BaraDB v1.1.0 — Multimodal Database Engine
```
## Running Tests
@@ -259,7 +259,7 @@ db.close()
./build/baradadb
# Expected output:
# BaraDB v0.1.0 — Multimodal Database Engine
# BaraDB v1.1.0 — Multimodal Database Engine
# BaraDB TCP listening on 127.0.0.1:9472
# Test with HTTP API
+3 -3
View File
@@ -98,13 +98,13 @@ Every message starts with a 8-byte header:
| INT16 | 0x03 | 2 | Signed 16-bit integer |
| INT32 | 0x04 | 4 | Signed 32-bit integer |
| INT64 | 0x05 | 8 | Signed 64-bit integer |
| FLOAT32 | 0x06 | 4 | IEEE 754 single precision |
| FLOAT64 | 0x07 | 8 | IEEE 754 double precision |
| FLOAT32 | 0x06 | 4 | IEEE 754 single precision (big-endian) |
| FLOAT64 | 0x07 | 8 | IEEE 754 double precision (big-endian) |
| STRING | 0x08 | variable | UTF-8 string (4-byte length prefix) |
| BYTES | 0x09 | variable | Raw bytes (4-byte length prefix) |
| ARRAY | 0x0A | variable | Array of values |
| OBJECT | 0x0B | variable | Key-value object |
| VECTOR | 0x0C | variable | Float32 array (4-byte length prefix) |
| VECTOR | 0x0C | variable | Float32 array (4-byte length prefix, big-endian floats) |
### Error Message Payload
+89 -8
View File
@@ -1,8 +1,89 @@
# Vector Search Engine
Native HNSW and IVF-PQ indexes for similarity search.
Native HNSW and IVF-PQ indexes for similarity search with full SQL integration.
## Usage
## SQL Usage
### Creating Vector Columns
```sql
CREATE TABLE items (
id INT PRIMARY KEY,
embedding VECTOR(768)
);
```
The `VECTOR(n)` type stores float32 arrays of fixed dimension `n`.
### Inserting Vectors
```sql
INSERT INTO items (id, embedding) VALUES (1, '[0.1, 0.2, 0.3, ...]');
```
### Vector Distance Functions
```sql
-- Cosine distance (0 = identical, 1 = orthogonal)
SELECT id, cosine_distance(embedding, '[0.1, 0.2, 0.3]') AS dist
FROM items;
-- Euclidean / L2 distance
SELECT id, euclidean_distance(embedding, '[0.1, 0.2, 0.3]') AS dist
FROM items;
-- L2 distance with <-> operator
SELECT id, embedding <-> '[0.1, 0.2, 0.3]' AS dist
FROM items;
-- Inner product (negative dot product for minimization)
SELECT id, inner_product(embedding, '[0.1, 0.2, 0.3]') AS dist
FROM items;
-- Manhattan / L1 distance
SELECT id, l1_distance(embedding, '[0.1, 0.2, 0.3]') AS dist
FROM items;
```
### Nearest Neighbor Search
```sql
-- Top-10 nearest neighbors by cosine distance
SELECT id FROM items
ORDER BY cosine_distance(embedding, '[0.1, 0.2, 0.3]') ASC
LIMIT 10;
-- Top-5 nearest neighbors by Euclidean distance
SELECT id FROM items
ORDER BY embedding <-> '[0.1, 0.2, 0.3]'
LIMIT 5;
```
### Vector Indexes
```sql
-- Create HNSW index for approximate nearest neighbor search
CREATE INDEX idx_items_vec ON items(embedding) USING hnsw;
-- The index is automatically maintained on INSERT and UPDATE
```
Supported index methods:
- `USING hnsw` — Hierarchical Navigable Small World (default: cosine metric)
- `USING ivfpq` — Inverted File with Product Quantization
### Dimension Validation
BaraDB validates vector dimensions at insert time:
```sql
-- This will fail: expected 768 dimensions but got 3
INSERT INTO items (id, embedding) VALUES (2, '[1.0, 2.0, 3.0]');
```
## Native Nim API
For embedded or high-performance use, use the native Nim API directly:
```nim
import barabadb/vector/engine
@@ -48,12 +129,12 @@ var ivfpq = newIVFPQIndex(
## Distance Metrics
| Metric | Description |
|--------|-------------|
| `cosine` | Cosine similarity |
| `euclidean` | L2 distance |
| `dotproduct` | Dot product similarity |
| `manhattan` | L1 distance |
| Metric | SQL Function | Description |
|--------|--------------|-------------|
| `cosine` | `cosine_distance(a, b)` | Cosine dissimilarity (1 - similarity) |
| `euclidean` | `euclidean_distance(a, b)` / `<->` | L2 distance |
| `dotproduct` | `inner_product(a, b)` | Negative dot product |
| `manhattan` | `l1_distance(a, b)` | L1 distance |
## Quantization
+106 -1
View File
@@ -489,4 +489,109 @@ LIMIT 10;
-- اجرای موازی
SELECT /*+ PARALLEL(4) */ * FROM large_table;
```
```
## توابع پنجره‌ای
```sql
-- توابع رتبه‌بندی
SELECT
name,
department,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS r,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dr
FROM employees;
-- توابع مقدار
SELECT
name,
salary,
LAG(salary, 1, 0) OVER (ORDER BY salary) AS prev_salary,
LEAD(salary, 1, 0) OVER (ORDER BY salary) AS next_salary,
FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS cheapest,
LAST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS most_expensive
FROM employees;
-- توابع توزیع
SELECT name, NTILE(4) OVER (ORDER BY salary) AS quartile FROM employees;
```
### مشخصات فریم
```sql
-- فریم ROWS
SUM(salary) OVER (
PARTITION BY department
ORDER BY hire_date
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
)
-- فریم RANGE
SUM(salary) OVER (
PARTITION BY department
ORDER BY hire_date
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
```
## ERP چند مستأجری
BaraDB از اجرای چندین شرکت (tenant) در یک نمونه پایگاه داده پشتیبانی می‌کند، با استفاده از **امنیت سطح سطر (RLS)** همراه با **متغیرهای جلسه**.
### متغیرهای جلسه
```sql
SET app.tenant_id = 'company-123';
SELECT current_setting('app.tenant_id') AS tenant;
```
### کاربر / نقش فعلی
```sql
SELECT current_user AS me, current_role AS my_role;
```
### جداسازی مستأجر با RLS
```sql
-- فعال کردن RLS روی جدول
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
-- ایجاد سیاست فیلترینگ بر اساس مستأجر
CREATE POLICY tenant_isolation ON invoices
FOR SELECT USING (tenant_id = current_setting('app.tenant_id'));
-- هر جلسه فقط داده‌های خود را می‌بیند
SET app.tenant_id = 'company-a';
SELECT * FROM invoices; -- فقط ردیف‌های company-a
```
### چرا چند مستأجری؟
- **یک نمونه، مستأجران زیاد** — نیازی به اجرای ۱۰۰ پایگاه داده جداگانه نیست
- **اسناد JSONB** — ذخیره‌سازی با طرح انعطاف‌پذیر، افزودن آسان فیلدها برای هر مستأجر
- **RLS تضمین می‌کند** — پایگاه داده مرزهای مستأجر را اعمال می‌کند، نه فقط برنامه
## کلمات کلیدی پشتیبانی‌شده
| دسته | کلمات کلیدی |
|----------|----------|
| DQL | SELECT, FROM, WHERE, ORDER BY, GROUP BY, HAVING, LIMIT, OFFSET, DISTINCT |
| DML | INSERT, UPDATE, DELETE, SET, VALUES |
| DDL | CREATE TYPE, DROP TYPE, CREATE INDEX, DROP INDEX, ALTER TYPE |
| Join | INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN, CROSS JOIN, ON |
| Set | UNION, UNION ALL, INTERSECT, EXCEPT |
| CTEs | WITH, RECURSIVE, AS |
| Case | CASE, WHEN, THEN, ELSE, END |
| Transaction | BEGIN, COMMIT, ROLLBACK, SAVEPOINT |
| Graph | MATCH, RETURN, WHERE, shortestPath, type |
| FTS | MATCH, AGAINST, relevance, IN BOOLEAN MODE, WITH FUZZINESS |
| Vector | cosine_distance, euclidean_distance, inner_product, l1_distance, l2_distance, <-> |
| JSON | ->, ->> |
| FTS | @@ (BM25 match) |
| Recovery | RECOVER TO TIMESTAMP |
| Functions | count, sum, avg, min, max, stddev, variance, abs, sqrt, lower, upper, len, trim, substr, now, last_insert_id, current_setting |
| Session | SET, current_setting, current_user, current_role |
| Window | OVER, PARTITION BY, ROWS, RANGE, UNBOUNDED PRECEDING, CURRENT ROW, FOLLOWING |
| Window Functions | ROW_NUMBER, RANK, DENSE_RANK, LEAD, LAG, FIRST_VALUE, LAST_VALUE, NTILE |
+1 -1
View File
@@ -3,7 +3,7 @@
## شروع سریع
```bash
git clone https://github.com/katehonz/barabaDB.git
git clone https://codeberg.org/baraba/baradb
cd barabaDB
docker build -t baradb:latest .
+3 -3
View File
@@ -99,7 +99,7 @@ OpenSSL همراه توزیع ویندوز Nim است. برای ساخت دست
### کلون کردن مخزن
```bash
git clone https://github.com/katehonz/barabaDB.git
git clone https://codeberg.org/baraba/baradb
cd barabaDB
```
@@ -144,7 +144,7 @@ strip build/baradadb
```bash
./build/baradadb --version
# انتظار: BaraDB v0.1.0 — Multimodal Database Engine
# انتظار: BaraDB v1.1.0 — Multimodal Database Engine
```
## اجرای تست‌ها
@@ -259,7 +259,7 @@ db.close()
./build/baradadb
# خروجی مورد انتظار:
# BaraDB v0.1.0 — Multimodal Database Engine
# BaraDB v1.1.0 — Multimodal Database Engine
# BaraDB TCP listening on 127.0.0.1:9472
# تست با HTTP API
+106 -1
View File
@@ -489,4 +489,109 @@ LIMIT 10;
-- Параллельное выполнение
SELECT /*+ PARALLEL(4) */ * FROM large_table;
```
```
## Оконные функции
```sql
-- Функции ранжирования
SELECT
name,
department,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS r,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dr
FROM employees;
-- Функции значения
SELECT
name,
salary,
LAG(salary, 1, 0) OVER (ORDER BY salary) AS prev_salary,
LEAD(salary, 1, 0) OVER (ORDER BY salary) AS next_salary,
FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS cheapest,
LAST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS most_expensive
FROM employees;
-- Функции распределения
SELECT name, NTILE(4) OVER (ORDER BY salary) AS quartile FROM employees;
```
### Спецификации фрейма
```sql
-- ROWS фрейм
SUM(salary) OVER (
PARTITION BY department
ORDER BY hire_date
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
)
-- RANGE фрейм
SUM(salary) OVER (
PARTITION BY department
ORDER BY hire_date
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
```
## Мультитенантный ERP
BaraDB поддерживает работу нескольких компаний (арендаторов) в одной базе данных, используя **безопасность на уровне строк (RLS)** в сочетании с **переменными сессии**.
### Переменные сессии
```sql
SET app.tenant_id = 'company-123';
SELECT current_setting('app.tenant_id') AS tenant;
```
### Текущий пользователь / роль
```sql
SELECT current_user AS me, current_role AS my_role;
```
### Изоляция арендаторов через RLS
```sql
-- Включить RLS на таблице
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
-- Создать политику фильтрации по арендатору
CREATE POLICY tenant_isolation ON invoices
FOR SELECT USING (tenant_id = current_setting('app.tenant_id'));
-- Каждая сессия видит только свои данные
SET app.tenant_id = 'company-a';
SELECT * FROM invoices; -- только строки company-a
```
### Зачем мультитенантность?
- **Один экземпляр, много арендаторов** — не нужно запускать 100 отдельных баз данных
- **JSONB документы** — гибкая схема, легко добавлять поля для каждого арендатора
- **RLS гарантирует изоляцию** — база данных обеспечивает границы арендаторов, а не только приложение
## Поддерживаемые ключевые слова
| Категория | Ключевые слова |
|----------|----------|
| DQL | SELECT, FROM, WHERE, ORDER BY, GROUP BY, HAVING, LIMIT, OFFSET, DISTINCT |
| DML | INSERT, UPDATE, DELETE, SET, VALUES |
| DDL | CREATE TYPE, DROP TYPE, CREATE INDEX, DROP INDEX, ALTER TYPE |
| Join | INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN, CROSS JOIN, ON |
| Set | UNION, UNION ALL, INTERSECT, EXCEPT |
| CTEs | WITH, RECURSIVE, AS |
| Case | CASE, WHEN, THEN, ELSE, END |
| Transaction | BEGIN, COMMIT, ROLLBACK, SAVEPOINT |
| Graph | MATCH, RETURN, WHERE, shortestPath, type |
| FTS | MATCH, AGAINST, relevance, IN BOOLEAN MODE, WITH FUZZINESS |
| Vector | cosine_distance, euclidean_distance, inner_product, l1_distance, l2_distance, <-> |
| JSON | ->, ->> |
| FTS | @@ (BM25 match) |
| Recovery | RECOVER TO TIMESTAMP |
| Functions | count, sum, avg, min, max, stddev, variance, abs, sqrt, lower, upper, len, trim, substr, now, last_insert_id, current_setting |
| Session | SET, current_setting, current_user, current_role |
| Window | OVER, PARTITION BY, ROWS, RANGE, UNBOUNDED PRECEDING, CURRENT ROW, FOLLOWING |
| Window Functions | ROW_NUMBER, RANK, DENSE_RANK, LEAD, LAG, FIRST_VALUE, LAST_VALUE, NTILE |
+1 -1
View File
@@ -156,7 +156,7 @@ raft_peers = "node2:9001,node3:9001"
```
```
BaraDB v0.1.0 — Multimodal Database Engine
BaraDB v1.1.0 — Multimodal Database Engine
Usage:
baradadb [options]
+1 -1
View File
@@ -4,7 +4,7 @@
```bash
# Клонировать репозиторий
git clone https://github.com/katehonz/barabaDB.git
git clone https://codeberg.org/baraba/baradb
cd barabaDB
# Собрать образ
+3 -3
View File
@@ -99,7 +99,7 @@ OpenSSL поставляется вместе с Nim для Windows. Для ру
### Клонирование репозитория
```bash
git clone https://github.com/katehonz/barabaDB.git
git clone https://codeberg.org/baraba/baradb
cd barabaDB
```
@@ -144,7 +144,7 @@ strip build/baradadb
```bash
./build/baradadb --version
# Ожидается: BaraDB v0.1.0 — Multimodal Database Engine
# Ожидается: BaraDB v1.1.0 — Multimodal Database Engine
```
## Запуск тестов
@@ -259,7 +259,7 @@ db.close()
./build/baradadb
# Ожидаемый вывод:
# BaraDB v0.1.0 — Multimodal Database Engine
# BaraDB v1.1.0 — Multimodal Database Engine
# BaraDB TCP listening on 127.0.0.1:9472
# Проверка через HTTP API
+164 -5
View File
@@ -98,13 +98,172 @@ RETURN friend.name;
```sql
BEGIN;
INSERT users { name := 'Alice', age := 30 };
INSERT orders { user_id := last_insert_id(), total := 100 };
COMMIT;
-- Savepoint ile
BEGIN;
INSERT users { name := 'Bob', age := 25 };
SAVEPOINT sp1;
INSERT orders { user_id := last_insert_id(), total := 200 };
-- Hata, savepoint'e geri al
ROLLBACK TO sp1;
COMMIT;
```
## Grafik Kalıpları
## Tam Metin Arama
```sql
MATCH (p:Person)-[:KNOWS]->(friend:Person)
WHERE p.name = 'Alice'
RETURN friend.name;
```
-- Temel arama
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('database programming');
-- İlgi puanı ile
SELECT title, relevance()
FROM articles
WHERE MATCH(title, body) AGAINST('Nim language')
ORDER BY relevance() DESC;
-- Boolean modu
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('+Nim -Python' IN BOOLEAN MODE);
-- Fuzzy arama
SELECT * FROM articles
WHERE MATCH(title) AGAINST('programing' WITH FUZZINESS 2);
```
## Kullanıcı Tanımlı Fonksiyonlar
```sql
-- UDF kaydet
CREATE FUNCTION greet(name str) -> str {
RETURN 'Hello, ' || name || '!';
};
-- Kullan
SELECT greet(name) FROM users;
-- Dahili fonksiyonlar
SELECT abs(-5), sqrt(16), lower('HELLO'), len('test');
```
## Sorgu İpuçları
```sql
-- İndeks kullanımını zorla
SELECT /*+ USE_INDEX(idx_users_age) */ * FROM users WHERE age > 18;
-- Approximate vektör araması zorla
SELECT /*+ APPROXIMATE */ * FROM vectors
ORDER BY cosine_distance(embedding, [...])
LIMIT 10;
-- Paralel çalıştırma
SELECT /*+ PARALLEL(4) */ * FROM large_table;
```
## Pencere Fonksiyonları
```sql
-- Sıralama fonksiyonları
SELECT
name,
department,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS r,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dr
FROM employees;
-- Değer fonksiyonları
SELECT
name,
salary,
LAG(salary, 1, 0) OVER (ORDER BY salary) AS prev_salary,
LEAD(salary, 1, 0) OVER (ORDER BY salary) AS next_salary,
FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS cheapest,
LAST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS most_expensive
FROM employees;
-- Dağılım fonksiyonları
SELECT name, NTILE(4) OVER (ORDER BY salary) AS quartile FROM employees;
```
### Çerçeve Spesifikasyonları
```sql
-- ROWS çerçevesi
SUM(salary) OVER (
PARTITION BY department
ORDER BY hire_date
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
)
-- RANGE çerçevesi
SUM(salary) OVER (
PARTITION BY department
ORDER BY hire_date
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
```
## Çok Kiracılı ERP
BaraDB, **Satır Düzeyinde Güvenlik (RLS)** ve **oturum değişkenlerini** birleştirerek tek bir veritabanı örneğinde birden fazla şirketi (kiracı) çalıştırmayı destekler.
### Oturum Değişkenleri
```sql
SET app.tenant_id = 'company-123';
SELECT current_setting('app.tenant_id') AS tenant;
```
### Mevcut Kullanıcı / Rol
```sql
SELECT current_user AS me, current_role AS my_role;
```
### RLS Kiracı İzolasyonu
```sql
-- Tabloda RLS etkinleştir
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
-- Kiracıya göre filtreleme ilkesi oluştur
CREATE POLICY tenant_isolation ON invoices
FOR SELECT USING (tenant_id = current_setting('app.tenant_id'));
-- Her oturum yalnızca kendi verilerini görür
SET app.tenant_id = 'company-a';
SELECT * FROM invoices; -- yalnızca company-a satırları
```
### Neden Çok Kiracılı?
- **Bir örnek, çok kiracı** — 100 ayrı veritabanı çalıştırmaya gerek yok
- **JSONB belgeleri** — Esnek şema depolama, her kiracı için kolayca alan ekleme
- **RLS izolasyonu garanti eder** — Veritabanı kiracı sınırlarını uygular, yalnızca uygulama değil
## Desteklenen Anahtar Kelimeler
| Kategori | Anahtar Kelimeler |
|----------|----------|
| DQL | SELECT, FROM, WHERE, ORDER BY, GROUP BY, HAVING, LIMIT, OFFSET, DISTINCT |
| DML | INSERT, UPDATE, DELETE, SET, VALUES |
| DDL | CREATE TYPE, DROP TYPE, CREATE INDEX, DROP INDEX, ALTER TYPE |
| Join | INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN, CROSS JOIN, ON |
| Set | UNION, UNION ALL, INTERSECT, EXCEPT |
| CTEs | WITH, RECURSIVE, AS |
| Case | CASE, WHEN, THEN, ELSE, END |
| Transaction | BEGIN, COMMIT, ROLLBACK, SAVEPOINT |
| Graph | MATCH, RETURN, WHERE, shortestPath, type |
| FTS | MATCH, AGAINST, relevance, IN BOOLEAN MODE, WITH FUZZINESS |
| Vector | cosine_distance, euclidean_distance, inner_product, l1_distance, l2_distance, <-> |
| JSON | ->, ->> |
| FTS | @@ (BM25 eşleşme) |
| Recovery | RECOVER TO TIMESTAMP |
| Functions | count, sum, avg, min, max, stddev, variance, abs, sqrt, lower, upper, len, trim, substr, now, last_insert_id, current_setting |
| Session | SET, current_setting, current_user, current_role |
| Window | OVER, PARTITION BY, ROWS, RANGE, UNBOUNDED PRECEDING, CURRENT ROW, FOLLOWING |
| Window Functions | ROW_NUMBER, RANK, DENSE_RANK, LEAD, LAG, FIRST_VALUE, LAST_VALUE, NTILE |
+1 -1
View File
@@ -3,7 +3,7 @@
## Hızlı Başlangıç
```bash
git clone https://github.com/katehonz/barabaDB.git
git clone https://codeberg.org/baraba/baradb
cd barabaDB
docker build -t baradb:latest .
+247 -1
View File
@@ -22,38 +22,284 @@
### Linux
```bash
# Resmi kurulum scripti
curl https://nim-lang.org/choosenim/init.sh -sSf | sh
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install nim
# Fedora
sudo dnf install nim
# Arch Linux
sudo pacman -S nim
```
### macOS
```bash
# Homebrew
brew install nim
# MacPorts
sudo port install nim
```
### Windows
```powershell
# choosenim ile
curl.exe -A "MSYS2_$(uname -m)" -L https://nim-lang.org/choosenim/init.ps1 | powershell -
# winget ile
winget install nim
# scoop ile
scoop install nim
```
### Kurulumu Doğrulama
```bash
nim --version
# Beklenen: Nim Compiler Version 2.2.0 veya daha yeni
```
## OpenSSL Kurulumu
### Linux
```bash
# Ubuntu/Debian
sudo apt-get install libssl-dev
# Fedora
sudo dnf install openssl-devel
# Arch Linux
sudo pacman -S openssl
```
### macOS
OpenSSL sistemle birlikte gelir. Gerekirse:
```bash
brew install openssl
```
### Windows
OpenSSL, Nim Windows dağıtımıyla birlikte gelir. Manuel derlemeler için [slproweb.com](https://slproweb.com/products/Win32OpenSSL.html) adresinden indirin.
## BaraDB Derleme
### Depoyu Klonlama
```bash
git clone https://github.com/katehonz/barabaDB.git
git clone https://codeberg.org/baraba/baradb
cd barabaDB
```
### Bağımlılıkları Kurma
```bash
nimble install -d -y
```
### Derleme Seçenekleri
#### Debug Derleme
```bash
nim c -d:ssl -o:build/baradadb src/baradadb.nim
```
#### Release Derleme (Önerilen)
```bash
nim c -d:ssl -d:release --opt:speed -o:build/baradadb src/baradadb.nim
```
#### Nimble Tasks Kullanma
```bash
# Debug derleme
nimble build_debug
# Release derleme
nimble build_release
```
#### Binary Boyutunu Küçültme
```bash
nim c -d:ssl -d:release --opt:size -o:build/baradadb src/baradadb.nim
strip build/baradadb
```
### Derlemeyi Doğrulama
```bash
./build/baradadb --version
# Beklenen: BaraDB v1.1.0 — Multimodal Database Engine
```
## Testleri Çalıştırma
### Tüm Testler
```bash
nim c -d:ssl -r tests/test_all.nim
```
### Belirli Test Süitleri
```bash
# Depolama testleri
nim c -d:ssl -r tests/test_storage.nim
# Sorgu motoru testleri
nim c -d:ssl -r tests/test_query.nim
# Protokol testleri
nim c -d:ssl -r tests/test_protocol.nim
```
### Benchmarklar
```bash
nim c -d:ssl -d:release -r benchmarks/bench_all.nim
```
## Kurulum Seçenekleri
### Sistem Geneli Kurulum
```bash
# Release binary derle
nimble build_release
# /usr/local/bin'e kur
sudo cp build/baradadb /usr/local/bin/
sudo chmod +x /usr/local/bin/baradadb
# Veri dizini oluştur
sudo mkdir -p /var/lib/baradb
sudo chmod 755 /var/lib/baradb
```
### Ön Derlenmiş Binary
En son sürümü platformunuz için indirin:
```bash
# Linux x86_64
wget https://github.com/katehonz/barabaDB/releases/latest/download/baradadb-linux-amd64
chmod +x baradadb-linux-amd64
mv baradadb-linux-amd64 /usr/local/bin/baradadb
# Linux ARM64
wget https://github.com/katehonz/barabaDB/releases/latest/download/baradadb-linux-arm64
chmod +x baradadb-linux-arm64
mv baradadb-linux-arm64 /usr/local/bin/baradadb
# macOS
wget https://github.com/katehonz/barabaDB/releases/latest/download/baradadb-darwin-amd64
chmod +x baradadb-darwin-amd64
mv baradadb-darwin-amd64 /usr/local/bin/baradadb
```
### Docker
```bash
# Resmi image'i çek
docker pull barabadb/barabadb:latest
# Çalıştır
docker run -d \
-p 9472:9472 \
-p 9470:9470 \
-p 9471:9471 \
-v baradb_data:/data \
barabadb/barabadb
```
### Docker Compose
```bash
docker-compose up -d
```
### Gömülü Kullanım (Nim Projeleri)
`.nimble` dosyanıza ekleyin:
```nim
requires "barabadb >= 1.1.0"
```
Kodunuzda kullanın:
```nim
import barabadb/storage/lsm
var db = newLSMTree("./data")
db.put("key", cast[seq[byte]]("value"))
let (found, val) = db.get("key")
db.close()
```
## İlk Çalıştırma
```bash
# Sunucuyu başlat
./build/baradadb
# Beklenen çıktı:
# BaraDB v1.1.0 — Multimodal Database Engine
# BaraDB TCP listening on 127.0.0.1:9472
# HTTP API ile test et
curl http://localhost:9470/health
# İnteraktif shell
./build/baradadb --shell
```
## Kurulum Sorunlarını Giderme
### "cannot open file: hunos"
```bash
nimble install -d -y
```
### "BaraDB requires SSL support"
Her zaman `-d:ssl` ile derleyin:
```bash
nim c -d:ssl -o:build/baradadb src/baradadb.nim
```
### Yavaş derleme
Paralel derleme kullanın:
```bash
nim c -d:ssl -d:release --parallelBuild:4 -o:build/baradadb src/baradadb.nim
```
### Büyük binary boyutu
Boyut optimizasyonu kullanın:
```bash
nim c -d:ssl -d:release --opt:size --passL:-s -o:build/baradadb src/baradadb.nim
```
## Sonraki Adımlar
- [Hızlı Başlangıç](quickstart.md)
+106 -1
View File
@@ -489,4 +489,109 @@ LIMIT 10;
-- 并行执行
SELECT /*+ PARALLEL(4) */ * FROM large_table;
```
```
## 窗口函数
```sql
-- 排名函数
SELECT
name,
department,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS r,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dr
FROM employees;
-- 值函数
SELECT
name,
salary,
LAG(salary, 1, 0) OVER (ORDER BY salary) AS prev_salary,
LEAD(salary, 1, 0) OVER (ORDER BY salary) AS next_salary,
FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS cheapest,
LAST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS most_expensive
FROM employees;
-- 分布函数
SELECT name, NTILE(4) OVER (ORDER BY salary) AS quartile FROM employees;
```
### 帧规格
```sql
-- ROWS 帧
SUM(salary) OVER (
PARTITION BY department
ORDER BY hire_date
ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
)
-- RANGE 帧
SUM(salary) OVER (
PARTITION BY department
ORDER BY hire_date
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
```
## 多租户 ERP
BaraDB 支持在单个数据库实例中运行多个公司(租户),使用**行级安全性(RLS)**结合**会话变量**。
### 会话变量
```sql
SET app.tenant_id = 'company-123';
SELECT current_setting('app.tenant_id') AS tenant;
```
### 当前用户 / 角色
```sql
SELECT current_user AS me, current_role AS my_role;
```
### RLS 租户隔离
```sql
-- 在表上启用 RLS
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
-- 创建按租户过滤的策略
CREATE POLICY tenant_isolation ON invoices
FOR SELECT USING (tenant_id = current_setting('app.tenant_id'));
-- 每个会话只能看到自己的数据
SET app.tenant_id = 'company-a';
SELECT * FROM invoices; -- 仅限 company-a 的行
```
### 为什么选择多租户?
- **一个实例,多个租户** — 无需运行 100 个独立的数据库
- **JSONB 文档** — 模式灵活的存储,易于为每个租户添加字段
- **RLS 保证隔离** — 数据库强制执行租户边界,而不仅仅是应用程序
## 支持的关键字
| 类别 | 关键字 |
|----------|----------|
| DQL | SELECT, FROM, WHERE, ORDER BY, GROUP BY, HAVING, LIMIT, OFFSET, DISTINCT |
| DML | INSERT, UPDATE, DELETE, SET, VALUES |
| DDL | CREATE TYPE, DROP TYPE, CREATE INDEX, DROP INDEX, ALTER TYPE |
| Join | INNER JOIN, LEFT JOIN, RIGHT JOIN, FULL JOIN, CROSS JOIN, ON |
| Set | UNION, UNION ALL, INTERSECT, EXCEPT |
| CTEs | WITH, RECURSIVE, AS |
| Case | CASE, WHEN, THEN, ELSE, END |
| Transaction | BEGIN, COMMIT, ROLLBACK, SAVEPOINT |
| Graph | MATCH, RETURN, WHERE, shortestPath, type |
| FTS | MATCH, AGAINST, relevance, IN BOOLEAN MODE, WITH FUZZINESS |
| Vector | cosine_distance, euclidean_distance, inner_product, l1_distance, l2_distance, <-> |
| JSON | ->, ->> |
| FTS | @@ (BM25 match) |
| Recovery | RECOVER TO TIMESTAMP |
| Functions | count, sum, avg, min, max, stddev, variance, abs, sqrt, lower, upper, len, trim, substr, now, last_insert_id, current_setting |
| Session | SET, current_setting, current_user, current_role |
| Window | OVER, PARTITION BY, ROWS, RANGE, UNBOUNDED PRECEDING, CURRENT ROW, FOLLOWING |
| Window Functions | ROW_NUMBER, RANK, DENSE_RANK, LEAD, LAG, FIRST_VALUE, LAST_VALUE, NTILE |
+1 -1
View File
@@ -156,7 +156,7 @@ raft_peers = "node2:9001,node3:9001"
```
```
BaraDB v0.1.0 — Multimodal Database Engine
BaraDB v1.1.0 — Multimodal Database Engine
Usage:
baradadb [options]
+1 -1
View File
@@ -3,7 +3,7 @@
## 快速开始
```bash
git clone https://github.com/katehonz/barabaDB.git
git clone https://codeberg.org/baraba/baradb
cd barabaDB
docker build -t baradb:latest .
+3 -3
View File
@@ -99,7 +99,7 @@ OpenSSL 已捆绑在 Nim Windows 分发版中。如需手动构建,
### 克隆仓库
```bash
git clone https://github.com/katehonz/barabaDB.git
git clone https://codeberg.org/baraba/baradb
cd barabaDB
```
@@ -144,7 +144,7 @@ strip build/baradadb
```bash
./build/baradadb --version
# 预期输出: BaraDB v0.1.0 — Multimodal Database Engine
# 预期输出: BaraDB v1.1.0 — Multimodal Database Engine
```
## 运行测试
@@ -259,7 +259,7 @@ db.close()
./build/baradadb
# 预期输出:
# BaraDB v0.1.0 — Multimodal Database Engine
# BaraDB v1.1.0 — Multimodal Database Engine
# BaraDB TCP listening on 127.0.0.1:9472
# 使用 HTTP API 测试
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# BaraDB — Client Integration Test Runner
# Spins up the BaraDB server in Docker and runs all client test suites sequentially.
#
# Usage:
# ./scripts/test-clients.sh
set -euo pipefail
COMPOSE="docker compose -f docker-compose.test.yml"
echo "=== Building BaraDB server image ==="
$COMPOSE build baradb
echo "=== Starting BaraDB server ==="
$COMPOSE up -d baradb
# Wait for server healthcheck
echo "=== Waiting for server to be healthy ==="
$COMPOSE ps baradb --format json 2>/dev/null | grep -q '"Health":"healthy"' || sleep 10
# Give a little extra time for the wire protocol port to be ready
sleep 3
run_test() {
local service=$1
echo ""
echo "========================================"
echo "=== Running $service tests ==="
echo "========================================"
if $COMPOSE run --rm "$service"; then
echo "$service tests PASSED"
else
echo "$service tests FAILED"
EXIT_CODE=1
fi
}
EXIT_CODE=0
run_test test-python
run_test test-javascript
run_test test-nim
run_test test-rust
echo ""
echo "========================================"
if [ "$EXIT_CODE" -eq 0 ]; then
echo "🎉 All client tests passed!"
else
echo "⚠️ Some client tests failed."
fi
echo "========================================"
echo "=== Stopping BaraDB server ==="
$COMPOSE down -v
exit $EXIT_CODE
+6 -9
View File
@@ -4,6 +4,7 @@ import std/random
import std/monotimes
import std/asyncdispatch
import std/net
import std/asyncnet
import std/strutils
import std/streams
import std/os
@@ -36,7 +37,7 @@ type
fanout*: int # number of nodes to gossip to per round
gossipPort*: int
running*: bool
sock*: Socket
sock*: AsyncSocket
onJoin*: proc(node: GossipNode) {.gcsafe.}
onLeave*: proc(nodeId: string) {.gcsafe.}
onSuspect*: proc(nodeId: string) {.gcsafe.}
@@ -305,25 +306,21 @@ proc startGossipRound*(gp: GossipProtocol, intervalMs: int = 2000) {.async.} =
gp.broadcastGossip()
proc startGossipListener*(gp: GossipProtocol) {.async.} =
gp.sock = newSocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
gp.sock = newAsyncSocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
gp.sock.setSockOpt(OptReuseAddr, true)
gp.sock.bindAddr(Port(gp.gossipPort))
gp.running = true
while gp.running:
try:
var data = newString(65535)
var senderAddr = ""
var senderPort: Port
let bytesRead = gp.sock.recvFrom(data, 65535, senderAddr, senderPort)
if bytesRead > 0:
data.setLen(bytesRead)
let (data, senderAddr, senderPort) = await gp.sock.recvFrom(65535)
if data.len > 0:
gp.handleIncomingGossip(data, senderAddr)
except:
# Small sleep on error to avoid spin
gp.sock.close()
# Recreate socket for next iteration
try:
gp.sock = newSocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
gp.sock = newAsyncSocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
gp.sock.setSockOpt(OptReuseAddr, true)
gp.sock.bindAddr(Port(gp.gossipPort))
except:
+7 -1
View File
@@ -108,6 +108,8 @@ proc queryHandler(server: HttpServer): RequestHandler =
let ctx = newContext(request)
server.metrics.queriesTotal += 1
var userId = ""
var role = ""
# Auth check
if server.config.authEnabled:
let authHeader = request.headers["Authorization"]
@@ -115,10 +117,12 @@ proc queryHandler(server: HttpServer): RequestHandler =
ctx.json(%*{"error": "Unauthorized"}, 401)
return
let tokenStr = authHeader[7..^1]
let (valid, userId, role) = server.verifyToken(tokenStr)
let (valid, uid, r) = server.verifyToken(tokenStr)
if not valid:
ctx.json(%*{"error": "Unauthorized"}, 401)
return
userId = uid
role = r
let body = parseJson(request.body)
if body == nil or "query" notin body:
@@ -131,6 +135,8 @@ proc queryHandler(server: HttpServer): RequestHandler =
return
var reqCtx = cloneForConnection(server.ctx)
reqCtx.currentUser = userId
reqCtx.currentRole = role
let tokens = tokenize(queryStr)
let astNode = parse(tokens)
+3 -1
View File
@@ -436,9 +436,11 @@ proc handleClient(server: Server, client: AsyncSocket, clientId: int) {.async.}
case header.kind
of mkAuth:
let tokenStr = parseAuthMessage(cast[seq[byte]](payload))
let (valid, userId, _) = verifyToken(secret, tokenStr)
let (valid, userId, role) = verifyToken(secret, tokenStr)
if valid:
authenticated = true
connCtx.currentUser = userId
connCtx.currentRole = role
let okMsg = makeAuthOkMessage(header.requestId)
await client.send(cast[string](okMsg))
info("Client " & $clientId & " authenticated as " & userId)
+15 -6
View File
@@ -169,12 +169,14 @@ proc serializeValue*(buf: var seq[byte], val: WireValue) =
of fkFloat32:
var fl = val.float32Val
var bytes: array[4, byte]
copyMem(addr bytes, unsafeAddr fl, 4)
var i32 = cast[int32](fl)
bigEndian32(addr bytes, unsafeAddr i32)
buf.add(bytes)
of fkFloat64:
var fl = val.float64Val
var bytes: array[8, byte]
copyMem(addr bytes, unsafeAddr fl, 8)
var i64 = cast[int64](fl)
bigEndian64(addr bytes, unsafeAddr i64)
buf.add(bytes)
of fkString: buf.writeString(val.strVal)
of fkBytes: buf.writeBytes(val.bytesVal)
@@ -192,7 +194,8 @@ proc serializeValue*(buf: var seq[byte], val: WireValue) =
for f in val.vecVal:
var fl = f
var bytes: array[4, byte]
copyMem(addr bytes, unsafeAddr fl, 4)
var i32 = cast[int32](fl)
bigEndian32(addr bytes, unsafeAddr i32)
buf.add(bytes)
of fkJson:
buf.writeString(val.jsonVal)
@@ -227,14 +230,18 @@ proc deserializeValue*(buf: openArray[byte], pos: var int, depth: int = 0): Wire
var fl: float32
var bytes: array[4, byte]
for i in 0..3: bytes[i] = buf[pos + i]
copyMem(addr fl, unsafeAddr bytes, 4)
var i32: int32
bigEndian32(addr i32, unsafeAddr bytes)
fl = cast[float32](i32)
pos += 4
result = WireValue(kind: fkFloat32, float32Val: fl)
of fkFloat64:
var fl: float64
var bytes: array[8, byte]
for i in 0..7: bytes[i] = buf[pos + i]
copyMem(addr fl, unsafeAddr bytes, 8)
var i64: int64
bigEndian64(addr i64, unsafeAddr bytes)
fl = cast[float64](i64)
pos += 8
result = WireValue(kind: fkFloat64, float64Val: fl)
of fkString:
@@ -270,7 +277,9 @@ proc deserializeValue*(buf: openArray[byte], pos: var int, depth: int = 0): Wire
var fl: float32
var bytes: array[4, byte]
for j in 0..3: bytes[j] = buf[pos + j]
copyMem(addr fl, unsafeAddr bytes, 4)
var i32: int32
bigEndian32(addr i32, unsafeAddr bytes)
fl = cast[float32](i32)
pos += 4
vec.add(fl)
result = WireValue(kind: fkVector, vecVal: vec)
+82
View File
@@ -8,6 +8,7 @@ type
nkInsert
nkUpdate
nkDelete
nkMerge
nkCreateType
nkDropType
nkAlterType
@@ -39,6 +40,7 @@ type
nkDisableRLS
nkGrant
nkRevoke
nkSetVar
# Clauses
nkFrom
@@ -77,6 +79,8 @@ type
nkIsExpr
nkStar
nkPlaceholder
nkCurrentUser
nkCurrentRole
# Graph-specific
nkGraphTraversal
@@ -89,6 +93,10 @@ type
nkVectorSimilar
nkVectorNearest
# Pivot
nkPivot
nkUnpivot
# Set operations
nkSetOp
@@ -102,6 +110,12 @@ type
nkColumnDef
nkConstraintDef
# Window functions
nkWindowExpr
nkOverClause
nkFrameSpec
nkFrameBoundary
# Top-level
nkStatementList
@@ -132,6 +146,11 @@ type
bkJsonPath = "->"
bkJsonPathText = "->>"
bkFtsMatch = "@@"
bkDistance = "<->"
bkJsonContains = "@>"
bkJsonContainedBy = "<@"
bkJsonHasAny = "?|"
bkJsonHasAll = "?&"
UnaryOpKind* = enum
ukNeg = "-"
@@ -151,6 +170,12 @@ type
sdkIntersect
sdkExcept
GroupingSetsKind* = enum
gskNone
gskGroupingSets
gskRollup
gskCube
SortDir* = enum
sdAsc
sdDesc
@@ -168,6 +193,8 @@ type
selJoins*: seq[Node]
selWhere*: Node
selGroupBy*: seq[Node]
selGroupingSetsKind*: GroupingSetsKind
selGroupingSets*: seq[seq[Node]]
selHaving*: Node
selOrderBy*: seq[Node]
selLimit*: Node
@@ -189,6 +216,15 @@ type
delAlias*: string
delWhere*: Node
delReturning*: seq[Node]
of nkMerge:
mergeTarget*: string
mergeTargetAlias*: string
mergeSource*: Node
mergeSourceAlias*: string
mergeOn*: Node
mergeMatchedUpdate*: seq[Node] # SET assignments, empty if no UPDATE
mergeNotMatchedInsert*: seq[Node] # column list for INSERT
mergeNotMatchedValues*: seq[Node] # value expressions for INSERT
of nkCreateType:
ctName*: string
ctBases*: seq[string]
@@ -288,6 +324,9 @@ type
rvPrivilege*: string
rvTable*: string
rvGrantee*: string
of nkSetVar:
svName*: string
svValue*: string
of nkApplyMigration:
amName*: string
of nkMigrationStatus:
@@ -309,6 +348,7 @@ type
of nkFrom:
fromTable*: string
fromAlias*: string
fromSubquery*: Node
of nkWhere:
whereExpr*: Node
of nkOrderBy:
@@ -340,6 +380,7 @@ type
of nkFuncCall:
funcName*: string
funcArgs*: seq[Node]
funcFilter*: Node
of nkTypeCast:
castType*: string
castExpr*: Node
@@ -391,11 +432,13 @@ type
isType*: string
isNegated*: bool
of nkGraphTraversal:
gtGraphName*: string
gtStart*: Node
gtEdge*: string
gtDirection*: string
gtEnd*: Node
gtMaxDepth*: int
gtReturnCols*: seq[string]
of nkBfsQuery:
bfsStart*: Node
bfsTarget*: Node
@@ -426,11 +469,22 @@ type
vnVector*: Node
vnLimit*: int
vnMetric*: string
of nkPivot:
pivotSource*: Node
pivotAgg*: Node
pivotForCol*: string
pivotInValues*: seq[string]
of nkUnpivot:
unpivotSource*: Node
unpivotValueCol*: string
unpivotForCol*: string
unpivotInCols*: seq[string]
of nkJoin:
joinKind*: JoinKind
joinTarget*: Node
joinOn*: Node
joinAlias*: string
joinLateral*: bool
of nkSetOp:
setOpKind*: SetOpKind
setOpAll*: bool
@@ -440,6 +494,10 @@ type
discard
of nkPlaceholder:
discard
of nkCurrentUser:
discard
of nkCurrentRole:
discard
of nkPropertyDef:
pdName*: string
pdType*: string
@@ -456,6 +514,24 @@ type
idName*: string
idExpr*: Node
idKind*: IndexKind
of nkWindowExpr:
winFunc*: string
winArgs*: seq[Node]
winOver*: Node
of nkOverClause:
overPartition*: seq[Node]
overOrderBy*: seq[Node]
overFrame*: Node
of nkFrameSpec:
frameMode*: string # "ROWS" or "RANGE"
frameStartType*: string # "UNBOUNDED PRECEDING", "CURRENT ROW", "EXPR PRECEDING", "EXPR FOLLOWING"
frameStartExpr*: Node
frameEndType*: string
frameEndExpr*: Node
frameExclude*: string # "NO OTHERS", "CURRENT ROW", "GROUP", "TIES"
of nkFrameBoundary:
fbType*: string # "UNBOUNDED PRECEDING", "CURRENT ROW", "EXPR PRECEDING", "EXPR FOLLOWING", "UNBOUNDED FOLLOWING"
fbExpr*: Node
of nkStatementList:
stmts*: seq[Node]
@@ -470,5 +546,11 @@ proc newNode*(kind: NodeKind, line: int = 0, col: int = 0): Node =
of nkAlterTable: result.altOps = @[]
of nkColumnDef: result.cdConstraints = @[]
of nkConstraintDef: result.cstColumns = @[]; result.cstRefColumns = @[]
of nkWindowExpr: result.winArgs = @[]
of nkOverClause: result.overPartition = @[]; result.overOrderBy = @[]
of nkMerge:
result.mergeMatchedUpdate = @[]
result.mergeNotMatchedInsert = @[]
result.mergeNotMatchedValues = @[]
of nkStatementList: result.stmts = @[]
else: discard
+22
View File
@@ -194,6 +194,28 @@ proc codegenPlan*(plan: IRPlan): StorageOp =
of irpkExplain:
return codegenPlan(plan.explainPlan)
of irpkWindow:
let sourceOp = codegenPlan(plan.winSource)
let op = newStorageOp(sokProject)
for wf in plan.winFuncs:
op.columns.add(wf.wfName)
if sourceOp != nil:
op.children.add(sourceOp)
return op
of irpkMerge:
return newStorageOp(sokScan)
of irpkPivot:
let sourceOp = codegenPlan(plan.pivotSource)
let op = newStorageOp(sokScan)
if sourceOp != nil: op.children.add(sourceOp)
return op
of irpkUnpivot:
let sourceOp = codegenPlan(plan.unpivotSource)
let op = newStorageOp(sokScan)
if sourceOp != nil: op.children.add(sourceOp)
return op
of irpkGraphTraversal:
return newStorageOp(sokScan)
proc estimateCost*(op: StorageOp): float64 =
if op == nil:
File diff suppressed because it is too large Load Diff
+83
View File
@@ -1,5 +1,6 @@
## BaraQL IR — Intermediate Representation for compilation
import std/tables
import std/strutils
import ../core/types
type
@@ -27,9 +28,15 @@ type
irBetween
irIsNull, irIsNotNull
irFtsMatch
irDistance
irJsonContains
irJsonContainedBy
irJsonHasAny
irJsonHasAll
IRAggregate* = enum
irCount, irSum, irAvg, irMin, irMax
irArrayAgg, irStringAgg
IRLiteral* = object
case kind*: ValueKind
@@ -52,6 +59,7 @@ type
irekExists
irekStar
irekJsonPath
irekWindowFunc
IRJoinKind* = enum
irjkInner
@@ -71,11 +79,22 @@ type
irpkInsert
irpkUpdate
irpkDelete
irpkMerge
irpkCreateType
irpkUnion
irpkCTE
irpkValues
irpkExplain
irpkWindow
irpkPivot
irpkUnpivot
irpkGraphTraversal
IRGroupingSetsKind* = enum
irgskNone
irgskGroupingSets
irgskRollup
irgskCube
IRPlan* = ref object
case kind*: IRPlanKind
@@ -94,12 +113,15 @@ type
groupKeys*: seq[IRExpr]
groupAggs*: seq[IRExpr]
groupHaving*: IRExpr
groupingSetsKind*: IRGroupingSetsKind
groupingSets*: seq[seq[IRExpr]]
of irpkJoin:
joinKind*: IRJoinKind
joinLeft*: IRPlan
joinRight*: IRPlan
joinCond*: IRExpr
joinAlias*: string
joinLateral*: bool
of irpkSort:
sortSource*: IRPlan
sortExprs*: seq[IRExpr]
@@ -121,6 +143,14 @@ type
deleteTable*: string
deleteAlias*: string
deleteSource*: IRPlan
of irpkMerge:
mergeTarget*: string
mergeTargetAlias*: string
mergeSourcePlan*: IRPlan
mergeOnCond*: IRExpr
mergeUpdateSets*: seq[(string, IRExpr)]
mergeInsertFields*: seq[string]
mergeInsertValues*: seq[seq[IRExpr]]
of irpkCreateType:
createTypeName*: string
createTypeDef*: IRType
@@ -136,6 +166,34 @@ type
valuesRows*: seq[seq[IRExpr]]
of irpkExplain:
explainPlan*: IRPlan
of irpkWindow:
winSource*: IRPlan
winFuncs*: seq[IRExpr]
winPartition*: seq[IRExpr]
winOrderBy*: seq[IRExpr]
winOrderDirs*: seq[bool]
winFrameMode*: string
winFrameStart*: string
winFrameEnd*: string
of irpkPivot:
pivotSource*: IRPlan
pivotAgg*: IRExpr
pivotForCol*: string
pivotInValues*: seq[string]
of irpkUnpivot:
unpivotSource*: IRPlan
unpivotValueCol*: string
unpivotForCol*: string
unpivotInCols*: seq[string]
of irpkGraphTraversal:
graphName*: string
graphAlgo*: string # "bfs", "dfs", "shortest", "pagerank"
graphStartNode*: string
graphEndNode*: string
graphEdgeLabel*: string
graphMaxDepth*: int
graphFilter*: IRExpr
graphReturnCols*: seq[string]
IRExpr* = ref object
case kind*: IRExprKind
@@ -154,6 +212,7 @@ type
aggOp*: IRAggregate
aggArgs*: seq[IRExpr]
aggDistinct*: bool
aggFilter*: IRExpr
of irekFuncCall:
irFunc*: string
irFuncArgs*: seq[IRExpr]
@@ -172,6 +231,15 @@ type
jpExpr*: IRExpr
jpKey*: string
jpAsText*: bool
of irekWindowFunc:
wfName*: string
wfArgs*: seq[IRExpr]
wfPartition*: seq[IRExpr]
wfOrderBy*: seq[IRExpr]
wfOrderDirs*: seq[bool]
wfFrameMode*: string
wfFrameStart*: string
wfFrameEnd*: string
type
TypeChecker* = ref object
@@ -241,6 +309,10 @@ proc inferExpr*(tc: TypeChecker, expr: IRExpr, context: Table[string, IRType]):
if expr.aggArgs.len > 0:
return tc.inferExpr(expr.aggArgs[0], context)
return nil
of irArrayAgg:
return IRType(name: "array", kind: itkArray)
of irStringAgg:
return IRType(name: "text", kind: itkScalar)
of irekFuncCall:
return IRType(name: "unknown", kind: itkScalar)
of irekCast:
@@ -255,3 +327,14 @@ proc inferExpr*(tc: TypeChecker, expr: IRExpr, context: Table[string, IRType]):
of irekJsonPath:
if expr.jpAsText: return IRType(name: "str", kind: itkScalar)
return IRType(name: "json", kind: itkScalar)
of irekWindowFunc:
# Window functions return int64 for ranking, or the type of the argument for value functions
case expr.wfName.toLower()
of "row_number", "rank", "dense_rank", "ntile":
return IRType(name: "int64", kind: itkScalar)
of "lead", "lag", "first_value", "last_value":
if expr.wfArgs.len > 0:
return tc.inferExpr(expr.wfArgs[0], context)
return nil
else:
return IRType(name: "unknown", kind: itkScalar)
+111
View File
@@ -35,6 +35,7 @@ type
tkOuter
tkFull
tkCross
tkLateral
tkOrder
tkBy
tkAsc
@@ -77,6 +78,7 @@ type
tkBetween
tkLike
tkILike
tkFilter
tkReturning
tkPrimary
tkKey
@@ -120,6 +122,24 @@ type
tkAvg
tkMin
tkMax
tkArrayAgg
tkStringAgg
tkGrouping
tkSets
tkRollup
tkCube
tkPivot
tkUnpivot
tkVertex
tkEdge
tkLabels
tkGraphTable
tkMatch
tkColumns
tkSrc
tkDst
tkMerge
tkMatched
tkArray
tkVector
tkGraph
@@ -127,6 +147,10 @@ type
tkArrowR # ->
tkArrowRR # ->>
tkFtsMatch # @@
tkJsonContains # @>
tkJsonContainedBy # <@
tkJsonHasAny # ?|
tkJsonHasAll # ?&
tkSimilar
tkNearest
tkTo
@@ -134,6 +158,27 @@ type
tkDfs
tkPath
# Window functions
tkOver
tkPartition
tkRow
tkRows
tkRange
tkUnbounded
tkPreceding
tkFollowing
tkCurrent
tkCurrentUser
tkCurrentRole
tkRowNumber
tkRank
tkDenseRank
tkLead
tkLag
tkFirstValue
tkLastValue
tkNtile
# Operators
tkPlus
tkMinus
@@ -166,6 +211,7 @@ type
tkConcat
tkCoalesce
tkFloorDiv
tkDistanceOp # <->
tkPlaceholder
# Special
@@ -206,6 +252,7 @@ const keywords*: Table[string, TokenKind] = {
"outer": tkOuter,
"full": tkFull,
"cross": tkCross,
"lateral": tkLateral,
"order": tkOrder,
"by": tkBy,
"asc": tkAsc,
@@ -248,6 +295,7 @@ const keywords*: Table[string, TokenKind] = {
"between": tkBetween,
"like": tkLike,
"ilike": tkILike,
"filter": tkFilter,
"returning": tkReturning,
"primary": tkPrimary,
"key": tkKey,
@@ -291,6 +339,27 @@ const keywords*: Table[string, TokenKind] = {
"avg": tkAvg,
"min": tkMin,
"max": tkMax,
"array_agg": tkArrayAgg,
"string_agg": tkStringAgg,
"grouping": tkGrouping,
"sets": tkSets,
"rollup": tkRollup,
"cube": tkCube,
"pivot": tkPivot,
"unpivot": tkUnpivot,
"vertex": tkVertex,
"vertices": tkVertex,
"edge": tkEdge,
"edges": tkEdge,
"label": tkLabels,
"labels": tkLabels,
"graph_table": tkGraphTable,
"match": tkMatch,
"columns": tkColumns,
"src": tkSrc,
"dst": tkDst,
"merge": tkMerge,
"matched": tkMatched,
"array": tkArray,
"vector": tkVector,
"graph": tkGraph,
@@ -301,6 +370,25 @@ const keywords*: Table[string, TokenKind] = {
"bfs": tkBfs,
"dfs": tkDfs,
"path": tkPath,
"over": tkOver,
"partition": tkPartition,
"row": tkRow,
"rows": tkRows,
"range": tkRange,
"unbounded": tkUnbounded,
"preceding": tkPreceding,
"following": tkFollowing,
"current": tkCurrent,
"current_user": tkCurrentUser,
"current_role": tkCurrentRole,
"row_number": tkRowNumber,
"rank": tkRank,
"dense_rank": tkDenseRank,
"lead": tkLead,
"lag": tkLag,
"first_value": tkFirstValue,
"last_value": tkLastValue,
"ntile": tkNtile,
}.toTable
proc newLexer*(input: string): Lexer =
@@ -495,6 +583,11 @@ proc nextToken*(l: var Lexer): Token =
discard l.advance()
return Token(kind: tkInvalid, value: "!", line: startLine, col: startCol)
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] == '=':
discard l.advance()
discard l.advance()
@@ -503,6 +596,10 @@ proc nextToken*(l: var Lexer): Token =
discard l.advance()
discard l.advance()
return Token(kind: tkNotEq, value: "<>", line: startLine, col: startCol)
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '@':
discard l.advance()
discard l.advance()
return Token(kind: tkJsonContainedBy, value: "<@", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkLt, value: "<", line: startLine, col: startCol)
of '>':
@@ -513,6 +610,16 @@ proc nextToken*(l: var Lexer): Token =
discard l.advance()
return Token(kind: tkGt, value: ">", line: startLine, col: startCol)
of '?':
if l.pos + 2 < l.input.len and l.input[l.pos + 1] == '|':
discard l.advance()
discard l.advance()
discard l.advance()
return Token(kind: tkJsonHasAny, value: "?|", line: startLine, col: startCol)
if l.pos + 2 < l.input.len and l.input[l.pos + 1] == '&':
discard l.advance()
discard l.advance()
discard l.advance()
return Token(kind: tkJsonHasAll, value: "?&", line: startLine, col: startCol)
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '?':
discard l.advance()
discard l.advance()
@@ -524,6 +631,10 @@ proc nextToken*(l: var Lexer): Token =
discard l.advance()
discard l.advance()
return Token(kind: tkFtsMatch, value: "@@", line: startLine, col: startCol)
if l.pos + 1 < l.input.len and l.input[l.pos + 1] == '>':
discard l.advance()
discard l.advance()
return Token(kind: tkJsonContains, value: "@>", line: startLine, col: startCol)
discard l.advance()
return Token(kind: tkInvalid, value: "@", line: startLine, col: startCol)
of '.':
+390 -24
View File
@@ -38,6 +38,10 @@ proc parseExpr(p: var Parser): Node
proc parseSelect(p: var Parser): Node
proc parseStatement*(p: var Parser): Node
proc parseCreateType(p: var Parser): Node
proc parseOverClause(p: var Parser): Node
proc parseFrameSpec(p: var Parser): Node
proc parseFrameBoundary(p: var Parser): string
proc parseSetVar(p: var Parser): Node
proc parsePrimary(p: var Parser): Node =
let tok = p.peek()
@@ -57,8 +61,15 @@ proc parsePrimary(p: var Parser): Node =
of tkNull:
discard p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
of tkIdent:
of tkCurrentUser:
discard p.advance()
Node(kind: nkCurrentUser, line: tok.line, col: tok.col)
of tkCurrentRole:
discard p.advance()
Node(kind: nkCurrentRole, line: tok.line, col: tok.col)
of tkIdent, tkRowNumber, tkRank, tkDenseRank, tkLead, tkLag, tkFirstValue, tkLastValue, tkNtile:
discard p.advance()
let funcName = tok.value
# Check for function call: ident(...)
if p.peek().kind == tkLParen:
discard p.advance() # consume (
@@ -68,15 +79,21 @@ proc parsePrimary(p: var Parser): Node =
while p.match(tkComma):
args.add(p.parseExpr())
discard p.expect(tkRParen)
return Node(kind: nkFuncCall, funcName: tok.value, funcArgs: args,
line: tok.line, col: tok.col)
var funcNode = Node(kind: nkFuncCall, funcName: funcName, funcArgs: args,
line: tok.line, col: tok.col)
# Check for window function: func(...) OVER (...)
if p.peek().kind == tkOver:
let overClause = p.parseOverClause()
return Node(kind: nkWindowExpr, winFunc: funcName, winArgs: args,
winOver: overClause, line: tok.line, col: tok.col)
return funcNode
# Check for dotted path: ident.ident.ident
var parts = @[tok.value]
var parts = @[funcName]
while p.peek().kind == tkDot:
discard p.advance() # consume .
parts.add(p.expect(tkIdent).value)
if parts.len == 1:
return Node(kind: nkIdent, identName: tok.value, line: tok.line, col: tok.col)
return Node(kind: nkIdent, identName: funcName, line: tok.line, col: tok.col)
return Node(kind: nkPath, pathParts: parts, line: tok.line, col: tok.col)
of tkLParen:
discard p.advance()
@@ -119,7 +136,7 @@ proc parsePrimary(p: var Parser): Node =
discard p.advance()
let operand = p.parsePrimary()
Node(kind: nkUnaryOp, unOp: ukNeg, unOperand: operand, line: tok.line, col: tok.col)
of tkCount, tkSum, tkAvg, tkMin, tkMax:
of tkCount, tkSum, tkAvg, tkMin, tkMax, tkArrayAgg, tkStringAgg:
let funcName = tok.value
discard p.advance()
discard p.expect(tkLParen)
@@ -131,9 +148,17 @@ proc parsePrimary(p: var Parser): Node =
hasDistinct = true
if p.peek().kind != tkRParen:
args.add(p.parseExpr())
while p.match(tkComma):
args.add(p.parseExpr())
discard p.expect(tkRParen)
var node = Node(kind: nkFuncCall, funcName: funcName.toLower(), funcArgs: args,
line: tok.line, col: tok.col)
# Handle FILTER (WHERE ...)
if p.match(tkFilter):
discard p.expect(tkLParen)
discard p.expect(tkWhere)
node.funcFilter = p.parseExpr()
discard p.expect(tkRParen)
return node
of tkCase:
discard p.advance()
@@ -157,6 +182,80 @@ proc parsePrimary(p: var Parser): Node =
discard p.advance()
Node(kind: nkNullLit, line: tok.line, col: tok.col)
proc parseOverClause(p: var Parser): Node =
## Parse OVER ( [PARTITION BY expr, ...] [ORDER BY expr [ASC|DESC], ...] [frame] )
discard p.expect(tkOver)
discard p.expect(tkLParen)
result = Node(kind: nkOverClause)
# PARTITION BY
if p.match(tkPartition):
discard p.expect(tkBy)
result.overPartition.add(p.parseExpr())
while p.match(tkComma):
result.overPartition.add(p.parseExpr())
# ORDER BY
if p.match(tkOrder):
discard p.expect(tkBy)
var expr = p.parseExpr()
var dir = sdAsc
if p.match(tkDesc):
dir = sdDesc
elif p.match(tkAsc):
dir = sdAsc
result.overOrderBy.add(Node(kind: nkOrderBy, orderByExpr: expr, orderByDir: dir))
while p.match(tkComma):
expr = p.parseExpr()
dir = sdAsc
if p.match(tkDesc):
dir = sdDesc
elif p.match(tkAsc):
dir = sdAsc
result.overOrderBy.add(Node(kind: nkOrderBy, orderByExpr: expr, orderByDir: dir))
# Frame specification
if p.peek().kind in {tkRows, tkRange}:
result.overFrame = p.parseFrameSpec()
discard p.expect(tkRParen)
proc parseFrameSpec(p: var Parser): Node =
## Parse ROWS|RANGE frame
let modeTok = p.advance() # tkRows or tkRange
result = Node(kind: nkFrameSpec, frameMode: modeTok.value)
if p.match(tkBetween):
result.frameStartType = p.parseFrameBoundary()
discard p.expect(tkAnd)
result.frameEndType = p.parseFrameBoundary()
else:
result.frameStartType = p.parseFrameBoundary()
result.frameEndType = "CURRENT ROW"
proc parseFrameBoundary(p: var Parser): string =
## Returns boundary type string like "UNBOUNDED PRECEDING", "CURRENT ROW", "1 PRECEDING"
if p.match(tkUnbounded):
if p.match(tkPreceding):
return "UNBOUNDED PRECEDING"
elif p.match(tkFollowing):
return "UNBOUNDED FOLLOWING"
else:
raise newException(ValueError, "Expected PRECEDING or FOLLOWING after UNBOUNDED")
elif p.match(tkCurrent):
discard p.expect(tkRow)
return "CURRENT ROW"
else:
# Expect a simple integer literal for offset boundaries
let offsetExpr = p.parsePrimary()
var offsetStr = ""
case offsetExpr.kind:
of nkIntLit: offsetStr = $offsetExpr.intVal
of nkIdent: offsetStr = offsetExpr.identName
else:
raise newException(ValueError, "Expected integer literal or identifier in frame boundary, got " & $offsetExpr.kind)
if p.match(tkPreceding):
return offsetStr & " PRECEDING"
elif p.match(tkFollowing):
return offsetStr & " FOLLOWING"
else:
raise newException(ValueError, "Expected PRECEDING or FOLLOWING after frame offset")
proc parsePostfix(p: var Parser): Node =
result = p.parsePrimary()
while p.peek().kind in {tkArrowR, tkArrowRR}:
@@ -226,7 +325,7 @@ proc parseComparison(p: var Parser): Node =
discard p.advance() # consume NULL token (assumed)
return Node(kind: nkIsExpr, isExpr: result, isNegated: negated,
line: tok.line, col: tok.col)
while p.peek().kind in {tkEq, tkNotEq, tkLt, tkLtEq, tkGt, tkGtEq, tkFtsMatch}:
while p.peek().kind in {tkEq, tkNotEq, tkLt, tkLtEq, tkGt, tkGtEq, tkFtsMatch, tkDistanceOp, tkJsonContains, tkJsonContainedBy, tkJsonHasAny, tkJsonHasAll}:
let op = case p.peek().kind
of tkEq: bkEq
of tkNotEq: bkNotEq
@@ -235,6 +334,11 @@ proc parseComparison(p: var Parser): Node =
of tkGt: bkGt
of tkGtEq: bkGtEq
of tkFtsMatch: bkFtsMatch
of tkDistanceOp: bkDistance
of tkJsonContains: bkJsonContains
of tkJsonContainedBy: bkJsonContainedBy
of tkJsonHasAny: bkJsonHasAny
of tkJsonHasAll: bkJsonHasAll
else: bkEq
let tok = p.advance()
let right = p.parseAddSub()
@@ -354,7 +458,66 @@ proc parseSelect(p: var Parser): Node =
elif p.peek().kind == tkIdent:
alias = p.advance().value
result.selFrom = Node(kind: nkFrom, fromTable: "(subquery)",
fromAlias: alias, line: tok.line, col: tok.col)
fromAlias: alias, fromSubquery: subquery, line: tok.line, col: tok.col)
elif p.peek().kind == tkGraphTable:
# GRAPH_TABLE(name MATCH (pattern) COLUMNS (cols))
discard p.advance()
discard p.expect(tkLParen)
let graphName = p.expect(tkIdent).value
discard p.expect(tkMatch)
# Parse pattern: (node)-[edge]->(node)
var patternNodes: seq[string]
var patternEdges: seq[string]
# First node
discard p.expect(tkLParen)
if p.peek().kind == tkIdent:
patternNodes.add(p.advance().value)
discard p.expect(tkRParen)
# Edge and next node(s)
while p.peek().kind == tkMinus or p.peek().kind == tkArrowR:
if p.match(tkArrowR):
discard
elif p.match(tkMinus):
if p.peek().kind == tkLBracket:
discard p.advance()
if p.peek().kind == tkIdent:
patternEdges.add(p.advance().value)
discard p.expect(tkRBracket)
discard p.expect(tkArrowR)
else:
discard
if p.peek().kind == tkLParen:
discard p.advance()
if p.peek().kind == tkIdent:
patternNodes.add(p.advance().value)
discard p.expect(tkRParen)
# COLUMNS (col1, col2, ...)
var returnCols: seq[string]
if p.match(tkColumns):
discard p.expect(tkLParen)
if p.peek().kind == tkIdent:
var colName = p.advance().value
# Handle dotted names: e.name
while p.peek().kind == tkDot:
discard p.advance() # skip dot
colName &= "." & p.expect(tkIdent).value
returnCols.add(colName)
while p.match(tkComma):
if p.peek().kind == tkIdent:
colName = p.advance().value
while p.peek().kind == tkDot:
discard p.advance()
colName &= "." & p.expect(tkIdent).value
returnCols.add(colName)
if p.match(tkAs):
discard p.advance() # skip alias
discard p.expect(tkRParen)
discard p.expect(tkRParen)
# Create a graph traversal node
result.selFrom = Node(kind: nkGraphTraversal, gtGraphName: graphName,
gtStart: nil, gtEdge: if patternEdges.len > 0: patternEdges[0] else: "",
gtDirection: "out", gtEnd: nil, gtMaxDepth: -1,
gtReturnCols: returnCols, line: tok.line, col: tok.col)
else:
let tableTok = p.expect(tkIdent)
var alias = ""
@@ -365,26 +528,89 @@ proc parseSelect(p: var Parser): Node =
result.selFrom = Node(kind: nkFrom, fromTable: tableTok.value,
fromAlias: alias, line: tableTok.line, col: tableTok.col)
# Parse PIVOT / UNPIVOT after FROM source
if p.peek().kind == tkPivot:
discard p.advance()
discard p.expect(tkLParen)
let aggFunc = p.parseExpr() # e.g. SUM(salary)
discard p.expect(tkFor)
let forCol = p.expect(tkIdent).value
discard p.expect(tkIn)
discard p.expect(tkLParen)
var inValues: seq[string] = @[]
inValues.add(p.expect(tkStringLit).value)
while p.match(tkComma):
inValues.add(p.expect(tkStringLit).value)
discard p.expect(tkRParen)
discard p.expect(tkRParen)
result.selFrom = Node(kind: nkPivot, pivotSource: result.selFrom,
pivotAgg: aggFunc, pivotForCol: forCol,
pivotInValues: inValues, line: tok.line, col: tok.col)
elif p.peek().kind == tkUnpivot:
discard p.advance()
discard p.expect(tkLParen)
let valCol = p.expect(tkIdent).value
discard p.expect(tkFor)
let forCol = p.expect(tkIdent).value
discard p.expect(tkIn)
discard p.expect(tkLParen)
var inCols: seq[string] = @[]
inCols.add(p.expect(tkIdent).value)
while p.match(tkComma):
inCols.add(p.expect(tkIdent).value)
discard p.expect(tkRParen)
discard p.expect(tkRParen)
result.selFrom = Node(kind: nkUnpivot, unpivotSource: result.selFrom,
unpivotValueCol: valCol, unpivotForCol: forCol,
unpivotInCols: inCols, line: tok.line, col: tok.col)
# Parse JOINs
while p.peek().kind == tkJoin or
p.peek().kind == tkLateral or
(p.peek().kind in {tkInner, tkLeft, tkRight, tkFull, tkCross} and
p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind == tkJoin):
let jk = p.parseJoinType()
discard p.expect(tkJoin)
let joinTable = p.expect(tkIdent)
p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind in {tkJoin, tkLateral}):
var isLateral = false
var jk = jkInner
# Check for LATERAL before join type
if p.match(tkLateral):
isLateral = true
# Could be standalone LATERAL or LATERAL JOIN
if p.peek().kind == tkJoin:
discard p.advance()
else:
jk = p.parseJoinType()
discard p.expect(tkJoin)
# Check for LATERAL after JOIN keyword
if p.match(tkLateral):
isLateral = true
var joinTarget: Node
var joinAlias = ""
if p.match(tkAs):
joinAlias = p.expect(tkIdent).value
elif p.peek().kind == tkIdent:
joinAlias = p.advance().value
if isLateral:
# LATERAL (subquery) AS alias
discard p.expect(tkLParen)
let subquery = p.parseSelect()
discard p.expect(tkRParen)
if p.match(tkAs):
joinAlias = p.expect(tkIdent).value
elif p.peek().kind == tkIdent:
joinAlias = p.advance().value
joinTarget = Node(kind: nkSubquery, subQuery: subquery,
line: tok.line, col: tok.col)
else:
let joinTable = p.expect(tkIdent)
if p.match(tkAs):
joinAlias = p.expect(tkIdent).value
elif p.peek().kind == tkIdent:
joinAlias = p.advance().value
joinTarget = Node(kind: nkFrom, fromTable: joinTable.value,
fromAlias: joinAlias, line: joinTable.line, col: joinTable.col)
var joinCond: Node = nil
if p.match(tkOn):
joinCond = p.parseExpr()
let joinTarget = Node(kind: nkFrom, fromTable: joinTable.value,
fromAlias: joinAlias, line: joinTable.line, col: joinTable.col)
result.selJoins.add(Node(kind: nkJoin, joinKind: jk, joinTarget: joinTarget,
joinOn: joinCond, joinAlias: joinAlias,
line: joinTable.line, col: joinTable.col))
joinOn: joinCond, joinAlias: joinAlias, joinLateral: isLateral,
line: tok.line, col: tok.col))
# Parse WHERE
if p.match(tkWhere):
@@ -394,9 +620,47 @@ proc parseSelect(p: var Parser): Node =
if p.match(tkGroup):
discard p.expect(tkBy)
result.selGroupBy = @[]
result.selGroupBy.add(p.parseExpr())
while p.match(tkComma):
result.selGroupingSetsKind = gskNone
result.selGroupingSets = @[]
# Check for GROUPING SETS, ROLLUP, CUBE
if p.peek().kind == tkGrouping:
discard p.advance()
discard p.expect(tkSets)
result.selGroupingSetsKind = gskGroupingSets
discard p.expect(tkLParen)
# Parse each set: (col1, col2) or ()
while true:
discard p.expect(tkLParen)
var setExprs: seq[Node] = @[]
if p.peek().kind != tkRParen:
setExprs.add(p.parseExpr())
while p.match(tkComma):
setExprs.add(p.parseExpr())
discard p.expect(tkRParen)
result.selGroupingSets.add(setExprs)
if not p.match(tkComma): break
discard p.expect(tkRParen)
elif p.peek().kind == tkRollup:
discard p.advance()
result.selGroupingSetsKind = gskRollup
discard p.expect(tkLParen)
result.selGroupBy.add(p.parseExpr())
while p.match(tkComma):
result.selGroupBy.add(p.parseExpr())
discard p.expect(tkRParen)
elif p.peek().kind == tkCube:
discard p.advance()
result.selGroupingSetsKind = gskCube
discard p.expect(tkLParen)
result.selGroupBy.add(p.parseExpr())
while p.match(tkComma):
result.selGroupBy.add(p.parseExpr())
discard p.expect(tkRParen)
else:
# Regular GROUP BY
result.selGroupBy.add(p.parseExpr())
while p.match(tkComma):
result.selGroupBy.add(p.parseExpr())
# Parse HAVING
if p.match(tkHaving):
@@ -540,6 +804,64 @@ proc parseDelete(p: var Parser): Node =
while p.match(tkComma):
result.delReturning.add(p.parseExpr())
proc parseMerge(p: var Parser): Node =
let tok = p.expect(tkMerge)
discard p.match(tkInto) # optional INTO
result = Node(kind: nkMerge, line: tok.line, col: tok.col)
result.mergeTarget = p.expect(tkIdent).value
if p.match(tkAs):
result.mergeTargetAlias = p.expect(tkIdent).value
elif p.peek().kind == tkIdent:
result.mergeTargetAlias = p.advance().value
discard p.expect(tkUsing)
# Source can be a table name or a subquery
if p.peek().kind == tkLParen:
discard p.advance() # consume (
result.mergeSource = p.parseSelect()
discard p.expect(tkRParen)
else:
let srcTable = p.expect(tkIdent).value
result.mergeSource = Node(kind: nkIdent, identName: srcTable, line: tok.line, col: tok.col)
if p.match(tkAs):
result.mergeSourceAlias = p.expect(tkIdent).value
elif p.peek().kind == tkIdent:
result.mergeSourceAlias = p.advance().value
discard p.expect(tkOn)
result.mergeOn = p.parseExpr()
# WHEN MATCHED THEN UPDATE SET ...
if p.match(tkWhen):
if p.match(tkNot):
discard p.expect(tkMatched)
discard p.expect(tkThen)
discard p.expect(tkInsert)
discard p.expect(tkLParen)
result.mergeNotMatchedInsert.add(Node(kind: nkIdent, identName: p.expect(tkIdent).value))
while p.match(tkComma):
result.mergeNotMatchedInsert.add(Node(kind: nkIdent, identName: p.expect(tkIdent).value))
discard p.expect(tkRParen)
discard p.expect(tkValues)
discard p.expect(tkLParen)
result.mergeNotMatchedValues.add(p.parseExpr())
while p.match(tkComma):
result.mergeNotMatchedValues.add(p.parseExpr())
discard p.expect(tkRParen)
else:
discard p.expect(tkMatched)
discard p.expect(tkThen)
discard p.expect(tkUpdate)
discard p.expect(tkSet)
let col = p.expect(tkIdent).value
discard p.expect(tkEq)
result.mergeMatchedUpdate.add(Node(kind: nkBinOp, binOp: bkAssign,
binLeft: Node(kind: nkIdent, identName: col),
binRight: p.parseExpr()))
while p.match(tkComma):
let col2 = p.expect(tkIdent).value
discard p.expect(tkEq)
result.mergeMatchedUpdate.add(Node(kind: nkBinOp, binOp: bkAssign,
binLeft: Node(kind: nkIdent, identName: col2),
binRight: p.parseExpr()))
proc parseCreateType(p: var Parser): Node =
let tok = p.expect(tkCreate)
discard p.expect(tkType)
@@ -672,6 +994,14 @@ proc parseCreateTable(p: var Parser): Node =
let size = p.expect(tkIntLit).value
colType &= "(" & size & ")"
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)
colDef.cdConstraints = @[]
@@ -737,13 +1067,13 @@ proc parseAlterTable(p: var Parser): Node =
# Check for ENABLE/DISABLE ROW LEVEL SECURITY
if p.peek().kind == tkEnable:
discard p.advance()
discard p.expect(tkIdent) # ROW
discard p.expect(tkRow) # ROW
discard p.expect(tkIdent) # LEVEL
discard p.expect(tkIdent) # SECURITY
return Node(kind: nkEnableRLS, erlsTable: tableName, line: tok.line, col: tok.col)
elif p.peek().kind == tkDisable:
discard p.advance()
discard p.expect(tkIdent) # ROW
discard p.expect(tkRow) # ROW
discard p.expect(tkIdent) # LEVEL
discard p.expect(tkIdent) # SECURITY
return Node(kind: nkDisableRLS, drlsTable: tableName, line: tok.line, col: tok.col)
@@ -781,6 +1111,10 @@ proc parseCreateIndex(p: var Parser): Node =
let idxMethod = p.expect(tkIdent).value.toLower()
if idxMethod == "fts" or idxMethod == "fulltext":
idxKind = ikFullText
elif idxMethod == "hnsw":
idxKind = ikHNSW
elif idxMethod == "ivfpq":
idxKind = ikIVFPQ
result = Node(kind: nkCreateIndex, ciName: idxName, ciTarget: tableName,
ciColumns: colNames, ciKind: idxKind, line: tok.line, col: tok.col)
@@ -1099,12 +1433,42 @@ proc parseRevoke(p: var Parser): Node =
result = Node(kind: nkRevoke, rvPrivilege: priv, rvTable: tableName,
rvGrantee: grantee, line: tok.line, col: tok.col)
proc parseSetVar(p: var Parser): Node =
let tok = p.expect(tkSet)
var varName = p.expect(tkIdent).value
while p.peek().kind == tkDot:
discard p.advance()
varName.add(".")
varName.add(p.expect(tkIdent).value)
if p.match(tkEq) or p.match(tkTo):
discard
let valTok = p.peek()
var valStr = ""
case valTok.kind
of tkStringLit:
valStr = valTok.value
discard p.advance()
of tkIntLit:
valStr = valTok.value
discard p.advance()
of tkFloatLit:
valStr = valTok.value
discard p.advance()
of tkIdent:
valStr = valTok.value
discard p.advance()
else:
raise newException(ValueError, "Expected value after SET " & varName)
result = Node(kind: nkSetVar, svName: varName, svValue: valStr,
line: tok.line, col: tok.col)
proc parseStatement*(p: var Parser): Node =
case p.peek().kind
of tkWith, tkSelect: p.parseSelect()
of tkInsert: p.parseInsert()
of tkUpdate: p.parseUpdate()
of tkDelete: p.parseDelete()
of tkMerge: p.parseMerge()
of tkCreate:
if p.pos + 1 < p.tokens.len:
let next = p.tokens[p.pos + 1]
@@ -1159,6 +1523,8 @@ proc parseStatement*(p: var Parser): Node =
p.parseGrant()
of tkRevoke:
p.parseRevoke()
of tkSet:
p.parseSetVar()
of tkApply:
if p.pos + 1 < p.tokens.len and p.tokens[p.pos + 1].kind == tkMigration:
p.parseApplyMigration()
+29
View File
@@ -79,3 +79,32 @@ suite "JOIN execution":
test "count after CROSS JOIN":
let r = execSql(ctx, "SELECT COUNT(*) AS cnt FROM users u CROSS JOIN orders o")
check r.rows[0]["cnt"] == "6"
test "LATERAL JOIN with correlated subquery":
let r = execSql(ctx,
"SELECT u.name, recent.total FROM users u JOIN LATERAL (SELECT o.total FROM orders o WHERE o.user_id = u.id ORDER BY o.total DESC LIMIT 1) AS recent ON 1=1")
check r.rows.len == 1
check r.rows[0]["name"] == "Alice"
check r.rows[0]["total"] == "99.5"
test "LATERAL JOIN returns no rows when subquery empty":
let r = execSql(ctx,
"SELECT u.name, x.total FROM users u JOIN LATERAL (SELECT o.total FROM orders o WHERE o.user_id = u.id AND o.total > 1000) AS x ON 1=1")
check r.rows.len == 0
test "LEFT LATERAL JOIN keeps unmatched rows":
let r = execSql(ctx,
"SELECT u.name, x.total FROM users u LEFT JOIN LATERAL (SELECT o.total FROM orders o WHERE o.user_id = u.id ORDER BY o.total DESC LIMIT 1) AS x ON 1=1")
check r.rows.len == 2
# Alice has match (99.5), Bob has no orders -> NULL
var foundBob = false
for row in r.rows:
if row["name"] == "Bob":
check row["total"] == ""
foundBob = true
check foundBob
test "CROSS JOIN LATERAL":
let r = execSql(ctx,
"SELECT u.name, x.total FROM users u CROSS JOIN LATERAL (SELECT o.total FROM orders o WHERE o.user_id = u.id) AS x")
check r.rows.len == 2 # Alice has 2 orders, Bob has 0
+526 -9
View File
@@ -969,8 +969,40 @@ suite "BaraQL Parser — Extended":
test "Parse GROUP BY with HAVING":
let ast = parse("SELECT dept, count(*) FROM employees GROUP BY dept HAVING count(*) > 5")
check ast.stmts[0].selGroupBy.len == 1
check ast.stmts[0].selHaving != nil
test "Parse FILTER clause":
let ast = parse("SELECT COUNT(*) FILTER (WHERE active = true) FROM users")
check ast.stmts[0].selResult.len == 1
check ast.stmts[0].selResult[0].funcFilter != nil
test "Parse ROLLUP":
let ast = parse("SELECT dept, SUM(amount) FROM sales GROUP BY ROLLUP (dept)")
check ast.stmts[0].selGroupingSetsKind == gskRollup
check ast.stmts[0].selGroupBy.len == 1
test "Parse CUBE":
let ast = parse("SELECT dept, job, SUM(amount) FROM sales GROUP BY CUBE (dept, job)")
check ast.stmts[0].selGroupingSetsKind == gskCube
check ast.stmts[0].selGroupBy.len == 2
test "Parse GROUPING SETS":
let ast = parse("SELECT dept, job, SUM(amount) FROM sales GROUP BY GROUPING SETS ((dept), (job), ())")
check ast.stmts[0].selGroupingSetsKind == gskGroupingSets
check ast.stmts[0].selGroupingSets.len == 3
test "Parse PIVOT":
let ast = parse("SELECT * FROM (SELECT dept, salary FROM emp) PIVOT (SUM(salary) FOR dept IN ('Eng', 'Sales'))")
check ast.stmts[0].selFrom.kind == nkPivot
check ast.stmts[0].selFrom.pivotForCol == "dept"
check ast.stmts[0].selFrom.pivotInValues.len == 2
test "Parse GRAPH_TABLE":
let ast = parse("SELECT * FROM GRAPH_TABLE(org_chart MATCH (e)-[r]->(d) COLUMNS (e.name, d.name))")
check ast.stmts[0].selFrom.kind == nkGraphTraversal
check ast.stmts[0].selFrom.gtGraphName == "org_chart"
test "Parse ORDER BY with direction":
let ast = parse("SELECT name FROM users ORDER BY age DESC")
check ast.stmts[0].selOrderBy.len == 1
@@ -994,6 +1026,25 @@ suite "BaraQL Parser — Extended":
let ast = parse("SELECT * FROM a JOIN b ON a.id = b.aid JOIN c ON b.id = c.bid")
check ast.stmts[0].selJoins.len == 2
test "Parse LATERAL JOIN":
let ast = parse("SELECT u.name, x.total FROM users u JOIN LATERAL (SELECT o.total FROM orders o WHERE o.user_id = u.id) AS x ON 1=1")
check ast.stmts[0].selJoins.len == 1
check ast.stmts[0].selJoins[0].joinLateral == true
check ast.stmts[0].selJoins[0].joinTarget.kind == nkSubquery
check ast.stmts[0].selJoins[0].joinAlias == "x"
test "Parse LEFT LATERAL JOIN":
let ast = parse("SELECT u.name FROM users u LEFT JOIN LATERAL (SELECT 1) AS x ON 1=1")
check ast.stmts[0].selJoins.len == 1
check ast.stmts[0].selJoins[0].joinKind == jkLeft
check ast.stmts[0].selJoins[0].joinLateral == true
test "Parse CROSS JOIN LATERAL":
let ast = parse("SELECT * FROM users u CROSS JOIN LATERAL (SELECT 1) AS x")
check ast.stmts[0].selJoins.len == 1
check ast.stmts[0].selJoins[0].joinKind == jkCross
check ast.stmts[0].selJoins[0].joinLateral == true
test "Parse CTE (WITH)":
let ast = parse("WITH active AS (SELECT * FROM users WHERE active = true) SELECT * FROM active")
check ast.stmts[0].selWith.len == 1
@@ -2236,6 +2287,69 @@ suite "Row-Level Security":
check res.success
check res.rows.len == 1 # superuser sees all (only 1 row exists)
suite "Session Variables and Multi-Tenant":
test "SET statement parse":
let ast = parse("SET app.tenant_id = 'company-123'")
check ast.stmts.len == 1
check ast.stmts[0].kind == nkSetVar
check ast.stmts[0].svName == "app.tenant_id"
check ast.stmts[0].svValue == "company-123"
test "SET and current_setting":
let tmpDir = getTempDir() / "baradb_session_test_" & $getMonoTime().ticks
var db = newLSMTree(tmpDir)
var ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("SET app.tenant_id = 'company-123'"))
let r = qexec.executeQuery(ctx, parse("SELECT current_setting('app.tenant_id') AS tenant"))
check r.success
check r.rows.len == 1
check r.rows[0]["tenant"] == "company-123"
test "current_user in SELECT":
let tmpDir = getTempDir() / "baradb_user_test_" & $getMonoTime().ticks
var db = newLSMTree(tmpDir)
var ctx = qexec.newExecutionContext(db)
ctx.currentUser = "alice"
let r = qexec.executeQuery(ctx, parse("SELECT current_user AS me"))
check r.success
check r.rows.len == 1
check r.rows[0]["me"] == "alice"
test "current_role in SELECT":
let tmpDir = getTempDir() / "baradb_role_test_" & $getMonoTime().ticks
var db = newLSMTree(tmpDir)
var ctx = qexec.newExecutionContext(db)
ctx.currentRole = "admin"
let r = qexec.executeQuery(ctx, parse("SELECT current_role AS role"))
check r.success
check r.rows.len == 1
check r.rows[0]["role"] == "admin"
test "Multi-tenant RLS with current_setting":
let tmpDir = getTempDir() / "baradb_multitenant_test_" & $getMonoTime().ticks
var db = newLSMTree(tmpDir)
var ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE TABLE invoices (id INTEGER, tenant_id TEXT, amount INT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO invoices (id, tenant_id, amount) VALUES (1, 'company-a', 100)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO invoices (id, tenant_id, amount) VALUES (2, 'company-b', 200)"))
# Set session variable for tenant
discard qexec.executeQuery(ctx, parse("SET app.tenant_id = 'company-a'"))
# Create policy that uses current_setting
ctx.users["app"] = qexec.UserDef(name: "app", passwordHash: "", isSuperuser: false, roles: @[])
ctx.currentUser = "app"
ctx.policies["invoices"] = @[
qexec.PolicyDef(name: "tenant_isolation", tableName: "invoices", command: "SELECT",
usingExpr: Node(kind: nkBinOp, binOp: bkEq,
binLeft: Node(kind: nkIdent, identName: "tenant_id"),
binRight: Node(kind: nkFuncCall, funcName: "current_setting",
funcArgs: @[Node(kind: nkStringLit, strVal: "app.tenant_id")])),
withCheckExpr: nil)
]
let r = qexec.executeQuery(ctx, parse("SELECT id, tenant_id FROM invoices"))
check r.success
check r.rows.len == 1
check r.rows[0]["tenant_id"] == "company-a"
suite "UTF-8 Support":
test "Tokenize UTF-8 identifiers":
let tokens = lex.tokenize("SELECT имя FROM потребители")
@@ -2252,7 +2366,8 @@ suite "UTF-8 Support":
check ast.stmts[0].selWhere.whereExpr.binRight.strVal == "София"
test "Execute query with UTF-8 data":
var db = newLSMTree("")
let tmpDir = getTempDir() / "baradb_test_" & $getMonoTime().ticks
var db = newLSMTree(tmpDir)
var ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE TABLE потребители (имя TEXT, град TEXT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO потребители (имя, град) VALUES ('Иван', 'София'), ('Мария', 'Пловдив')"))
@@ -2264,7 +2379,8 @@ suite "UTF-8 Support":
suite "B-Tree Range Scan":
test "BETWEEN uses index range scan":
var db = newLSMTree("")
let tmpDir = getTempDir() / "baradb_test_" & $getMonoTime().ticks
var db = newLSMTree(tmpDir)
var ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE TABLE products (id INTEGER, name TEXT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO products (id, name) VALUES (1, 'apple'), (2, 'banana'), (3, 'cherry'), (4, 'date'), (5, 'elderberry')"))
@@ -2274,7 +2390,8 @@ suite "B-Tree Range Scan":
check res.rows.len == 3
test "Greater than uses index range scan":
var db = newLSMTree("")
let tmpDir = getTempDir() / "baradb_test_" & $getMonoTime().ticks
var db = newLSMTree(tmpDir)
var ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE TABLE nums (id INTEGER, val TEXT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO nums (id, val) VALUES (1, '10'), (2, '20'), (3, '30'), (4, '40'), (5, '50')"))
@@ -2284,7 +2401,8 @@ suite "B-Tree Range Scan":
check res.rows.len == 3
test "Less than or equal uses index range scan":
var db = newLSMTree("")
let tmpDir = getTempDir() / "baradb_test_" & $getMonoTime().ticks
var db = newLSMTree(tmpDir)
var ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE TABLE nums2 (id INTEGER, val TEXT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO nums2 (id, val) VALUES (1, '10'), (2, '20'), (3, '30'), (4, '40'), (5, '50')"))
@@ -2407,13 +2525,18 @@ suite "Enhanced Migrations":
suite "Parameterized queries":
var db: LSMTree
var ctx: qexec.ExecutionContext
var tmpDir: string
var paramInitialized = false
setup:
db = newLSMTree("")
ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE TABLE users (id INT, name TEXT, age INT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO users (id, name, age) VALUES (2, 'Bob', 25)"))
if not paramInitialized:
tmpDir = getTempDir() / "baradb_param_test_" & $getMonoTime().ticks
db = newLSMTree(tmpDir)
ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE TABLE users (id INT, name TEXT, age INT, active BOOLEAN)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO users (id, name, age, active) VALUES (1, 'Alice', 30, true)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO users (id, name, age, active) VALUES (2, 'Bob', 25, false)"))
paramInitialized = true
test "SELECT with placeholder params":
let sql = "SELECT * FROM users WHERE id = ?"
@@ -2532,6 +2655,34 @@ suite "Parameterized queries":
check r.success
check r.rows.len >= 1
test "JSON contains @>":
discard qexec.executeQuery(ctx, parse("CREATE TABLE IF NOT EXISTS jsontest2 (id INT PRIMARY KEY, data JSON)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO jsontest2 (id, data) VALUES (1, '{\"name\": \"Alice\", \"age\": 30}')"))
let r = qexec.executeQuery(ctx, parse("SELECT id FROM jsontest2 WHERE data @> '{\"name\": \"Alice\"}'"))
check r.success
check r.rows.len == 1
check r.rows[0]["id"] == "1"
test "JSON contained by <@":
let r = qexec.executeQuery(ctx, parse("SELECT id FROM jsontest2 WHERE '{\"name\": \"Alice\"}' <@ data"))
check r.success
check r.rows.len == 1
test "JSON has key json_has_key":
let r = qexec.executeQuery(ctx, parse("SELECT id FROM jsontest2 WHERE json_has_key(data, 'name') = 'true'"))
check r.success
check r.rows.len == 1
test "JSON has any key ?|":
let r = qexec.executeQuery(ctx, parse("SELECT id FROM jsontest2 WHERE data ?| '[\"name\", \"missing\"]'"))
check r.success
check r.rows.len == 1
test "JSON has all keys ?&":
let r = qexec.executeQuery(ctx, parse("SELECT id FROM jsontest2 WHERE data ?& '[\"name\", \"age\"]'"))
check r.success
check r.rows.len == 1
test "FTS match operator @@ parse":
let ast = parse("SELECT * FROM docs WHERE content @@ 'hello'")
check ast.stmts[0].kind == nkSelect
@@ -2576,8 +2727,374 @@ suite "Parameterized queries":
check r.rows.len == 1
check r.rows[0]["name"] == "Bob"
suite "Window Functions":
var db: LSMTree
var ctx: qexec.ExecutionContext
var tmpDir: string
setup:
tmpDir = getTempDir() / "baradb_window_test_" & $getMonoTime().ticks
db = newLSMTree(tmpDir)
ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE TABLE employees (id INT, name TEXT, department TEXT, salary INT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO employees (id, name, department, salary) VALUES (1, 'Alice', 'Engineering', 90000)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO employees (id, name, department, salary) VALUES (2, 'Bob', 'Engineering', 80000)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO employees (id, name, department, salary) VALUES (3, 'Charlie', 'Sales', 70000)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO employees (id, name, department, salary) VALUES (4, 'Diana', 'Sales', 75000)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO employees (id, name, department, salary) VALUES (5, 'Eve', 'Engineering', 95000)"))
teardown:
removeDir(tmpDir)
test "ROW_NUMBER with PARTITION BY and ORDER BY":
let r = qexec.executeQuery(ctx, parse("SELECT name, department, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn FROM employees"))
check r.success
check r.rows.len == 5
# Engineering: Eve(95000)=1, Alice(90000)=2, Bob(80000)=3
# Sales: Diana(75000)=1, Charlie(70000)=2
var found = initTable[string, string]()
for row in r.rows:
found[row["name"]] = row["rn"]
check found["Eve"] == "1"
check found["Alice"] == "2"
check found["Bob"] == "3"
check found["Diana"] == "1"
check found["Charlie"] == "2"
test "RANK and DENSE_RANK":
let r = qexec.executeQuery(ctx, parse("SELECT name, salary, RANK() OVER (ORDER BY salary DESC) AS r, DENSE_RANK() OVER (ORDER BY salary DESC) AS dr FROM employees"))
check r.success
check r.rows.len == 5
for row in r.rows:
if row["name"] == "Eve":
check row["r"] == "1"
check row["dr"] == "1"
if row["name"] == "Alice":
check row["r"] == "2"
check row["dr"] == "2"
test "LEAD and LAG":
let r = qexec.executeQuery(ctx, parse("SELECT name, salary, LAG(salary, 1, 0) OVER (ORDER BY salary) AS prev, LEAD(salary, 1, 0) OVER (ORDER BY salary) AS next FROM employees"))
check r.success
check r.rows.len == 5
for row in r.rows:
if row["name"] == "Charlie":
check row["prev"] == "0"
check row["next"] == "75000"
if row["name"] == "Diana":
check row["prev"] == "70000"
check row["next"] == "80000"
test "FIRST_VALUE and LAST_VALUE":
let r = qexec.executeQuery(ctx, parse("SELECT department, FIRST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS first, LAST_VALUE(name) OVER (PARTITION BY department ORDER BY salary) AS last FROM employees"))
check r.success
check r.rows.len == 5
test "NTILE":
let r = qexec.executeQuery(ctx, parse("SELECT name, NTILE(2) OVER (ORDER BY salary) AS bucket FROM employees"))
check r.success
check r.rows.len == 5
for row in r.rows:
check row["bucket"] in @["1", "2"]
test "FIRST_VALUE with frame":
let r = qexec.executeQuery(ctx, parse("SELECT name, salary, FIRST_VALUE(salary) OVER (ORDER BY salary ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS first_sal FROM employees"))
check r.success
check r.rows.len == 5
for row in r.rows:
if row["name"] == "Charlie":
check row["first_sal"] == "70000"
if row["name"] == "Diana":
check row["first_sal"] == "70000"
if row["name"] == "Bob":
check row["first_sal"] == "75000"
test "LAST_VALUE with frame":
let r = qexec.executeQuery(ctx, parse("SELECT name, salary, LAST_VALUE(salary) OVER (ORDER BY salary ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING) AS last_sal FROM employees"))
check r.success
check r.rows.len == 5
for row in r.rows:
if row["name"] == "Charlie":
check row["last_sal"] == "75000"
if row["name"] == "Diana":
check row["last_sal"] == "80000"
if row["name"] == "Bob":
check row["last_sal"] == "90000"
suite "GROUP BY Aggregates":
var db: LSMTree
var ctx: qexec.ExecutionContext
var tmpDir: string
setup:
tmpDir = getTempDir() / "baradb_group_test_" & $getMonoTime().ticks
db = newLSMTree(tmpDir)
ctx = qexec.newExecutionContext(db)
discard qexec.executeQuery(ctx, parse("CREATE TABLE sales (id INT, dept TEXT, amount INT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO sales (id, dept, amount) VALUES (1, 'A', 100)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO sales (id, dept, amount) VALUES (2, 'A', 200)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO sales (id, dept, amount) VALUES (3, 'B', 150)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO sales (id, dept, amount) VALUES (4, 'B', 50)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO sales (id, dept, amount) VALUES (5, 'C', 300)"))
teardown:
removeDir(tmpDir)
test "GROUP BY with COUNT(*)":
let r = qexec.executeQuery(ctx, parse("SELECT dept, COUNT(*) AS cnt FROM sales GROUP BY dept"))
check r.success
check r.rows.len == 3
test "GROUP BY with SUM":
let r = qexec.executeQuery(ctx, parse("SELECT dept, SUM(amount) AS total FROM sales GROUP BY dept"))
check r.success
check r.rows.len == 3
var foundA = false
for row in r.rows:
if row["dept"] == "A":
check row["total"] == "300.0"
foundA = true
check foundA
test "GROUP BY with AVG":
let r = qexec.executeQuery(ctx, parse("SELECT dept, AVG(amount) AS avg_amt FROM sales GROUP BY dept"))
check r.success
check r.rows.len == 3
var foundB = false
for row in r.rows:
if row["dept"] == "B":
check row["avg_amt"] == "100.0"
foundB = true
check foundB
test "GROUP BY with MIN and MAX":
let r = qexec.executeQuery(ctx, parse("SELECT dept, MIN(amount) AS lo, MAX(amount) AS hi FROM sales GROUP BY dept"))
check r.success
check r.rows.len == 3
var foundA = false
for row in r.rows:
if row["dept"] == "A":
check row["lo"] == "100"
check row["hi"] == "200"
foundA = true
check foundA
test "GROUP BY with HAVING":
let r = qexec.executeQuery(ctx, parse("SELECT dept, SUM(amount) AS total FROM sales GROUP BY dept HAVING SUM(amount) > 200"))
check r.success
check r.rows.len == 2 # A (300) and C (300)
for row in r.rows:
check row["dept"] in @["A", "C"]
test "GROUP BY with multiple aggregates":
let r = qexec.executeQuery(ctx, parse("SELECT dept, COUNT(*) AS cnt, SUM(amount) AS total, AVG(amount) AS avg_amt FROM sales GROUP BY dept"))
check r.success
check r.rows.len == 3
test "COUNT with FILTER (WHERE ...)":
let r = qexec.executeQuery(ctx, parse("SELECT COUNT(*) AS total, COUNT(*) FILTER (WHERE amount > 100) AS big FROM sales"))
check r.success
check r.rows.len == 1
check r.rows[0]["total"] == "5"
check r.rows[0]["big"] == "3"
test "SUM with FILTER (WHERE ...)":
let r = qexec.executeQuery(ctx, parse("SELECT dept, SUM(amount) FILTER (WHERE amount > 100) AS big_total FROM sales GROUP BY dept"))
check r.success
check r.rows.len == 3
var foundA = false
for row in r.rows:
if row["dept"] == "A":
check row["big_total"] == "200.0"
foundA = true
check foundA
test "ARRAY_AGG":
let r = qexec.executeQuery(ctx, parse("SELECT dept, ARRAY_AGG(amount) AS amounts FROM sales GROUP BY dept"))
check r.success
check r.rows.len == 3
var foundA = false
for row in r.rows:
if row["dept"] == "A":
check "100" in row["amounts"]
check "200" in row["amounts"]
foundA = true
check foundA
test "STRING_AGG":
let r = qexec.executeQuery(ctx, parse("SELECT dept, STRING_AGG(amount, ',') AS vals FROM sales GROUP BY dept"))
check r.success
check r.rows.len == 3
test "ROLLUP":
let r = qexec.executeQuery(ctx, parse("SELECT dept, SUM(amount) AS total FROM sales GROUP BY ROLLUP (dept)"))
check r.success
# 3 dept groups + 1 grand total = 4 rows
check r.rows.len == 4
test "PIVOT":
discard qexec.executeQuery(ctx, parse("CREATE TABLE emp (name TEXT, dept TEXT, salary INT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO emp (name, dept, salary) VALUES ('Alice', 'Eng', 90000)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO emp (name, dept, salary) VALUES ('Bob', 'Eng', 80000)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO emp (name, dept, salary) VALUES ('Charlie', 'Sales', 70000)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO emp (name, dept, salary) VALUES ('Diana', 'Sales', 75000)"))
let r = qexec.executeQuery(ctx, parse("SELECT * FROM (SELECT name, dept, salary FROM emp) PIVOT (SUM(salary) FOR dept IN ('Eng', 'Sales'))"))
check r.success
check r.rows.len == 4 # one row per employee
test "CUBE execution":
let r = qexec.executeQuery(ctx, parse("SELECT dept, SUM(amount) AS total FROM sales GROUP BY CUBE (dept)"))
check r.success
# 3 dept groups + 1 grand total = 4 rows
check r.rows.len == 4
test "GROUPING SETS execution":
let r = qexec.executeQuery(ctx, parse("SELECT dept, SUM(amount) AS total FROM sales GROUP BY GROUPING SETS ((dept), ())"))
check r.success
# 3 dept groups + 1 grand total = 4 rows
check r.rows.len == 4
test "UNPIVOT execution":
discard qexec.executeQuery(ctx, parse("CREATE TABLE pivoted (name TEXT, eng_salary INT, sales_salary INT)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO pivoted (name, eng_salary, sales_salary) VALUES ('Alice', 90000, 0)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO pivoted (name, eng_salary, sales_salary) VALUES ('Bob', 0, 70000)"))
let r = qexec.executeQuery(ctx, parse("SELECT * FROM pivoted UNPIVOT (salary FOR dept IN (eng_salary, sales_salary))"))
check r.success
check r.rows.len == 4
# JOIN tests
include "join_tests"
# TLA+ faithfulness tests
include "tla_faithfulness"
suite "MERGE Statement":
var db: LSMTree
var ctx: qexec.ExecutionContext
var tmpDir: string
setup:
tmpDir = getTempDir() / "baradb_merge_test_" & $getMonoTime().ticks
db = newLSMTree(tmpDir)
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("INSERT INTO inventory (id, sku, qty) VALUES (1, 'SKU001', 100)"))
discard qexec.executeQuery(ctx, parse("INSERT INTO inventory (id, sku, qty) VALUES (2, 'SKU002', 200)"))
discard qexec.executeQuery(ctx, parse("CREATE TABLE updates (sku TEXT, delta INT)"))
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)"))
teardown:
removeDir(tmpDir)
test "MERGE WHEN MATCHED UPDATE":
let r = qexec.executeQuery(ctx, parse("""
MERGE INTO inventory AS target
USING updates AS source
ON target.sku = source.sku
WHEN MATCHED THEN UPDATE SET qty = target.qty + source.delta
"""))
check r.success
check r.affectedRows == 1
let verify = qexec.executeQuery(ctx, parse("SELECT * FROM inventory WHERE sku = 'SKU001'"))
check verify.rows[0]["qty"] == "150.0"
test "MERGE WHEN NOT MATCHED INSERT":
let r = qexec.executeQuery(ctx, parse("""
MERGE INTO inventory AS target
USING updates AS source
ON target.sku = source.sku
WHEN NOT MATCHED THEN INSERT (id, sku, qty) VALUES (3, source.sku, source.delta)
"""))
check r.success
check r.affectedRows == 1
let verify = qexec.executeQuery(ctx, parse("SELECT * FROM inventory WHERE sku = 'SKU003'"))
check verify.rows.len == 1
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"