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
This commit is contained in:
2026-05-14 23:05:47 +03:00
parent f7d4961125
commit c55d3080cf
48 changed files with 5792 additions and 544 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ 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:
+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://github.com/katehonz/barabaDB.git
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())
+4 -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/katehonz/barabaDB"
documentation = "https://docs.baradb.dev"
readme = "README.md"
keywords = ["database", "baradb", "multimodal", "client", "wire-protocol"]
@@ -13,9 +13,10 @@ categories = ["database", "network-programming"]
rust-version = "1.70"
[dependencies]
tokio = { version = "1.35", features = ["full"] }
[dev-dependencies]
[[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://github.com/katehonz/barabaDB.git
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);
}
}
}
}
+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,
}
}
}