LessDB Architecture

LessDB Architecture

LessDB is a SQL-first analytical database in Rust that scales vertically (one machine, many cores, optional GPU) and horizontally (stateless compute over shared object storage), with ClickHouse-grade compression, DuckDB-grade embeddability, and native MCP access for AI agents.

┌────────────────────────────────────────────────────────────────────────┐
│  Integrations                                                            │
│  CLI (`less`)  │  Python SDK  │  Node SDK  │  HTTP/Arrow API  │  MCP     │
└───────────────────────────────┬────────────────────────────────────────┘
                                │ SQL + Arrow
┌───────────────────────────────▼────────────────────────────────────────┐
│  less-query — DataFusion 55 (SQL parser, optimizer, hash joins,         │
│  aggregations, window functions, EXPLAIN, partitioning)                 │
│                                                                         │
│  LessTableProvider: part pruning (bloom + typed min/max) →              │
│  one parallel ParquetSource scan per surviving part                     │
└───────────────────────────────┬────────────────────────────────────────┘
                                │ parts, metadata
┌───────────────────────────────▼────────────────────────────────────────┐
│  less-engine — table engines                                            │
│                                                                         │
│  MergeTree (local disk)         SharedMergeTree (object storage)        │
│  • insert buffer → flush       • same immutable parts, but in S3-       │
│    → immutable parts            compatible object storage               │
│  • merge → dedup (UNIQUE)     • catalog + part metadata shared:        │
│  • parts under <data>/parts/    any compute node serves any table       │
│  • single-node vertical scale  • horizontal scale = add compute         │
└───────────────────────────────┬────────────────────────────────────────┘
                                │ data.parquet + meta.json
┌───────────────────────────────▼────────────────────────────────────────┐
│  less-storage — part format, codecs, bloom filters, object store        │
│  less-catalog — schemas, table manifests                                │
└────────────────────────────────────────────────────────────────────────┘

1. The data part

A part is the unit of immutability (the MergeTree concept, same as ClickHouse). Each part is a directory (or object-store prefix) containing:

Parts are written once, never mutated. Inserts accumulate in an in-memory buffer and flush into a new part; deletes/updates (v2) are handled by replacing-merges: parts are merged into bigger parts, with uniqueness deduplication applied during the merge.

Why parquet under the hood: it gives us battle-tested columnar encoding, predicate/statistics-based row-group pruning inside every part, and interoperability with the entire Arrow ecosystem (DuckDB, pandas, pyarrow, Spark) — the DuckDB-style "your existing tools just work" property. The part metadata layer (meta.json) is what ClickHouse's sparse primary index and bloom-filter files do; keeping it outside the parquet file means the planner prunes parts without opening them.

2. MergeTree (vertical scale)

INSERT ──► buffer (Arrow batches, in memory)
              │ flush at flush_rows (default 256k)
              ▼
        write_part(): sort by sort key → dedup UNIQUE (keep last) →
        column stats + blooms → data.parquet + meta.json
              │
              ▼
        parts: p0 … pn  (immutable)
              │ merge when parts ≥ auto_merge_parts (or OPTIMIZE)
              ▼
        merge_all(): bounded passes — each pass merges one size-tiered
        selection capped at max_merge_rows (default 4M) input rows →
        sort → dedup → write level+1 part → delete inputs; repeats to
        convergence (parts larger than the cap wait for streaming merges)

3. SharedMergeTree (horizontal scale, cloud-native)

This is the ClickHouse-Cloud architecture the project chose instead of ReplicatedMergeTree: no replicas, no quorum protocol — the shared source of truth is object storage.

                ┌───────────────────────────────┐
                │  shared object storage (S3/… ) │
                │  catalog/<table>.json          │  ◄── table manifests (shared)
                │  tables/<t>/parts/<part>/*     │  ◄── immutable data parts
                └───────────────▲───────────────┘
                                │ same objects for every node
   compute A        compute B        compute C   …
   (stateless)      (stateless)      (stateless)
   local disk:      local disk:      local disk:
   buffers, caches, buffers, caches, buffers, caches,
   local tables     local tables     local tables

Compute and storage are cleanly separated. A node's durable state for SharedMergeTree tables is zero: parts and table manifests both live in the shared store, discovered by listing — a fresh node with an empty local directory sees every table immediately (proven by the multi-node tests in less-engine and less-query). Local disk is ephemeral compute scratch: insert buffers, caches, and any node-local MergeTree tables. Kill a node, start a replacement — nothing is lost, nothing needs rebalancing.

Multi-writer via CAS coordination (less-catalog/metastore.rs): a pluggable MetaStore with put_if_absent + compare_and_swap removes the single-writer assumption. Part publication is a conditional create of meta.json (readers only see parts whose metadata exists; a concurrent publication of the same uuid-named part fails loudly). Merges first claim their input part set under metastore/merge-claims/<table>/<part> (TTL 10 min, expired claims are taken over where update-CAS exists), so two writers can flush and OPTIMIZE concurrently without double-merging rows. Implementations: FileMetaStore (flock-serialized local files) and ObjectMetaStore (claims as objects in the same S3/GCS/Azure bucket — insert-CAS via conditional copy); etcd transactions are the roadmap backend for full update-CAS across machines. Proven by the multi_writer_concurrent_flush_and_optimize test (two nodes, concurrent flushes + concurrent OPTIMIZE → every row exactly once).

3.5 Distributed fan-out (P2 v1)

less-fanout adds a query coordinator over the HTTP SQL API:

4. Query engine (DataFusion)

DataFusion provides the battle-tested SQL front end — parser, type coercion, cost-based optimizer, hash joins (the StarRocks-style join machinery), aggregations, window functions, subqueries — so LessDB inherits a huge, audited SQL surface instead of reimplementing one.

TTL retention: CREATE TABLE … TTL <col> INTERVAL <n> DAY|HOUR|MONTH adds part retention on a Timestamp/Date column; expired rows are dropped by the same replace-parts machinery (whole expired parts removed, mixed parts rewritten) at flush and OPTIMIZE.

DELETE/UPDATE (replace-parts mutations): DataFusion plans DELETE FROM t WHERE … / UPDATE t SET … WHERE … as DML against the table provider; LessTableProvider compiles the predicates/assignments to physical expressions and the engine rewrites affected parts (filter out matching rows, or apply assignments) into fresh immutable parts, then swaps. Buffered rows are flushed first so mutations cover the whole table, and rewritten parts carry the old part's wal_lsn_max so WAL replay can never resurrect deleted rows. SharedMergeTree mutations claim each part in the metastore exactly like merges (a claim conflict fails loudly instead of partially applying). Mutation marks + background rewrite is the roadmap replacement for hot paths.

The piece LessDB owns is LessTableProvider:

  1. Prune parts with PartMeta only (no data I/O):
    • col = X on a UNIQUE column → bloom filter says "definitely absent"?
    • skip the part.

    • any col <op> literal → compare the literal against the part's typed
    • min/max; skip when provably disjoint. Comparisons are typed (StatValue), never lexicographic-on-numbers.

  1. Plan one ParquetSource scan per surviving part, each in its own
  2. file group → parallel execution across parts and cores.

  1. Push down the row predicate into the parquet reader when all its
  2. columns are projected (parquet does its own stats pruning + row filter).

EXPLAIN shows everything, including the provider's pushdown decisions.

Memory limits: with memory_limit (bytes, less init --memory-limit N) the DataFusion session runs on a bounded GreedyMemoryPool wrapped in a tracking pool that mirrors reserved bytes into the lessdb_memory_pool_bytes Prometheus gauge. Reservations beyond the cap fail the query with ResourcesExhausted instead of exhausting the machine — the first line of admission control. 0 (default) keeps DataFusion's unbounded pool.

5. GPU acceleration

less-gpu (optional, wgpu — Metal on macOS, Vulkan on Linux) provides the kernels that dominate scan cost: filtered sum/dot product (the shape of WHERE … AND SUM(…) and of join/projection cost math), with per-workgroup parallel reduction and a trivial CPU final reduction. The dispatch policy (roadmap): route an operator to the GPU only above a data-size threshold, keep a CPU fallback, and never block the pipeline on PCIe round-trips for small batches. less gpu benchmarks GPU vs CPU on the same data.

5.5 The in-memory tier: contexts & graphs (agent-native memory)

For agentic workloads LessDB ships a RAM-resident tier that replaces Obsidian-style context vaults and standalone graph databases with one embedded store (less-graph + less-memory, persisted under <data_dir>/memory/):

Design notes: adjacency lists give O(degree) traversal; search is a ranked scan (fine at context scale, an inverted index is on the roadmap); graph query surface is the Rust API + openCypher + MCP/CLI tools, with SQL graph table-functions (neighbors(), shortest_path()) planned on top of the same store.

6. Compression

7. Integrations

SurfaceWhatHow
CLIless init/create/insert/sql/optimize/bench/server/mcp/cypherless-cli
HTTPPOST /v1/sql → JSON or Arrow IPC streamless-server (axum)
Pythonin-process engine (DuckDB style), pyarrow/pandas interopPyO3/maturin (abi3-py39+)
Nodein-process engine, Arrow IPC buffersnapi-rs
MCPdatabase tools (less_query, less_explain, less_schema, less_stats, less_tables, less_optimize) + context/graph tools (context_) + memory-table tools (memory_)less-mcp, stdio JSON-RPC
Skillsless-query, less-admin, less-context agent skillsskills/

8. Directory layout

crates/
  less-common   errors, config
  less-storage  codecs, bloom, part format, object store
  less-catalog  TypeSpec schemas, table manifests
  less-engine   MergeTree + SharedMergeTree, buffer/flush/merge
  less-query    DataFusion session + table provider + pruning
  less-server   HTTP API
  less-mcp      MCP server over stdio
  less-gpu      wgpu kernels (optional)
  less-graph    in-memory property graph + context store
  less-cypher   openCypher subset over less-graph
  less-memory   in-memory SQL tables with PK index
  less-fanout   distributed query coordinator (sharded scans + merge)
  less-cli      the `less` binary
sdks/python     PyO3 binding (maturin)
sdks/node       napi-rs binding
skills/         installable agent skills
docs/           this document set

9. Security: LDAP / Active Directory

less-auth plugs LDAP/Active Directory authentication into every remote interface (HTTP today; Flight SQL and networked MCP later):

TLS on the HTTP server: less server --tls-cert cert.pem --tls-key key.pem (or tls_cert/tls_key in config.json) serves HTTPS via axum-server + rustls — no separate proxy needed. An e2e test in less-server generates a self-signed cert with rcgen and completes a rustls client handshake against /health.

9.5 Vector search (LanceDB-style)

less-vector adds native embedding search to the query layer:

Roadmap: vector columns inside MergeTree tables with per-part ANN indexes, filtered (pre-filtering) search, PQ for cosine/dot, HNSW.

10. Observability: Prometheus

less-telemetry is a small, dependency-light registry rendered in the Prometheus text format at GET /metrics (also less metrics for the CLI process). Instrumented end to end:

Sample scrape config:

scrape_configs:
  - job_name: lessdb
    static_configs:
      - targets: ["db-host:7080"]
    metrics_path: /metrics

11. Correctness & concurrency notes