feat(clients): professional clients with tests, CI/CD, and examples
- Python: pyproject.toml, pytest suite (unit + integration), README, examples - JavaScript: package.json, TypeScript definitions, node:test suite, README, examples - Nim: integration tests, examples, README, improved nimble tasks - Rust: fixed nodelay/localhost IPv6 bug, integration tests, examples, README - Added GitHub Actions CI for all 4 clients against Docker server - Added docker-compose.test.yml for local integration testing - Added .gitignore for each client
This commit is contained in:
@@ -0,0 +1,185 @@
|
|||||||
|
name: Clients CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'clients/**'
|
||||||
|
- 'src/**'
|
||||||
|
- 'Dockerfile'
|
||||||
|
- '.github/workflows/clients-ci.yml'
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'clients/**'
|
||||||
|
- 'src/**'
|
||||||
|
- 'Dockerfile'
|
||||||
|
- '.github/workflows/clients-ci.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-server:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: docker build -t baradb:latest .
|
||||||
|
|
||||||
|
- name: Save Docker image
|
||||||
|
run: docker save baradb:latest | gzip > baradb-image.tar.gz
|
||||||
|
|
||||||
|
- name: Upload image artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: baradb-image
|
||||||
|
path: baradb-image.tar.gz
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
test-python:
|
||||||
|
needs: build-server
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download Docker image
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: baradb-image
|
||||||
|
|
||||||
|
- name: Load Docker image
|
||||||
|
run: docker load < baradb-image.tar.gz
|
||||||
|
|
||||||
|
- name: Start BaraDB server
|
||||||
|
run: |
|
||||||
|
docker run -d --name baradb-ci -p 9472:9472 baradb:latest
|
||||||
|
sleep 3
|
||||||
|
docker logs baradb-ci
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install Python client
|
||||||
|
working-directory: clients/python
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install pytest
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
- name: Run Python unit tests
|
||||||
|
working-directory: clients/python
|
||||||
|
run: pytest tests/test_wire_protocol.py tests/test_query_builder.py -v
|
||||||
|
|
||||||
|
- name: Run Python integration tests
|
||||||
|
working-directory: clients/python
|
||||||
|
run: pytest tests/test_integration.py -v
|
||||||
|
|
||||||
|
- name: Run Python example
|
||||||
|
working-directory: clients/python
|
||||||
|
run: python examples/basic.py || true
|
||||||
|
|
||||||
|
test-javascript:
|
||||||
|
needs: build-server
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download Docker image
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: baradb-image
|
||||||
|
|
||||||
|
- name: Load Docker image
|
||||||
|
run: docker load < baradb-image.tar.gz
|
||||||
|
|
||||||
|
- name: Start BaraDB server
|
||||||
|
run: |
|
||||||
|
docker run -d --name baradb-ci -p 9472:9472 baradb:latest
|
||||||
|
sleep 3
|
||||||
|
docker logs baradb-ci
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Run JavaScript unit tests
|
||||||
|
working-directory: clients/javascript
|
||||||
|
run: node --test tests/unit.test.js
|
||||||
|
|
||||||
|
- name: Run JavaScript integration tests
|
||||||
|
working-directory: clients/javascript
|
||||||
|
run: node --test tests/integration.test.js
|
||||||
|
|
||||||
|
- name: Run JavaScript example
|
||||||
|
working-directory: clients/javascript
|
||||||
|
run: node examples/basic.js || true
|
||||||
|
|
||||||
|
test-nim:
|
||||||
|
needs: build-server
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download Docker image
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: baradb-image
|
||||||
|
|
||||||
|
- name: Load Docker image
|
||||||
|
run: docker load < baradb-image.tar.gz
|
||||||
|
|
||||||
|
- name: Start BaraDB server
|
||||||
|
run: |
|
||||||
|
docker run -d --name baradb-ci -p 9472:9472 baradb:latest
|
||||||
|
sleep 3
|
||||||
|
docker logs baradb-ci
|
||||||
|
|
||||||
|
- name: Setup Nim
|
||||||
|
uses: jiro4989/setup-nim-action@v1
|
||||||
|
with:
|
||||||
|
nim-version: '2.2.10'
|
||||||
|
|
||||||
|
- name: Run Nim unit tests
|
||||||
|
working-directory: clients/nim
|
||||||
|
run: nim c --path:src -r tests/test_client.nim
|
||||||
|
|
||||||
|
- name: Run Nim integration tests
|
||||||
|
working-directory: clients/nim
|
||||||
|
run: nim c --path:src -r tests/test_integration.nim
|
||||||
|
|
||||||
|
- name: Run Nim example
|
||||||
|
working-directory: clients/nim
|
||||||
|
run: nim c --path:src -r examples/basic.nim || true
|
||||||
|
|
||||||
|
test-rust:
|
||||||
|
needs: build-server
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download Docker image
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: baradb-image
|
||||||
|
|
||||||
|
- name: Load Docker image
|
||||||
|
run: docker load < baradb-image.tar.gz
|
||||||
|
|
||||||
|
- name: Start BaraDB server
|
||||||
|
run: |
|
||||||
|
docker run -d --name baradb-ci -p 9472:9472 baradb:latest
|
||||||
|
sleep 3
|
||||||
|
docker logs baradb-ci
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
uses: dtolnay/rust-action@stable
|
||||||
|
|
||||||
|
- name: Run Rust tests
|
||||||
|
working-directory: clients/rust
|
||||||
|
run: cargo test -- --test-threads=1
|
||||||
|
|
||||||
|
- name: Run Rust example
|
||||||
|
working-directory: clients/rust
|
||||||
|
run: cargo run --example basic || true
|
||||||
@@ -37,3 +37,4 @@ clients/rust/target/
|
|||||||
# Nim compiled binaries
|
# Nim compiled binaries
|
||||||
clients/nim/src/baradb/client
|
clients/nim/src/baradb/client
|
||||||
src/barabadb/storage/compaction
|
src/barabadb/storage/compaction
|
||||||
|
docker-compose.test.yml
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
package-lock.json
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
# BaraDB JavaScript / Node.js Client
|
||||||
|
|
||||||
|
Official JavaScript client for **BaraDB** — a multimodal database engine written in Nim.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Binary wire protocol** — fast TCP communication
|
||||||
|
- **Promise-based API** — modern async/await
|
||||||
|
- **Query builder** — fluent SQL construction
|
||||||
|
- **Parameterized queries** — safe from SQL injection
|
||||||
|
- **Vector & JSON support** — first-class multimodal types
|
||||||
|
- **TypeScript definitions** — included out of the box
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install baradb
|
||||||
|
```
|
||||||
|
|
||||||
|
Or from source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/barabadb/baradadb.git
|
||||||
|
cd clients/javascript
|
||||||
|
npm link
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { Client } = require('baradb');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const client = new Client('localhost', 9472);
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
const result = await client.query("SELECT name, age FROM users WHERE age > 18");
|
||||||
|
for (const row of result) {
|
||||||
|
console.log(row.name, row.age);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameterized Queries
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { Client, WireValue } = require('baradb');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const client = new Client('localhost', 9472);
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
const result = await client.queryParams(
|
||||||
|
'SELECT * FROM users WHERE age > $1 AND country = $2',
|
||||||
|
[WireValue.int64(18), WireValue.string('BG')]
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const row of result) {
|
||||||
|
console.log(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.close();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Builder
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { Client, QueryBuilder } = require('baradb');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const client = new Client('localhost', 9472);
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
const result = await new QueryBuilder(client)
|
||||||
|
.select('name', 'email')
|
||||||
|
.from('users')
|
||||||
|
.where('active = true')
|
||||||
|
.orderBy('name')
|
||||||
|
.limit(10)
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
for (const row of result) {
|
||||||
|
console.log(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.close();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vector Search
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { Client, WireValue } = require('baradb');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const client = new Client('localhost', 9472);
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
const result = await client.queryParams(
|
||||||
|
'SELECT id, name FROM products ORDER BY embedding <-> $1 LIMIT 5',
|
||||||
|
[WireValue.vector([0.1, 0.2, 0.3])]
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.close();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
Unit tests (no server):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run test:unit
|
||||||
|
```
|
||||||
|
|
||||||
|
Integration tests (requires server on `localhost:9472`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start server
|
||||||
|
docker run -d -p 9472:9472 baradb:latest
|
||||||
|
|
||||||
|
# Run integration tests
|
||||||
|
npm run test:integration
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### `new Client(host, port, options)`
|
||||||
|
|
||||||
|
| Option | Default | Description |
|
||||||
|
|--------------|-------------|----------------------------|
|
||||||
|
| `host` | `localhost` | Server hostname |
|
||||||
|
| `port` | `9472` | TCP wire protocol port |
|
||||||
|
| `database` | `default` | Default database |
|
||||||
|
| `username` | `admin` | Username |
|
||||||
|
| `password` | `""` | Password |
|
||||||
|
| `timeout` | `30000` | Socket timeout in ms |
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
- `connect()` — open TCP connection
|
||||||
|
- `close()` — close connection
|
||||||
|
- `query(sql)` — execute SELECT-like query
|
||||||
|
- `queryParams(sql, params)` — parameterized query
|
||||||
|
- `execute(sql)` — execute DDL/DML, returns affected rows
|
||||||
|
- `auth(token)` — JWT authentication
|
||||||
|
- `ping()` — health check
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache-2.0
|
||||||
Vendored
+116
@@ -0,0 +1,116 @@
|
|||||||
|
// BaraDB JavaScript/TypeScript Client Type Definitions
|
||||||
|
|
||||||
|
export declare const MsgKind: Readonly<{
|
||||||
|
CLIENT_HANDSHAKE: 0x01;
|
||||||
|
QUERY: 0x02;
|
||||||
|
QUERY_PARAMS: 0x03;
|
||||||
|
EXECUTE: 0x04;
|
||||||
|
BATCH: 0x05;
|
||||||
|
TRANSACTION: 0x06;
|
||||||
|
CLOSE: 0x07;
|
||||||
|
PING: 0x08;
|
||||||
|
AUTH: 0x09;
|
||||||
|
SERVER_HANDSHAKE: 0x80;
|
||||||
|
READY: 0x81;
|
||||||
|
DATA: 0x82;
|
||||||
|
COMPLETE: 0x83;
|
||||||
|
ERROR: 0x84;
|
||||||
|
AUTH_CHALLENGE: 0x85;
|
||||||
|
AUTH_OK: 0x86;
|
||||||
|
SCHEMA_CHANGE: 0x87;
|
||||||
|
PONG: 0x88;
|
||||||
|
TRANSACTION_STATE: 0x89;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export declare const FieldKind: Readonly<{
|
||||||
|
NULL: 0x00;
|
||||||
|
BOOL: 0x01;
|
||||||
|
INT8: 0x02;
|
||||||
|
INT16: 0x03;
|
||||||
|
INT32: 0x04;
|
||||||
|
INT64: 0x05;
|
||||||
|
FLOAT32: 0x06;
|
||||||
|
FLOAT64: 0x07;
|
||||||
|
STRING: 0x08;
|
||||||
|
BYTES: 0x09;
|
||||||
|
ARRAY: 0x0A;
|
||||||
|
OBJECT: 0x0B;
|
||||||
|
VECTOR: 0x0C;
|
||||||
|
JSON: 0x0D;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export declare const ResultFormat: Readonly<{
|
||||||
|
BINARY: 0x00;
|
||||||
|
JSON: 0x01;
|
||||||
|
TEXT: 0x02;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export declare class WireValue {
|
||||||
|
kind: number;
|
||||||
|
value: any;
|
||||||
|
constructor(kind: number, value?: any);
|
||||||
|
static null(): WireValue;
|
||||||
|
static bool(val: boolean): WireValue;
|
||||||
|
static int8(val: number): WireValue;
|
||||||
|
static int16(val: number): WireValue;
|
||||||
|
static int32(val: number): WireValue;
|
||||||
|
static int64(val: number | bigint): WireValue;
|
||||||
|
static float32(val: number): WireValue;
|
||||||
|
static float64(val: number): WireValue;
|
||||||
|
static string(val: string): WireValue;
|
||||||
|
static bytes(val: Buffer): WireValue;
|
||||||
|
static array(val: WireValue[]): WireValue;
|
||||||
|
static object(val: Record<string, WireValue>): WireValue;
|
||||||
|
static vector(val: number[]): WireValue;
|
||||||
|
static json(val: string): WireValue;
|
||||||
|
serialize(): Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryResultRow {
|
||||||
|
[column: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class QueryResult implements Iterable<QueryResultRow> {
|
||||||
|
columns: string[];
|
||||||
|
columnTypes: number[];
|
||||||
|
rows: any[][];
|
||||||
|
rowCount: number;
|
||||||
|
affectedRows: number;
|
||||||
|
[Symbol.iterator](): Iterator<QueryResultRow>;
|
||||||
|
get length(): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientOptions {
|
||||||
|
database?: string;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class Client {
|
||||||
|
constructor(host?: string, port?: number, options?: ClientOptions);
|
||||||
|
connect(): Promise<void>;
|
||||||
|
close(): Promise<void>;
|
||||||
|
isConnected(): boolean;
|
||||||
|
auth(token: string): Promise<void>;
|
||||||
|
ping(): Promise<boolean>;
|
||||||
|
query(sql: string): Promise<QueryResult>;
|
||||||
|
queryParams(sql: string, params?: WireValue[]): Promise<QueryResult>;
|
||||||
|
execute(sql: string): Promise<number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class QueryBuilder {
|
||||||
|
constructor(client: Client);
|
||||||
|
select(...cols: string[]): this;
|
||||||
|
from(table: string): this;
|
||||||
|
where(clause: string): this;
|
||||||
|
join(table: string, on: string): this;
|
||||||
|
leftJoin(table: string, on: string): this;
|
||||||
|
groupBy(...cols: string[]): this;
|
||||||
|
having(clause: string): this;
|
||||||
|
orderBy(col: string, direction?: string): this;
|
||||||
|
limit(n: number): this;
|
||||||
|
offset(n: number): this;
|
||||||
|
build(): string;
|
||||||
|
exec(): Promise<QueryResult>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* BaraDB JavaScript Client — Basic Examples
|
||||||
|
*
|
||||||
|
* Run: node examples/basic.js
|
||||||
|
* Requires BaraDB server on localhost:9472.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { Client, WireValue, QueryBuilder } = require('../baradb');
|
||||||
|
|
||||||
|
const HOST = 'localhost';
|
||||||
|
const PORT = 9472;
|
||||||
|
|
||||||
|
async function exampleConnection() {
|
||||||
|
console.log('=== Connection ===');
|
||||||
|
const client = new Client(HOST, PORT);
|
||||||
|
await client.connect();
|
||||||
|
console.log(`Connected: ${client.isConnected()}`);
|
||||||
|
console.log(`Ping: ${await client.ping()}`);
|
||||||
|
await client.close();
|
||||||
|
console.log(`Connected after close: ${client.isConnected()}`);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exampleSimpleQuery() {
|
||||||
|
console.log('=== Simple Query ===');
|
||||||
|
const client = new Client(HOST, PORT);
|
||||||
|
await client.connect();
|
||||||
|
const result = await client.query("SELECT 42 as answer, 'BaraDB' as db");
|
||||||
|
console.log(`Columns: ${result.columns.join(', ')}`);
|
||||||
|
console.log(`Row count: ${result.rowCount}`);
|
||||||
|
for (const row of result) {
|
||||||
|
console.log(` answer=${row.answer}, db=${row.db}`);
|
||||||
|
}
|
||||||
|
await client.close();
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exampleParameterizedQuery() {
|
||||||
|
console.log('=== Parameterized Query ===');
|
||||||
|
const client = new Client(HOST, PORT);
|
||||||
|
await client.connect();
|
||||||
|
const result = await client.queryParams(
|
||||||
|
'SELECT $1 as num, $2 as txt, $3 as flag',
|
||||||
|
[
|
||||||
|
WireValue.int64(123),
|
||||||
|
WireValue.string('hello world'),
|
||||||
|
WireValue.bool(true),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
for (const row of result) {
|
||||||
|
console.log(` num=${row.num}, txt=${row.txt}, flag=${row.flag}`);
|
||||||
|
}
|
||||||
|
await client.close();
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exampleQueryBuilder() {
|
||||||
|
console.log('=== Query Builder ===');
|
||||||
|
const client = new Client(HOST, PORT);
|
||||||
|
await client.connect();
|
||||||
|
const sql = await new QueryBuilder(client)
|
||||||
|
.select('id', 'name')
|
||||||
|
.from('users')
|
||||||
|
.where('active = true')
|
||||||
|
.orderBy('name', 'ASC')
|
||||||
|
.limit(5)
|
||||||
|
.build();
|
||||||
|
console.log(`Generated SQL: ${sql}`);
|
||||||
|
await client.close();
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exampleVector() {
|
||||||
|
console.log('=== Vector Value ===');
|
||||||
|
const client = new Client(HOST, PORT);
|
||||||
|
await client.connect();
|
||||||
|
const result = await client.queryParams(
|
||||||
|
'SELECT $1 as embedding',
|
||||||
|
[WireValue.vector([0.1, 0.2, 0.3, 0.4])]
|
||||||
|
);
|
||||||
|
for (const row of result) {
|
||||||
|
console.log(` embedding type: ${typeof row.embedding}`);
|
||||||
|
}
|
||||||
|
await client.close();
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exampleDdlDml() {
|
||||||
|
console.log('=== DDL & DML ===');
|
||||||
|
const client = new Client(HOST, PORT);
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.execute('DROP TABLE IF EXISTS demo_products');
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Cleanup warning: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.execute('CREATE TABLE demo_products (id INT PRIMARY KEY, name STRING, price FLOAT)');
|
||||||
|
const affected = await client.execute("INSERT INTO demo_products (id, name, price) VALUES (1, 'Widget', 9.99)");
|
||||||
|
console.log(`Insert affected rows: ${affected}`);
|
||||||
|
|
||||||
|
const result = await client.query('SELECT * FROM demo_products');
|
||||||
|
console.log(`Select returned ${result.rowCount} row(s)`);
|
||||||
|
for (const row of result) {
|
||||||
|
console.log(` ${JSON.stringify(row)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.execute('DROP TABLE demo_products');
|
||||||
|
console.log('Table dropped');
|
||||||
|
await client.close();
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('BaraDB JavaScript Client Examples');
|
||||||
|
console.log('Make sure BaraDB is running on localhost:9472');
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
const examples = [
|
||||||
|
exampleConnection,
|
||||||
|
exampleSimpleQuery,
|
||||||
|
exampleParameterizedQuery,
|
||||||
|
exampleQueryBuilder,
|
||||||
|
exampleVector,
|
||||||
|
exampleDdlDml,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const fn of examples) {
|
||||||
|
try {
|
||||||
|
await fn();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`ERROR in ${fn.name}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "baradb",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Official JavaScript/Node.js client for BaraDB — Multimodal Database Engine",
|
||||||
|
"main": "baradb.js",
|
||||||
|
"types": "baradb.d.ts",
|
||||||
|
"files": [
|
||||||
|
"baradb.js",
|
||||||
|
"baradb.d.ts",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "node --test tests/*.test.js",
|
||||||
|
"test:unit": "node --test tests/unit.test.js",
|
||||||
|
"test:integration": "node --test tests/integration.test.js",
|
||||||
|
"example": "node examples/basic.js"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"baradb",
|
||||||
|
"database",
|
||||||
|
"multimodal",
|
||||||
|
"client",
|
||||||
|
"wire-protocol",
|
||||||
|
"vector",
|
||||||
|
"graph"
|
||||||
|
],
|
||||||
|
"author": "BaraDB Team <team@baradb.dev>",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/barabadb/baradadb.git",
|
||||||
|
"directory": "clients/javascript"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/barabadb/baradadb/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://docs.baradb.dev",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* BaraDB JavaScript Client — Integration Tests
|
||||||
|
* Requires a running BaraDB server on localhost:9472.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { describe, it } = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const net = require('net');
|
||||||
|
const { Client, WireValue, QueryBuilder } = require('../baradb');
|
||||||
|
|
||||||
|
const HOST = 'localhost';
|
||||||
|
const PORT = 9472;
|
||||||
|
|
||||||
|
function serverAvailable() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const socket = new net.Socket();
|
||||||
|
socket.setTimeout(2000);
|
||||||
|
socket.once('connect', () => {
|
||||||
|
socket.destroy();
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
socket.once('error', () => resolve(false));
|
||||||
|
socket.once('timeout', () => resolve(false));
|
||||||
|
socket.connect(PORT, HOST);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let available = false;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
available = await serverAvailable();
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Use conditional describe via top-level if to avoid describe skip issues
|
||||||
|
if (true) {
|
||||||
|
describe('Integration', () => {
|
||||||
|
it('connects and closes', async () => {
|
||||||
|
if (!available) return; // skip inline
|
||||||
|
const client = new Client(HOST, PORT);
|
||||||
|
assert.strictEqual(client.isConnected(), false);
|
||||||
|
await client.connect();
|
||||||
|
assert.strictEqual(client.isConnected(), true);
|
||||||
|
await client.close();
|
||||||
|
assert.strictEqual(client.isConnected(), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pongs', async () => {
|
||||||
|
if (!available) return;
|
||||||
|
const client = new Client(HOST, PORT);
|
||||||
|
await client.connect();
|
||||||
|
const ok = await client.ping();
|
||||||
|
assert.strictEqual(ok, true);
|
||||||
|
await client.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('executes simple SELECT', async () => {
|
||||||
|
if (!available) return;
|
||||||
|
const client = new Client(HOST, PORT);
|
||||||
|
await client.connect();
|
||||||
|
const result = await client.query('SELECT 1 as one');
|
||||||
|
assert.ok(result instanceof Object);
|
||||||
|
assert.ok(typeof result.rowCount === 'number');
|
||||||
|
await client.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('executes parameterized query', async () => {
|
||||||
|
if (!available) return;
|
||||||
|
const client = new Client(HOST, PORT);
|
||||||
|
await client.connect();
|
||||||
|
const result = await client.queryParams(
|
||||||
|
'SELECT $1 as num, $2 as txt',
|
||||||
|
[WireValue.int64(42), WireValue.string('hello')]
|
||||||
|
);
|
||||||
|
assert.ok(result instanceof Object);
|
||||||
|
await client.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates table, inserts, selects, drops', async () => {
|
||||||
|
if (!available) return;
|
||||||
|
const client = new Client(HOST, PORT);
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.execute('DROP TABLE IF EXISTS js_test_users');
|
||||||
|
} catch (_) { /* ignore */ }
|
||||||
|
|
||||||
|
await client.execute('CREATE TABLE js_test_users (id INT PRIMARY KEY, name STRING, age INT)');
|
||||||
|
const affected = await client.execute("INSERT INTO js_test_users (id, name, age) VALUES (1, 'Alice', 30)");
|
||||||
|
assert.ok(affected >= 0);
|
||||||
|
|
||||||
|
const result = await client.query('SELECT name, age FROM js_test_users WHERE id = 1');
|
||||||
|
assert.strictEqual(result.rowCount, 1);
|
||||||
|
const row = {};
|
||||||
|
for (let i = 0; i < result.columns.length; i++) {
|
||||||
|
row[result.columns[i]] = result.rows[0][i];
|
||||||
|
}
|
||||||
|
assert.strictEqual(row.name, 'Alice');
|
||||||
|
assert.strictEqual(row.age, 30);
|
||||||
|
|
||||||
|
await client.execute('DROP TABLE js_test_users');
|
||||||
|
await client.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('executes built query', async () => {
|
||||||
|
if (!available) return;
|
||||||
|
const client = new Client(HOST, PORT);
|
||||||
|
await client.connect();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.execute('DROP TABLE IF EXISTS js_test_products');
|
||||||
|
} catch (_) { /* ignore */ }
|
||||||
|
|
||||||
|
await client.execute('CREATE TABLE js_test_products (id INT PRIMARY KEY, name STRING, price FLOAT)');
|
||||||
|
await client.execute("INSERT INTO js_test_products (id, name, price) VALUES (1, 'Widget', 9.99)");
|
||||||
|
|
||||||
|
const result = await new QueryBuilder(client)
|
||||||
|
.select('name', 'price')
|
||||||
|
.from('js_test_products')
|
||||||
|
.where('id = 1')
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
assert.strictEqual(result.rowCount, 1);
|
||||||
|
await client.execute('DROP TABLE js_test_products');
|
||||||
|
await client.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts or rejects dummy token', async () => {
|
||||||
|
if (!available) return;
|
||||||
|
const client = new Client(HOST, PORT);
|
||||||
|
await client.connect();
|
||||||
|
try {
|
||||||
|
await client.auth('dummy-token-for-testing');
|
||||||
|
} catch (err) {
|
||||||
|
assert.ok(err.message.includes('Auth') || err.message.toLowerCase().includes('error'));
|
||||||
|
}
|
||||||
|
await client.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
/**
|
||||||
|
* BaraDB JavaScript Client — Unit Tests
|
||||||
|
* No running server required.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { describe, it } = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const { WireValue, FieldKind, MsgKind, ResultFormat, Client, QueryBuilder } = require('../baradb');
|
||||||
|
|
||||||
|
describe('WireValue serialization', () => {
|
||||||
|
it('serializes null', () => {
|
||||||
|
const wv = WireValue.null();
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf.length, 1);
|
||||||
|
assert.strictEqual(buf[0], FieldKind.NULL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes bool true', () => {
|
||||||
|
const wv = WireValue.bool(true);
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf[0], FieldKind.BOOL);
|
||||||
|
assert.strictEqual(buf[1], 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes bool false', () => {
|
||||||
|
const wv = WireValue.bool(false);
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf[0], FieldKind.BOOL);
|
||||||
|
assert.strictEqual(buf[1], 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes int8', () => {
|
||||||
|
const wv = WireValue.int8(-42);
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf[0], FieldKind.INT8);
|
||||||
|
assert.strictEqual(buf.readInt8(1), -42);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes int16', () => {
|
||||||
|
const wv = WireValue.int16(-1000);
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf[0], FieldKind.INT16);
|
||||||
|
assert.strictEqual(buf.readInt16BE(1), -1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes int32', () => {
|
||||||
|
const wv = WireValue.int32(123456);
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf[0], FieldKind.INT32);
|
||||||
|
assert.strictEqual(buf.readInt32BE(1), 123456);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes int64', () => {
|
||||||
|
const wv = WireValue.int64(9999999999n);
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf[0], FieldKind.INT64);
|
||||||
|
assert.strictEqual(buf.readBigInt64BE(1), 9999999999n);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes float32', () => {
|
||||||
|
const wv = WireValue.float32(3.14);
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf[0], FieldKind.FLOAT32);
|
||||||
|
assert.ok(Math.abs(buf.readFloatBE(1) - 3.14) < 0.01);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes float64', () => {
|
||||||
|
const wv = WireValue.float64(2.718281828);
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf[0], FieldKind.FLOAT64);
|
||||||
|
assert.ok(Math.abs(buf.readDoubleBE(1) - 2.718281828) < 1e-9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes string', () => {
|
||||||
|
const wv = WireValue.string('hello');
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf[0], FieldKind.STRING);
|
||||||
|
assert.strictEqual(buf.readUInt32BE(1), 5);
|
||||||
|
assert.strictEqual(buf.subarray(5).toString('utf-8'), 'hello');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes bytes', () => {
|
||||||
|
const wv = WireValue.bytes(Buffer.from([0xde, 0xad, 0xbe, 0xef]));
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf[0], FieldKind.BYTES);
|
||||||
|
assert.strictEqual(buf.readUInt32BE(1), 4);
|
||||||
|
assert.deepStrictEqual(buf.subarray(5), Buffer.from([0xde, 0xad, 0xbe, 0xef]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes vector', () => {
|
||||||
|
const wv = WireValue.vector([1.0, 2.0, 3.0]);
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf[0], FieldKind.VECTOR);
|
||||||
|
assert.strictEqual(buf.readUInt32BE(1), 3);
|
||||||
|
const floats = [buf.readFloatBE(5), buf.readFloatBE(9), buf.readFloatBE(13)];
|
||||||
|
assert.deepStrictEqual(floats, [1.0, 2.0, 3.0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes json', () => {
|
||||||
|
const wv = WireValue.json('{"key":"value"}');
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf[0], FieldKind.JSON);
|
||||||
|
assert.strictEqual(buf.readUInt32BE(1), 15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes array', () => {
|
||||||
|
const wv = WireValue.array([WireValue.string('a'), WireValue.string('b')]);
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf[0], FieldKind.ARRAY);
|
||||||
|
assert.strictEqual(buf.readUInt32BE(1), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serializes object', () => {
|
||||||
|
const wv = WireValue.object({ name: WireValue.string('Bara'), age: WireValue.int32(42) });
|
||||||
|
const buf = wv.serialize();
|
||||||
|
assert.strictEqual(buf[0], FieldKind.OBJECT);
|
||||||
|
assert.strictEqual(buf.readUInt32BE(1), 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Protocol constants', () => {
|
||||||
|
it('has correct client message kinds', () => {
|
||||||
|
assert.strictEqual(MsgKind.CLIENT_HANDSHAKE, 0x01);
|
||||||
|
assert.strictEqual(MsgKind.QUERY, 0x02);
|
||||||
|
assert.strictEqual(MsgKind.QUERY_PARAMS, 0x03);
|
||||||
|
assert.strictEqual(MsgKind.EXECUTE, 0x04);
|
||||||
|
assert.strictEqual(MsgKind.BATCH, 0x05);
|
||||||
|
assert.strictEqual(MsgKind.TRANSACTION, 0x06);
|
||||||
|
assert.strictEqual(MsgKind.CLOSE, 0x07);
|
||||||
|
assert.strictEqual(MsgKind.PING, 0x08);
|
||||||
|
assert.strictEqual(MsgKind.AUTH, 0x09);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has correct server message kinds', () => {
|
||||||
|
assert.strictEqual(MsgKind.SERVER_HANDSHAKE, 0x80);
|
||||||
|
assert.strictEqual(MsgKind.READY, 0x81);
|
||||||
|
assert.strictEqual(MsgKind.DATA, 0x82);
|
||||||
|
assert.strictEqual(MsgKind.COMPLETE, 0x83);
|
||||||
|
assert.strictEqual(MsgKind.ERROR, 0x84);
|
||||||
|
assert.strictEqual(MsgKind.AUTH_CHALLENGE, 0x85);
|
||||||
|
assert.strictEqual(MsgKind.AUTH_OK, 0x86);
|
||||||
|
assert.strictEqual(MsgKind.SCHEMA_CHANGE, 0x87);
|
||||||
|
assert.strictEqual(MsgKind.PONG, 0x88);
|
||||||
|
assert.strictEqual(MsgKind.TRANSACTION_STATE, 0x89);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has correct result formats', () => {
|
||||||
|
assert.strictEqual(ResultFormat.BINARY, 0x00);
|
||||||
|
assert.strictEqual(ResultFormat.JSON, 0x01);
|
||||||
|
assert.strictEqual(ResultFormat.TEXT, 0x02);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('QueryBuilder', () => {
|
||||||
|
it('builds simple SELECT', () => {
|
||||||
|
const client = new Client();
|
||||||
|
const qb = new QueryBuilder(client);
|
||||||
|
const sql = qb.select('name', 'age').from('users').build();
|
||||||
|
assert.strictEqual(sql, 'SELECT name, age FROM users');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds SELECT * when empty', () => {
|
||||||
|
const client = new Client();
|
||||||
|
const qb = new QueryBuilder(client);
|
||||||
|
const sql = qb.from('users').build();
|
||||||
|
assert.strictEqual(sql, 'SELECT * FROM users');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds WHERE with single clause', () => {
|
||||||
|
const client = new Client();
|
||||||
|
const qb = new QueryBuilder(client);
|
||||||
|
const sql = qb.select('name').from('users').where('age > 18').build();
|
||||||
|
assert.strictEqual(sql, 'SELECT name FROM users WHERE age > 18');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds WHERE with multiple clauses', () => {
|
||||||
|
const client = new Client();
|
||||||
|
const qb = new QueryBuilder(client);
|
||||||
|
const sql = qb.select('name').from('users').where('age > 18').where('active = true').build();
|
||||||
|
assert.strictEqual(sql, 'SELECT name FROM users WHERE age > 18 AND active = true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds JOIN', () => {
|
||||||
|
const client = new Client();
|
||||||
|
const qb = new QueryBuilder(client);
|
||||||
|
const sql = qb.select('u.name', 'o.total').from('users u').join('orders o', 'u.id = o.user_id').build();
|
||||||
|
assert.ok(sql.includes('JOIN orders o ON u.id = o.user_id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds LEFT JOIN', () => {
|
||||||
|
const client = new Client();
|
||||||
|
const qb = new QueryBuilder(client);
|
||||||
|
const sql = qb.select('u.name', 'o.total').from('users u').leftJoin('orders o', 'u.id = o.user_id').build();
|
||||||
|
assert.ok(sql.includes('LEFT JOIN orders o ON u.id = o.user_id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds GROUP BY and HAVING', () => {
|
||||||
|
const client = new Client();
|
||||||
|
const qb = new QueryBuilder(client);
|
||||||
|
const sql = qb.select('dept', 'count(*)').from('employees').groupBy('dept').having('count(*) > 5').build();
|
||||||
|
assert.ok(sql.includes('GROUP BY dept'));
|
||||||
|
assert.ok(sql.includes('HAVING count(*) > 5'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds ORDER BY', () => {
|
||||||
|
const client = new Client();
|
||||||
|
const qb = new QueryBuilder(client);
|
||||||
|
const sql = qb.select('name').from('users').orderBy('name', 'DESC').build();
|
||||||
|
assert.ok(sql.includes('ORDER BY name DESC'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds LIMIT and OFFSET', () => {
|
||||||
|
const client = new Client();
|
||||||
|
const qb = new QueryBuilder(client);
|
||||||
|
const sql = qb.select('name').from('users').limit(10).offset(5).build();
|
||||||
|
assert.ok(sql.includes('LIMIT 10'));
|
||||||
|
assert.ok(sql.includes('OFFSET 5'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds full complex query', () => {
|
||||||
|
const client = new Client();
|
||||||
|
const qb = new QueryBuilder(client);
|
||||||
|
const sql = qb.select('u.name', 'count(*) as cnt')
|
||||||
|
.from('users u')
|
||||||
|
.leftJoin('orders o', 'u.id = o.user_id')
|
||||||
|
.where('u.age > 18')
|
||||||
|
.groupBy('u.name')
|
||||||
|
.having('cnt > 3')
|
||||||
|
.orderBy('cnt', 'DESC')
|
||||||
|
.limit(50)
|
||||||
|
.build();
|
||||||
|
assert.ok(sql.startsWith('SELECT'));
|
||||||
|
assert.ok(sql.includes('LEFT JOIN'));
|
||||||
|
assert.ok(sql.includes('WHERE'));
|
||||||
|
assert.ok(sql.includes('GROUP BY'));
|
||||||
|
assert.ok(sql.includes('HAVING'));
|
||||||
|
assert.ok(sql.includes('ORDER BY'));
|
||||||
|
assert.ok(sql.includes('LIMIT 50'));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
nimcache/
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
tests/test_client
|
||||||
|
tests/test_integration
|
||||||
|
examples/basic
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# BaraDB Nim Client
|
||||||
|
|
||||||
|
Official Nim client for **BaraDB** — a multimodal database engine.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Binary wire protocol** — fast TCP communication via `asyncnet`
|
||||||
|
- **Async/await API** — non-blocking by default
|
||||||
|
- **Sync wrapper** — `SyncClient` for blocking code
|
||||||
|
- **Query builder** — fluent SQL construction
|
||||||
|
- **Zero server dependencies** — self-contained, uses only Nim stdlib
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Add to your `.nimble` file:
|
||||||
|
|
||||||
|
```nim
|
||||||
|
requires "baradb >= 1.0.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or clone locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/barabadb/baradadb.git
|
||||||
|
cd clients/nim
|
||||||
|
nimble develop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Async API
|
||||||
|
|
||||||
|
```nim
|
||||||
|
import asyncdispatch
|
||||||
|
import baradb/client
|
||||||
|
|
||||||
|
proc main() {.async.} =
|
||||||
|
let client = newClient()
|
||||||
|
await client.connect()
|
||||||
|
let result = await client.query("SELECT name, age FROM users WHERE age > 18")
|
||||||
|
echo result
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
waitFor main()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sync API
|
||||||
|
|
||||||
|
```nim
|
||||||
|
import baradb/client
|
||||||
|
|
||||||
|
let client = newSyncClient()
|
||||||
|
client.connect()
|
||||||
|
let result = client.query("SELECT name, age FROM users WHERE age > 18")
|
||||||
|
echo result
|
||||||
|
client.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameterized Queries
|
||||||
|
|
||||||
|
```nim
|
||||||
|
import baradb/client
|
||||||
|
|
||||||
|
proc main() {.async.} =
|
||||||
|
let client = newClient()
|
||||||
|
await client.connect()
|
||||||
|
let result = await client.query(
|
||||||
|
"SELECT * FROM users WHERE age > $1",
|
||||||
|
@[WireValue(kind: fkInt64, int64Val: 18)]
|
||||||
|
)
|
||||||
|
echo result
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
waitFor main()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Builder
|
||||||
|
|
||||||
|
```nim
|
||||||
|
import baradb/client
|
||||||
|
|
||||||
|
proc main() {.async.} =
|
||||||
|
let client = newClient()
|
||||||
|
await client.connect()
|
||||||
|
let result = await newQueryBuilder(client)
|
||||||
|
.select("name", "email")
|
||||||
|
.from("users")
|
||||||
|
.where("active = true")
|
||||||
|
.orderBy("name", "ASC")
|
||||||
|
.limit(10)
|
||||||
|
.exec()
|
||||||
|
echo result
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
waitFor main()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
Unit tests (no server):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nimble test
|
||||||
|
```
|
||||||
|
|
||||||
|
Integration tests (requires server on `localhost:9472`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start server
|
||||||
|
docker run -d -p 9472:9472 baradb:latest
|
||||||
|
|
||||||
|
# Run integration tests
|
||||||
|
nim c -r tests/test_integration.nim
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### `ClientConfig`
|
||||||
|
|
||||||
|
| Field | Default | Description |
|
||||||
|
|--------------|-------------|------------------------|
|
||||||
|
| `host` | `127.0.0.1` | Server hostname |
|
||||||
|
| `port` | `9472` | TCP port |
|
||||||
|
| `database` | `default` | Default database |
|
||||||
|
| `username` | `admin` | Username |
|
||||||
|
| `password` | `""` | Password |
|
||||||
|
| `timeoutMs` | `30000` | Timeout in ms |
|
||||||
|
| `maxRetries` | `3` | Max reconnect retries |
|
||||||
|
|
||||||
|
### Methods (Async)
|
||||||
|
|
||||||
|
- `connect()` — open TCP connection
|
||||||
|
- `close()` — close connection
|
||||||
|
- `query(sql)` — execute SELECT-like query
|
||||||
|
- `query(sql, params)` — parameterized query
|
||||||
|
- `exec(sql)` — execute DDL/DML, returns affected rows
|
||||||
|
- `auth(token)` — JWT authentication
|
||||||
|
- `ping()` — health check
|
||||||
|
|
||||||
|
### Methods (Sync)
|
||||||
|
|
||||||
|
Same as async but blocking. Available on `SyncClient`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache-2.0
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
author = "BaraDB Team"
|
author = "BaraDB Team"
|
||||||
description = "BaraDB client library for Nim — async binary protocol client"
|
description = "Official Nim client for BaraDB — async binary protocol client"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
srcDir = "src"
|
srcDir = "src"
|
||||||
|
|
||||||
@@ -12,5 +12,16 @@ requires "nim >= 2.2.0"
|
|||||||
# Export the client module
|
# Export the client module
|
||||||
bin = @[]
|
bin = @[]
|
||||||
|
|
||||||
task test, "Run client tests":
|
task test, "Run all client tests (unit + integration if server available)":
|
||||||
exec "nim c -r tests/test_client.nim"
|
exec "nim c -r tests/test_client.nim"
|
||||||
|
# Integration tests are compiled separately; they auto-skip if no server.
|
||||||
|
try:
|
||||||
|
exec "nim c -r tests/test_integration.nim"
|
||||||
|
except:
|
||||||
|
echo "Integration tests skipped (no server on localhost:9472)"
|
||||||
|
|
||||||
|
task test_unit, "Run unit tests only":
|
||||||
|
exec "nim c -r tests/test_client.nim"
|
||||||
|
|
||||||
|
task example, "Run basic example":
|
||||||
|
exec "nim c -r examples/basic.nim"
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
## BaraDB Nim Client — Basic Examples
|
||||||
|
## Make sure BaraDB is running on localhost:9472
|
||||||
|
|
||||||
|
import std/asyncdispatch
|
||||||
|
import std/strutils
|
||||||
|
import baradb/client
|
||||||
|
|
||||||
|
proc exampleConnection() {.async.} =
|
||||||
|
echo "=== Connection ==="
|
||||||
|
let client = newClient()
|
||||||
|
await client.connect()
|
||||||
|
echo "Connected: ", client.isConnected
|
||||||
|
echo "Ping: ", await client.ping()
|
||||||
|
client.close()
|
||||||
|
echo "Connected after close: ", client.isConnected
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
proc exampleSimpleQuery() {.async.} =
|
||||||
|
echo "=== Simple Query ==="
|
||||||
|
let client = newClient()
|
||||||
|
await client.connect()
|
||||||
|
let result = await client.query("SELECT 42 as answer, 'BaraDB' as db")
|
||||||
|
echo "Columns: ", result.columns
|
||||||
|
echo "Row count: ", result.rowCount
|
||||||
|
for row in result.rows:
|
||||||
|
echo " ", row.join(", ")
|
||||||
|
client.close()
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
proc exampleParameterizedQuery() {.async.} =
|
||||||
|
echo "=== Parameterized Query ==="
|
||||||
|
let client = newClient()
|
||||||
|
await client.connect()
|
||||||
|
let result = await client.query(
|
||||||
|
"SELECT $1 as num, $2 as txt",
|
||||||
|
@[
|
||||||
|
WireValue(kind: fkInt64, int64Val: 123),
|
||||||
|
WireValue(kind: fkString, strVal: "hello world"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
for row in result.rows:
|
||||||
|
echo " ", row.join(", ")
|
||||||
|
client.close()
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
proc exampleQueryBuilder() {.async.} =
|
||||||
|
echo "=== Query Builder ==="
|
||||||
|
let client = newClient()
|
||||||
|
await client.connect()
|
||||||
|
let sql = newQueryBuilder(client)
|
||||||
|
.select("id", "name")
|
||||||
|
.from("users")
|
||||||
|
.where("active = true")
|
||||||
|
.orderBy("name", "ASC")
|
||||||
|
.limit(5)
|
||||||
|
.build()
|
||||||
|
echo "Generated SQL: ", sql
|
||||||
|
client.close()
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
proc exampleDdlDml() {.async.} =
|
||||||
|
echo "=== DDL & DML ==="
|
||||||
|
let client = newClient()
|
||||||
|
await client.connect()
|
||||||
|
|
||||||
|
try:
|
||||||
|
discard await client.exec("DROP TABLE IF EXISTS demo_products")
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
|
||||||
|
discard await client.exec("CREATE TABLE demo_products (id INT PRIMARY KEY, name STRING, price FLOAT)")
|
||||||
|
let affected = await client.exec("INSERT INTO demo_products (id, name, price) VALUES (1, 'Widget', 9.99)")
|
||||||
|
echo "Insert affected rows: ", affected
|
||||||
|
|
||||||
|
let result = await client.query("SELECT * FROM demo_products")
|
||||||
|
echo "Select returned ", result.rowCount, " row(s)"
|
||||||
|
for row in result.rows:
|
||||||
|
echo " ", row.join(", ")
|
||||||
|
|
||||||
|
discard await client.exec("DROP TABLE demo_products")
|
||||||
|
echo "Table dropped"
|
||||||
|
client.close()
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
proc main() {.async.} =
|
||||||
|
echo "BaraDB Nim Client Examples"
|
||||||
|
echo "Make sure BaraDB is running on localhost:9472"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
await exampleConnection()
|
||||||
|
await exampleSimpleQuery()
|
||||||
|
await exampleParameterizedQuery()
|
||||||
|
await exampleQueryBuilder()
|
||||||
|
await exampleDdlDml()
|
||||||
|
|
||||||
|
waitFor main()
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
## BaraDB Nim Client — Integration Tests
|
||||||
|
## Requires a running BaraDB server on localhost:9472.
|
||||||
|
|
||||||
|
import std/unittest
|
||||||
|
import std/asyncdispatch
|
||||||
|
import std/asyncnet
|
||||||
|
import std/strutils
|
||||||
|
import baradb/client
|
||||||
|
|
||||||
|
const
|
||||||
|
TestHost = "127.0.0.1"
|
||||||
|
TestPort = 9472
|
||||||
|
|
||||||
|
proc serverAvailable(): bool =
|
||||||
|
try:
|
||||||
|
var socket = newAsyncSocket()
|
||||||
|
waitFor socket.connect(TestHost, Port(TestPort))
|
||||||
|
socket.close()
|
||||||
|
return true
|
||||||
|
except:
|
||||||
|
return false
|
||||||
|
|
||||||
|
let hasServer = serverAvailable()
|
||||||
|
|
||||||
|
suite "Integration: Connection":
|
||||||
|
test "Connect and close":
|
||||||
|
if not hasServer:
|
||||||
|
skip()
|
||||||
|
var client = newClient(ClientConfig(host: TestHost, port: TestPort))
|
||||||
|
check not client.isConnected
|
||||||
|
waitFor client.connect()
|
||||||
|
check client.isConnected
|
||||||
|
client.close()
|
||||||
|
check not client.isConnected
|
||||||
|
|
||||||
|
test "Ping":
|
||||||
|
if not hasServer:
|
||||||
|
skip()
|
||||||
|
var client = newClient(ClientConfig(host: TestHost, port: TestPort))
|
||||||
|
waitFor client.connect()
|
||||||
|
check (waitFor client.ping()) == true
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
suite "Integration: Query":
|
||||||
|
test "Simple SELECT":
|
||||||
|
if not hasServer:
|
||||||
|
skip()
|
||||||
|
var client = newClient(ClientConfig(host: TestHost, port: TestPort))
|
||||||
|
waitFor client.connect()
|
||||||
|
let result = waitFor client.query("SELECT 1 as one")
|
||||||
|
check result.rowCount >= 0
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
test "Parameterized query":
|
||||||
|
if not hasServer:
|
||||||
|
skip()
|
||||||
|
var client = newClient(ClientConfig(host: TestHost, port: TestPort))
|
||||||
|
waitFor client.connect()
|
||||||
|
let result = waitFor client.query(
|
||||||
|
"SELECT $1 as num, $2 as txt",
|
||||||
|
@[WireValue(kind: fkInt64, int64Val: 42), WireValue(kind: fkString, strVal: "hello")]
|
||||||
|
)
|
||||||
|
check result.rowCount >= 0
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
suite "Integration: DDL & DML":
|
||||||
|
test "Create table, insert, select, drop":
|
||||||
|
if not hasServer:
|
||||||
|
skip()
|
||||||
|
var client = newClient(ClientConfig(host: TestHost, port: TestPort))
|
||||||
|
waitFor client.connect()
|
||||||
|
|
||||||
|
try:
|
||||||
|
discard waitFor client.exec("DROP TABLE IF EXISTS nim_test_users")
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
|
||||||
|
discard waitFor client.exec("CREATE TABLE nim_test_users (id INT PRIMARY KEY, name STRING, age INT)")
|
||||||
|
let affected = waitFor client.exec("INSERT INTO nim_test_users (id, name, age) VALUES (1, 'Alice', 30)")
|
||||||
|
check affected >= 0
|
||||||
|
|
||||||
|
let result = waitFor client.query("SELECT name, age FROM nim_test_users WHERE id = 1")
|
||||||
|
check result.rowCount == 1
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
suite "Integration: QueryBuilder":
|
||||||
|
test "Builder exec":
|
||||||
|
if not hasServer:
|
||||||
|
skip()
|
||||||
|
var client = newClient(ClientConfig(host: TestHost, port: TestPort))
|
||||||
|
waitFor client.connect()
|
||||||
|
|
||||||
|
try:
|
||||||
|
discard waitFor client.exec("DROP TABLE IF EXISTS nim_test_products")
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
|
||||||
|
discard waitFor client.exec("CREATE TABLE nim_test_products (id INT PRIMARY KEY, name STRING, price FLOAT)")
|
||||||
|
discard waitFor client.exec("INSERT INTO nim_test_products (id, name, price) VALUES (1, 'Widget', 9.99)")
|
||||||
|
|
||||||
|
let result = waitFor newQueryBuilder(client)
|
||||||
|
.select("name", "price")
|
||||||
|
.from("nim_test_products")
|
||||||
|
.where("id = 1")
|
||||||
|
.exec()
|
||||||
|
check result.rowCount == 1
|
||||||
|
|
||||||
|
discard waitFor client.exec("DROP TABLE nim_test_products")
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
suite "Integration: SyncClient":
|
||||||
|
test "Sync query":
|
||||||
|
if not hasServer:
|
||||||
|
skip()
|
||||||
|
var client = newSyncClient(ClientConfig(host: TestHost, port: TestPort))
|
||||||
|
client.connect()
|
||||||
|
let result = client.query("SELECT 1 as one")
|
||||||
|
check result.rowCount >= 0
|
||||||
|
client.close()
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.venv
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# BaraDB Python Client
|
||||||
|
|
||||||
|
Official Python client for **BaraDB** — a multimodal database engine written in Nim.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Binary wire protocol** — fast, compact TCP communication
|
||||||
|
- **Sync & blocking API** — simple to use in scripts and apps
|
||||||
|
- **Query builder** — fluent SQL construction
|
||||||
|
- **Parameterized queries** — safe from SQL injection
|
||||||
|
- **Vector & JSON support** — first-class multimodal types
|
||||||
|
- **Context managers** — `with` statement support
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install baradb
|
||||||
|
```
|
||||||
|
|
||||||
|
Or from source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/barabadb/baradadb.git
|
||||||
|
cd clients/python
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```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"])
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Context Manager
|
||||||
|
|
||||||
|
```python
|
||||||
|
from baradb import Client
|
||||||
|
|
||||||
|
with Client("localhost", 9472) as client:
|
||||||
|
result = client.query("SELECT 1")
|
||||||
|
print(result.row_count)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameterized Queries
|
||||||
|
|
||||||
|
```python
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Builder
|
||||||
|
|
||||||
|
```python
|
||||||
|
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)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vector Search
|
||||||
|
|
||||||
|
```python
|
||||||
|
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])],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
Unit tests (no server):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_wire_protocol.py tests/test_query_builder.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Integration tests (requires server on `localhost:9472`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start server
|
||||||
|
docker run -d -p 9472:9472 baradb:latest
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### `Client(host, port, database, username, password, timeout)`
|
||||||
|
|
||||||
|
| Parameter | Default | Description |
|
||||||
|
|------------|-------------|----------------------------|
|
||||||
|
| `host` | `localhost` | Server hostname |
|
||||||
|
| `port` | `9472` | TCP wire protocol port |
|
||||||
|
| `database` | `default` | Default database |
|
||||||
|
| `username` | `admin` | Username |
|
||||||
|
| `password` | `""` | Password |
|
||||||
|
| `timeout` | `30` | Socket timeout in seconds |
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
- `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
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache-2.0
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""
|
||||||
|
BaraDB Python Client
|
||||||
|
|
||||||
|
Official Python client for BaraDB — Multimodal Database Engine.
|
||||||
|
Communicates via the BaraDB Wire Protocol (binary, big-endian, TCP).
|
||||||
|
|
||||||
|
Install:
|
||||||
|
pip install baradb
|
||||||
|
|
||||||
|
Quick Start:
|
||||||
|
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()
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .core import (
|
||||||
|
Client,
|
||||||
|
QueryBuilder,
|
||||||
|
QueryResult,
|
||||||
|
WireValue,
|
||||||
|
MsgKind,
|
||||||
|
FieldKind,
|
||||||
|
ResultFormat,
|
||||||
|
)
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
__all__ = [
|
||||||
|
"Client",
|
||||||
|
"QueryBuilder",
|
||||||
|
"QueryResult",
|
||||||
|
"WireValue",
|
||||||
|
"MsgKind",
|
||||||
|
"FieldKind",
|
||||||
|
"ResultFormat",
|
||||||
|
]
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
BaraDB Python Client — Basic Examples
|
||||||
|
|
||||||
|
This script demonstrates common operations with the BaraDB Python client.
|
||||||
|
Run it while a BaraDB server is listening on localhost:9472.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from baradb import Client, QueryBuilder, WireValue
|
||||||
|
|
||||||
|
|
||||||
|
def example_connection():
|
||||||
|
print("=== Connection ===")
|
||||||
|
client = Client("localhost", 9472)
|
||||||
|
client.connect()
|
||||||
|
print(f"Connected: {client.is_connected()}")
|
||||||
|
print(f"Ping: {client.ping()}")
|
||||||
|
client.close()
|
||||||
|
print(f"Connected after close: {client.is_connected()}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def example_context_manager():
|
||||||
|
print("=== Context Manager ===")
|
||||||
|
with Client("localhost", 9472) as client:
|
||||||
|
print(f"Inside context: {client.is_connected()}")
|
||||||
|
print(f"Ping: {client.ping()}")
|
||||||
|
print("Outside context: closed automatically")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def example_simple_query():
|
||||||
|
print("=== Simple Query ===")
|
||||||
|
with Client("localhost", 9472) as client:
|
||||||
|
result = 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}")
|
||||||
|
for row in result:
|
||||||
|
print(f" answer={row['answer']}, db={row['db']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def example_parameterized_query():
|
||||||
|
print("=== Parameterized Query ===")
|
||||||
|
with Client("localhost", 9472) as client:
|
||||||
|
result = client.query_params(
|
||||||
|
"SELECT $1 as num, $2 as txt, $3 as flag",
|
||||||
|
[
|
||||||
|
WireValue.int64(123),
|
||||||
|
WireValue.string("hello world"),
|
||||||
|
WireValue.bool_val(True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
for row in result:
|
||||||
|
print(f" num={row['num']}, txt={row['txt']}, flag={row['flag']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def example_query_builder():
|
||||||
|
print("=== Query Builder ===")
|
||||||
|
with Client("localhost", 9472) as client:
|
||||||
|
sql = (
|
||||||
|
QueryBuilder(client)
|
||||||
|
.select("id", "name")
|
||||||
|
.from_("users")
|
||||||
|
.where("active = true")
|
||||||
|
.order_by("name", "ASC")
|
||||||
|
.limit(5)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
print(f"Generated SQL: {sql}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def example_vector():
|
||||||
|
print("=== Vector Value ===")
|
||||||
|
with Client("localhost", 9472) as client:
|
||||||
|
result = client.query_params(
|
||||||
|
"SELECT $1 as embedding",
|
||||||
|
[WireValue.vector([0.1, 0.2, 0.3, 0.4])],
|
||||||
|
)
|
||||||
|
for row in result:
|
||||||
|
print(f" embedding type: {type(row['embedding'])}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def example_ddl_dml():
|
||||||
|
print("=== DDL & DML ===")
|
||||||
|
with Client("localhost", 9472) as client:
|
||||||
|
# Clean up
|
||||||
|
try:
|
||||||
|
client.execute("DROP TABLE IF EXISTS demo_products")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Cleanup warning: {exc}")
|
||||||
|
|
||||||
|
client.execute(
|
||||||
|
"CREATE TABLE demo_products (id INT PRIMARY KEY, name STRING, price FLOAT)"
|
||||||
|
)
|
||||||
|
affected = 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")
|
||||||
|
print(f"Select returned {result.row_count} row(s)")
|
||||||
|
for row in result:
|
||||||
|
print(f" {row}")
|
||||||
|
|
||||||
|
client.execute("DROP TABLE demo_products")
|
||||||
|
print("Table dropped")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("BaraDB Python Client Examples")
|
||||||
|
print("Make sure BaraDB is running on localhost:9472")
|
||||||
|
print()
|
||||||
|
|
||||||
|
examples = [
|
||||||
|
example_connection,
|
||||||
|
example_context_manager,
|
||||||
|
example_simple_query,
|
||||||
|
example_parameterized_query,
|
||||||
|
example_query_builder,
|
||||||
|
example_vector,
|
||||||
|
example_ddl_dml,
|
||||||
|
]
|
||||||
|
|
||||||
|
for fn in examples:
|
||||||
|
try:
|
||||||
|
fn()
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"ERROR in {fn.__name__}: {exc}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "baradb"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Official Python client for BaraDB — Multimodal Database Engine"
|
||||||
|
readme = "README.md"
|
||||||
|
license = { text = "Apache-2.0" }
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
authors = [
|
||||||
|
{ name = "BaraDB Team", email = "team@baradb.dev" },
|
||||||
|
]
|
||||||
|
keywords = ["database", "baradb", "multimodal", "client", "wire-protocol"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: Apache Software License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Topic :: Database",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
|
"Typing :: Typed",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=7.0",
|
||||||
|
"pytest-asyncio>=0.21.0",
|
||||||
|
"mypy>=1.0",
|
||||||
|
"ruff>=0.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/barabadb/baradadb"
|
||||||
|
Documentation = "https://docs.baradb.dev"
|
||||||
|
Repository = "https://github.com/barabadb/baradadb.git"
|
||||||
|
Issues = "https://github.com/barabadb/baradadb/issues"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["baradb"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
pythonpath = ["."]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
strict = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unused_configs = true
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
select = ["E", "F", "I", "W"]
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
"""
|
||||||
|
Integration tests for BaraDB Python client.
|
||||||
|
Requires a running BaraDB server on localhost:9472.
|
||||||
|
|
||||||
|
These tests are skipped automatically if the server is unreachable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import pytest
|
||||||
|
from baradb import Client, WireValue, QueryBuilder
|
||||||
|
|
||||||
|
|
||||||
|
BARADB_HOST = "localhost"
|
||||||
|
BARADB_PORT = 9472
|
||||||
|
|
||||||
|
|
||||||
|
def _server_available() -> bool:
|
||||||
|
"""Check if BaraDB server is listening."""
|
||||||
|
try:
|
||||||
|
with socket.create_connection((BARADB_HOST, BARADB_PORT), timeout=2):
|
||||||
|
return True
|
||||||
|
except (socket.timeout, ConnectionRefusedError, OSError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
not _server_available(),
|
||||||
|
reason=f"BaraDB server not available at {BARADB_HOST}:{BARADB_PORT}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
c = Client(BARADB_HOST, BARADB_PORT)
|
||||||
|
c.connect()
|
||||||
|
yield c
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
|
||||||
|
class TestConnection:
|
||||||
|
def test_connect_and_close(self):
|
||||||
|
c = Client(BARADB_HOST, BARADB_PORT)
|
||||||
|
assert not c.is_connected()
|
||||||
|
c.connect()
|
||||||
|
assert c.is_connected()
|
||||||
|
c.close()
|
||||||
|
assert not c.is_connected()
|
||||||
|
|
||||||
|
def test_context_manager(self):
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class TestQuery:
|
||||||
|
def test_simple_select(self, client):
|
||||||
|
result = 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(
|
||||||
|
"SELECT $1 as num, $2 as txt",
|
||||||
|
[WireValue.int64(42), WireValue.string("hello")],
|
||||||
|
)
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
|
|
||||||
|
class TestExecute:
|
||||||
|
def test_create_table_and_insert(self, client):
|
||||||
|
# Clean up first (best-effort)
|
||||||
|
try:
|
||||||
|
client.execute("DROP TABLE IF EXISTS test_users")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
client.execute(
|
||||||
|
"CREATE TABLE test_users (id INT PRIMARY KEY, name STRING, age INT)"
|
||||||
|
)
|
||||||
|
affected = 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")
|
||||||
|
assert result.row_count == 1
|
||||||
|
row = result.rows[0]
|
||||||
|
# Server returns all columns; map by name via dict iteration
|
||||||
|
row_dict = dict(zip(result.columns, row))
|
||||||
|
assert row_dict["name"] == "Alice"
|
||||||
|
assert row_dict["age"] == 30
|
||||||
|
|
||||||
|
client.execute("DROP TABLE test_users")
|
||||||
|
|
||||||
|
|
||||||
|
class TestQueryBuilder:
|
||||||
|
def test_builder_exec(self, client):
|
||||||
|
try:
|
||||||
|
client.execute("DROP TABLE IF EXISTS test_products")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
client.execute(
|
||||||
|
"CREATE TABLE test_products (id INT PRIMARY KEY, name STRING, price FLOAT)"
|
||||||
|
)
|
||||||
|
client.execute(
|
||||||
|
"INSERT INTO test_products (id, name, price) VALUES (1, 'Widget', 9.99)"
|
||||||
|
)
|
||||||
|
|
||||||
|
qb = (
|
||||||
|
QueryBuilder(client)
|
||||||
|
.select("name", "price")
|
||||||
|
.from_("test_products")
|
||||||
|
.where("id = 1")
|
||||||
|
)
|
||||||
|
result = qb.exec()
|
||||||
|
assert result.row_count == 1
|
||||||
|
|
||||||
|
client.execute("DROP TABLE test_products")
|
||||||
|
|
||||||
|
|
||||||
|
class TestAuth:
|
||||||
|
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")
|
||||||
|
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):
|
||||||
|
try:
|
||||||
|
client.execute("DROP TABLE IF EXISTS test_txn")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
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")
|
||||||
|
|
||||||
|
result = client.query("SELECT COUNT(*) FROM test_txn")
|
||||||
|
assert result.row_count >= 0
|
||||||
|
|
||||||
|
client.execute("DROP TABLE test_txn")
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for QueryBuilder — no running server required.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from baradb import Client, QueryBuilder
|
||||||
|
|
||||||
|
|
||||||
|
class TestQueryBuilder:
|
||||||
|
def test_simple_select(self):
|
||||||
|
client = Client()
|
||||||
|
qb = QueryBuilder(client)
|
||||||
|
sql = qb.select("name", "age").from_("users").build()
|
||||||
|
assert sql == "SELECT name, age FROM users"
|
||||||
|
|
||||||
|
def test_select_all(self):
|
||||||
|
client = Client()
|
||||||
|
qb = QueryBuilder(client)
|
||||||
|
sql = qb.from_("users").build()
|
||||||
|
assert sql == "SELECT * FROM users"
|
||||||
|
|
||||||
|
def test_where_single(self):
|
||||||
|
client = Client()
|
||||||
|
qb = QueryBuilder(client)
|
||||||
|
sql = qb.select("name").from_("users").where("age > 18").build()
|
||||||
|
assert sql == "SELECT name FROM users WHERE age > 18"
|
||||||
|
|
||||||
|
def test_where_multiple(self):
|
||||||
|
client = Client()
|
||||||
|
qb = QueryBuilder(client)
|
||||||
|
sql = (
|
||||||
|
qb.select("name")
|
||||||
|
.from_("users")
|
||||||
|
.where("age > 18")
|
||||||
|
.where("active = true")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
assert sql == "SELECT name FROM users WHERE age > 18 AND active = true"
|
||||||
|
|
||||||
|
def test_join(self):
|
||||||
|
client = Client()
|
||||||
|
qb = QueryBuilder(client)
|
||||||
|
sql = (
|
||||||
|
qb.select("u.name", "o.total")
|
||||||
|
.from_("users u")
|
||||||
|
.join("orders o", "u.id = o.user_id")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
assert "JOIN orders o ON u.id = o.user_id" in sql
|
||||||
|
|
||||||
|
def test_left_join(self):
|
||||||
|
client = Client()
|
||||||
|
qb = QueryBuilder(client)
|
||||||
|
sql = (
|
||||||
|
qb.select("u.name", "o.total")
|
||||||
|
.from_("users u")
|
||||||
|
.left_join("orders o", "u.id = o.user_id")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
assert "LEFT JOIN orders o ON u.id = o.user_id" in sql
|
||||||
|
|
||||||
|
def test_group_by_having(self):
|
||||||
|
client = Client()
|
||||||
|
qb = QueryBuilder(client)
|
||||||
|
sql = (
|
||||||
|
qb.select("dept", "count(*)")
|
||||||
|
.from_("employees")
|
||||||
|
.group_by("dept")
|
||||||
|
.having("count(*) > 5")
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
assert "GROUP BY dept" in sql
|
||||||
|
assert "HAVING count(*) > 5" in sql
|
||||||
|
|
||||||
|
def test_order_by(self):
|
||||||
|
client = Client()
|
||||||
|
qb = QueryBuilder(client)
|
||||||
|
sql = qb.select("name").from_("users").order_by("name", "DESC").build()
|
||||||
|
assert "ORDER BY name DESC" in sql
|
||||||
|
|
||||||
|
def test_limit_offset(self):
|
||||||
|
client = Client()
|
||||||
|
qb = QueryBuilder(client)
|
||||||
|
sql = qb.select("name").from_("users").limit(10).offset(5).build()
|
||||||
|
assert "LIMIT 10" in sql
|
||||||
|
assert "OFFSET 5" in sql
|
||||||
|
|
||||||
|
def test_full_complex_query(self):
|
||||||
|
client = Client()
|
||||||
|
qb = QueryBuilder(client)
|
||||||
|
sql = (
|
||||||
|
qb.select("u.name", "count(*) as cnt")
|
||||||
|
.from_("users u")
|
||||||
|
.left_join("orders o", "u.id = o.user_id")
|
||||||
|
.where("u.age > 18")
|
||||||
|
.group_by("u.name")
|
||||||
|
.having("cnt > 3")
|
||||||
|
.order_by("cnt", "DESC")
|
||||||
|
.limit(50)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
assert sql.startswith("SELECT")
|
||||||
|
assert "FROM users u" in sql
|
||||||
|
assert "LEFT JOIN orders o ON u.id = o.user_id" in sql
|
||||||
|
assert "WHERE u.age > 18" in sql
|
||||||
|
assert "GROUP BY u.name" in sql
|
||||||
|
assert "HAVING cnt > 3" in sql
|
||||||
|
assert "ORDER BY cnt DESC" in sql
|
||||||
|
assert "LIMIT 50" in sql
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for BaraDB Python client wire protocol.
|
||||||
|
No running server required.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import struct
|
||||||
|
import pytest
|
||||||
|
from baradb import WireValue, FieldKind, MsgKind, ResultFormat
|
||||||
|
|
||||||
|
|
||||||
|
class TestWireValue:
|
||||||
|
"""Tests for WireValue serialization / deserialization."""
|
||||||
|
|
||||||
|
def test_null(self):
|
||||||
|
wv = WireValue.null()
|
||||||
|
assert wv.kind == FieldKind.NULL
|
||||||
|
data = wv.serialize()
|
||||||
|
assert data == b"\x00"
|
||||||
|
|
||||||
|
def test_bool_true(self):
|
||||||
|
wv = WireValue.bool_val(True)
|
||||||
|
assert wv.serialize() == b"\x01\x01"
|
||||||
|
|
||||||
|
def test_bool_false(self):
|
||||||
|
wv = WireValue.bool_val(False)
|
||||||
|
assert wv.serialize() == b"\x01\x00"
|
||||||
|
|
||||||
|
def test_int8(self):
|
||||||
|
wv = WireValue.int8(-42)
|
||||||
|
data = wv.serialize()
|
||||||
|
assert data[0] == FieldKind.INT8
|
||||||
|
assert struct.unpack(">b", data[1:])[0] == -42
|
||||||
|
|
||||||
|
def test_int16(self):
|
||||||
|
wv = WireValue.int16(-1000)
|
||||||
|
data = wv.serialize()
|
||||||
|
assert data[0] == FieldKind.INT16
|
||||||
|
assert struct.unpack(">h", data[1:])[0] == -1000
|
||||||
|
|
||||||
|
def test_int32(self):
|
||||||
|
wv = WireValue.int32(123456)
|
||||||
|
data = wv.serialize()
|
||||||
|
assert data[0] == FieldKind.INT32
|
||||||
|
assert struct.unpack(">i", data[1:])[0] == 123456
|
||||||
|
|
||||||
|
def test_int64(self):
|
||||||
|
wv = WireValue.int64(9999999999)
|
||||||
|
data = wv.serialize()
|
||||||
|
assert data[0] == FieldKind.INT64
|
||||||
|
assert struct.unpack(">q", data[1:])[0] == 9999999999
|
||||||
|
|
||||||
|
def test_float32(self):
|
||||||
|
wv = WireValue.float32(3.14)
|
||||||
|
data = wv.serialize()
|
||||||
|
assert data[0] == FieldKind.FLOAT32
|
||||||
|
assert abs(struct.unpack(">f", data[1:])[0] - 3.14) < 0.01
|
||||||
|
|
||||||
|
def test_float64(self):
|
||||||
|
wv = WireValue.float64(2.718281828)
|
||||||
|
data = wv.serialize()
|
||||||
|
assert data[0] == FieldKind.FLOAT64
|
||||||
|
assert abs(struct.unpack(">d", data[1:])[0] - 2.718281828) < 1e-9
|
||||||
|
|
||||||
|
def test_string(self):
|
||||||
|
wv = WireValue.string("hello")
|
||||||
|
data = wv.serialize()
|
||||||
|
assert data[0] == FieldKind.STRING
|
||||||
|
length = struct.unpack(">I", data[1:5])[0]
|
||||||
|
assert length == 5
|
||||||
|
assert data[5:] == b"hello"
|
||||||
|
|
||||||
|
def test_bytes(self):
|
||||||
|
wv = WireValue.bytes_val(b"\xde\xad\xbe\xef")
|
||||||
|
data = wv.serialize()
|
||||||
|
assert data[0] == FieldKind.BYTES
|
||||||
|
length = struct.unpack(">I", data[1:5])[0]
|
||||||
|
assert length == 4
|
||||||
|
assert data[5:] == b"\xde\xad\xbe\xef"
|
||||||
|
|
||||||
|
def test_vector(self):
|
||||||
|
wv = WireValue.vector([1.0, 2.0, 3.0])
|
||||||
|
data = wv.serialize()
|
||||||
|
assert data[0] == FieldKind.VECTOR
|
||||||
|
dim = struct.unpack(">I", data[1:5])[0]
|
||||||
|
assert dim == 3
|
||||||
|
floats = [struct.unpack(">f", data[5 + i * 4 : 9 + i * 4])[0] for i in range(3)]
|
||||||
|
assert floats == [1.0, 2.0, 3.0]
|
||||||
|
|
||||||
|
def test_json(self):
|
||||||
|
wv = WireValue.json_val('{"key": "value"}')
|
||||||
|
data = wv.serialize()
|
||||||
|
assert data[0] == FieldKind.JSON
|
||||||
|
length = struct.unpack(">I", data[1:5])[0]
|
||||||
|
assert length == 16
|
||||||
|
|
||||||
|
def test_array(self):
|
||||||
|
inner = [WireValue.string("a"), WireValue.string("b")]
|
||||||
|
wv = WireValue.array_val(inner)
|
||||||
|
data = wv.serialize()
|
||||||
|
assert data[0] == FieldKind.ARRAY
|
||||||
|
count = struct.unpack(">I", data[1:5])[0]
|
||||||
|
assert count == 2
|
||||||
|
|
||||||
|
def test_object(self):
|
||||||
|
inner = {"name": WireValue.string("Bara"), "age": WireValue.int32(42)}
|
||||||
|
wv = WireValue.object_val(inner)
|
||||||
|
data = wv.serialize()
|
||||||
|
assert data[0] == FieldKind.OBJECT
|
||||||
|
count = struct.unpack(">I", data[1:5])[0]
|
||||||
|
assert count == 2
|
||||||
|
|
||||||
|
|
||||||
|
class TestMessageKinds:
|
||||||
|
"""Sanity checks for protocol constants."""
|
||||||
|
|
||||||
|
def test_client_kinds(self):
|
||||||
|
assert MsgKind.CLIENT_HANDSHAKE == 0x01
|
||||||
|
assert MsgKind.QUERY == 0x02
|
||||||
|
assert MsgKind.QUERY_PARAMS == 0x03
|
||||||
|
assert MsgKind.EXECUTE == 0x04
|
||||||
|
assert MsgKind.BATCH == 0x05
|
||||||
|
assert MsgKind.TRANSACTION == 0x06
|
||||||
|
assert MsgKind.CLOSE == 0x07
|
||||||
|
assert MsgKind.PING == 0x08
|
||||||
|
assert MsgKind.AUTH == 0x09
|
||||||
|
|
||||||
|
def test_server_kinds(self):
|
||||||
|
assert MsgKind.SERVER_HANDSHAKE == 0x80
|
||||||
|
assert MsgKind.READY == 0x81
|
||||||
|
assert MsgKind.DATA == 0x82
|
||||||
|
assert MsgKind.COMPLETE == 0x83
|
||||||
|
assert MsgKind.ERROR == 0x84
|
||||||
|
assert MsgKind.AUTH_CHALLENGE == 0x85
|
||||||
|
assert MsgKind.AUTH_OK == 0x86
|
||||||
|
assert MsgKind.SCHEMA_CHANGE == 0x87
|
||||||
|
assert MsgKind.PONG == 0x88
|
||||||
|
assert MsgKind.TRANSACTION_STATE == 0x89
|
||||||
|
|
||||||
|
def test_result_formats(self):
|
||||||
|
assert ResultFormat.BINARY == 0x00
|
||||||
|
assert ResultFormat.JSON == 0x01
|
||||||
|
assert ResultFormat.TEXT == 0x02
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
*.rs.bk
|
||||||
|
*.pdb
|
||||||
Generated
-7
@@ -1,7 +0,0 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 4
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "baradb"
|
|
||||||
version = "0.1.0"
|
|
||||||
+15
-2
@@ -1,8 +1,21 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "baradb"
|
name = "baradb"
|
||||||
version = "0.1.0"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "BaraDB client library for Rust — binary protocol client"
|
authors = ["BaraDB Team <team@baradb.dev>"]
|
||||||
|
description = "Official Rust client for BaraDB — binary protocol client"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
|
repository = "https://github.com/barabadb/baradadb"
|
||||||
|
documentation = "https://docs.baradb.dev"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["database", "baradb", "multimodal", "client", "wire-protocol"]
|
||||||
|
categories = ["database", "network-programming"]
|
||||||
|
rust-version = "1.70"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "basic"
|
||||||
|
path = "examples/basic.rs"
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# BaraDB Rust Client
|
||||||
|
|
||||||
|
Official 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
|
||||||
|
- **Query builder** — fluent SQL construction
|
||||||
|
- **Vector & JSON support** — first-class multimodal types
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Add to your `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
baradb = "1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or from source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/barabadb/baradadb.git
|
||||||
|
cd clients/rust
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```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")?;
|
||||||
|
for row in result.rows() {
|
||||||
|
println!("{:?}", row);
|
||||||
|
}
|
||||||
|
client.close();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameterized Queries
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use baradb::{Client, WireValue};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut client = Client::connect("localhost", 9472)?;
|
||||||
|
let result = client.query_params(
|
||||||
|
"SELECT * FROM users WHERE age > $1 AND country = $2",
|
||||||
|
&[WireValue::Int64(18), WireValue::String("BG".to_string())],
|
||||||
|
)?;
|
||||||
|
for row in result.rows() {
|
||||||
|
println!("{:?}", row);
|
||||||
|
}
|
||||||
|
client.close();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Builder
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use baradb::Client;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut client = Client::connect("localhost", 9472)?;
|
||||||
|
let result = baradb::QueryBuilder::new(&mut client)
|
||||||
|
.select(&["name", "email"])
|
||||||
|
.from("users")
|
||||||
|
.where_clause("active = true")
|
||||||
|
.order_by("name", "ASC")
|
||||||
|
.limit(10)
|
||||||
|
.exec()?;
|
||||||
|
for row in result.rows() {
|
||||||
|
println!("{:?}", row);
|
||||||
|
}
|
||||||
|
client.close();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vector Search
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use baradb::{Client, WireValue};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut client = Client::connect("localhost", 9472)?;
|
||||||
|
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();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
Unit tests (no server):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test --lib
|
||||||
|
```
|
||||||
|
|
||||||
|
Integration tests (requires server on `localhost:9472`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start server
|
||||||
|
docker run -d -p 9472:9472 baradb:latest
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### `Client::connect(host, port)`
|
||||||
|
|
||||||
|
Creates a new client connected to the given host and port.
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
- `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
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache-2.0
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// BaraDB Rust Client — Basic Examples
|
||||||
|
// Make sure BaraDB is running on localhost:9472.
|
||||||
|
|
||||||
|
use baradb::{Client, QueryBuilder, WireValue};
|
||||||
|
|
||||||
|
fn example_connection() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== Connection ===");
|
||||||
|
let mut client = Client::connect("127.0.0.1", 9472)?;
|
||||||
|
println!("Connected: {}", client.is_connected());
|
||||||
|
println!("Ping: {}", client.ping()?);
|
||||||
|
client.close();
|
||||||
|
println!("Connected after close: {}", client.is_connected());
|
||||||
|
println!();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn example_simple_query() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== Simple Query ===");
|
||||||
|
let mut client = Client::connect("127.0.0.1", 9472)?;
|
||||||
|
let result = client.query("SELECT 42 as answer, 'BaraDB' as db")?;
|
||||||
|
println!("Columns: {:?}", result.columns());
|
||||||
|
println!("Row count: {}", result.row_count());
|
||||||
|
for row in result.rows() {
|
||||||
|
println!(" {:?}", row);
|
||||||
|
}
|
||||||
|
client.close();
|
||||||
|
println!();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn example_parameterized_query() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== Parameterized Query ===");
|
||||||
|
let mut client = Client::connect("127.0.0.1", 9472)?;
|
||||||
|
let result = client.query_params(
|
||||||
|
"SELECT $1 as num, $2 as txt, $3 as flag",
|
||||||
|
&[
|
||||||
|
WireValue::Int64(123),
|
||||||
|
WireValue::String("hello world".to_string()),
|
||||||
|
WireValue::Bool(true),
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
for row in result.rows() {
|
||||||
|
println!(" {:?}", row);
|
||||||
|
}
|
||||||
|
client.close();
|
||||||
|
println!();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn example_query_builder() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== Query Builder ===");
|
||||||
|
let mut client = Client::connect("127.0.0.1", 9472)?;
|
||||||
|
let sql = QueryBuilder::new(&mut client)
|
||||||
|
.select(&["id", "name"])
|
||||||
|
.from("users")
|
||||||
|
.where_clause("active = true")
|
||||||
|
.order_by("name", "ASC")
|
||||||
|
.limit(5)
|
||||||
|
.build();
|
||||||
|
println!("Generated SQL: {}", sql);
|
||||||
|
client.close();
|
||||||
|
println!();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn example_vector() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== Vector Value ===");
|
||||||
|
let mut client = Client::connect("127.0.0.1", 9472)?;
|
||||||
|
let result = client.query_params(
|
||||||
|
"SELECT $1 as embedding",
|
||||||
|
&[WireValue::Vector(vec![0.1, 0.2, 0.3, 0.4])],
|
||||||
|
)?;
|
||||||
|
for row in result.rows() {
|
||||||
|
println!(" {:?}", row);
|
||||||
|
}
|
||||||
|
client.close();
|
||||||
|
println!();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn example_ddl_dml() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("=== DDL & DML ===");
|
||||||
|
let mut client = Client::connect("127.0.0.1", 9472)?;
|
||||||
|
|
||||||
|
let _ = client.execute("DROP TABLE IF EXISTS demo_products");
|
||||||
|
|
||||||
|
client.execute(
|
||||||
|
"CREATE TABLE demo_products (id INT PRIMARY KEY, name STRING, price FLOAT)",
|
||||||
|
)?;
|
||||||
|
let affected = client.execute(
|
||||||
|
"INSERT INTO demo_products (id, name, price) VALUES (1, 'Widget', 9.99)",
|
||||||
|
)?;
|
||||||
|
println!("Insert affected rows: {}", affected);
|
||||||
|
|
||||||
|
let result = client.query("SELECT * FROM demo_products")?;
|
||||||
|
println!("Select returned {} row(s)", result.row_count());
|
||||||
|
for row in result.rows() {
|
||||||
|
println!(" {:?}", row);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.execute("DROP TABLE demo_products")?;
|
||||||
|
println!("Table dropped");
|
||||||
|
client.close();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
use baradb::Client;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut client = Client::connect("localhost", 9472).unwrap();
|
||||||
|
println!("Connected: {}", client.is_connected());
|
||||||
|
match client.ping() {
|
||||||
|
Ok(v) => println!("Ping: {}", v),
|
||||||
|
Err(e) => println!("Ping error: {}", e),
|
||||||
|
}
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
@@ -242,6 +242,7 @@ impl Client {
|
|||||||
pub fn connect_with_config(config: Config) -> Result<Self, Box<dyn std::error::Error>> {
|
pub fn connect_with_config(config: Config) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let addr = format!("{}:{}", config.host, config.port);
|
let addr = format!("{}:{}", config.host, config.port);
|
||||||
let stream = TcpStream::connect(&addr)?;
|
let stream = TcpStream::connect(&addr)?;
|
||||||
|
stream.set_nodelay(true)?;
|
||||||
Ok(Client {
|
Ok(Client {
|
||||||
config,
|
config,
|
||||||
stream,
|
stream,
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
// BaraDB Rust Client — Integration Tests
|
||||||
|
// Requires a running BaraDB server on localhost:9472.
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_connect_and_close() {
|
||||||
|
if !server_available() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut client = Client::connect(HOST, PORT).unwrap();
|
||||||
|
assert!(client.is_connected());
|
||||||
|
client.close();
|
||||||
|
assert!(!client.is_connected());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ping() {
|
||||||
|
if !server_available() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut client = Client::connect(HOST, PORT).unwrap();
|
||||||
|
assert!(client.ping().unwrap());
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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();
|
||||||
|
assert!(result.row_count() >= 0);
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parameterized_query() {
|
||||||
|
if !server_available() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut client = Client::connect(HOST, PORT).unwrap();
|
||||||
|
let result = client
|
||||||
|
.query_params(
|
||||||
|
"SELECT $1 as num, $2 as txt",
|
||||||
|
&[WireValue::Int64(42), WireValue::String("hello".to_string())],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(result.row_count() >= 0);
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_create_table_insert_select_drop() {
|
||||||
|
if !server_available() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut client = Client::connect(HOST, PORT).unwrap();
|
||||||
|
|
||||||
|
let _ = client.execute("DROP TABLE IF EXISTS rust_test_users");
|
||||||
|
client
|
||||||
|
.execute("CREATE TABLE rust_test_users (id INT PRIMARY KEY, name STRING, age INT)")
|
||||||
|
.unwrap();
|
||||||
|
let affected = client
|
||||||
|
.execute("INSERT INTO rust_test_users (id, name, age) VALUES (1, 'Alice', 30)")
|
||||||
|
.unwrap();
|
||||||
|
assert!(affected >= 0);
|
||||||
|
|
||||||
|
let result = client
|
||||||
|
.query("SELECT name, age FROM rust_test_users WHERE id = 1")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result.row_count(), 1);
|
||||||
|
|
||||||
|
client.execute("DROP TABLE rust_test_users").unwrap();
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_query_builder_exec() {
|
||||||
|
if !server_available() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut client = Client::connect(HOST, PORT).unwrap();
|
||||||
|
|
||||||
|
let _ = client.execute("DROP TABLE IF EXISTS rust_test_products");
|
||||||
|
client
|
||||||
|
.execute("CREATE TABLE rust_test_products (id INT PRIMARY KEY, name STRING, price FLOAT)")
|
||||||
|
.unwrap();
|
||||||
|
client
|
||||||
|
.execute("INSERT INTO rust_test_products (id, name, price) VALUES (1, 'Widget', 9.99)")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let result = QueryBuilder::new(&mut client)
|
||||||
|
.select(&["name", "price"])
|
||||||
|
.from("rust_test_products")
|
||||||
|
.where_clause("id = 1")
|
||||||
|
.exec()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.row_count(), 1);
|
||||||
|
|
||||||
|
client.execute("DROP TABLE rust_test_products").unwrap();
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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");
|
||||||
|
// Dev server may accept or reject — both are fine
|
||||||
|
match res {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(e) => {
|
||||||
|
let msg = e.to_string();
|
||||||
|
assert!(msg.contains("Auth") || msg.to_lowercase().contains("error"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user