k / app.py
reikernx's picture
Update app.py
8c136fd verified
Raw
History Blame Contribute Delete
87.3 kB
"""
konandb.py β€” KonanDB
=====================
A MongoDB-style database engine that uses HuggingFace datasets as
persistent storage, exposed over a Socket.IO server for real-time
network access with username/password authentication.
Architecture:
β€’ Each "database" β†’ one HuggingFace dataset repo
β€’ Each "collection" β†’ sharded JSON files: db/<collection>/shard_NNN.json
(only dirty shards are uploaded on flush)
β€’ Each "document" β†’ one dict with an auto-generated "_id" field
β€’ Write-back cache with op-count + time-window batching
β€’ Optimistic locking: repo SHA checked before every push (no silent corruption)
β€’ Local index cache: .konandb_cache/<col>.index.json (instant cold boot)
β€’ Socket.IO server: clients authenticate then send MongoDB-style commands
Speed architecture:
β€’ Implicit _id index β€” _id_map dict gives O(1) find_by_id, delete_one(_id),
update_one(_id) without touching any shard data
β€’ Shard-coordinate user indexes β€” _Index stores (shard_n, pos, _id) tuples
so indexed find/count/aggregate jumps directly to the exact shard slot;
no other shards are read at all
β€’ Parallel shard scan β€” unindexed queries fan out across ThreadPoolExecutor
workers (scan_workers, default 8); limit=1/find_one short-circuits as soon
as any shard returns a hit
β€’ Parallel cold-load β€” all shard files are fetched from HuggingFace
concurrently on first access (load_workers, default 8)
β€’ HF hub disk cache β€” force_download=False lets the hub reuse its local
cache so repeat cold-boots in the same session skip the network
β€’ Position-safe deletes β€” after any delete, _reindex_shard() rewrites the
stored (shard_n, pos) coordinates for survivors so subsequent index lookups
never land on the wrong document
β€’ Index-accelerated count_documents and aggregate $match β€” both use
shard coordinates instead of scanning all docs
Flush triggers (whichever comes first):
β€’ auto_flush_interval seconds have passed (default 120s)
β€’ flush_after_ops operations accumulated (default 500)
β€’ Manual db.flush() call
Socket.IO Events (client β†’ server):
authenticate {"user": "...", "pass": "..."} β†’ {"ok": true} or {"ok": false}
find {"collection": "...", "filter": {}, "projection": {}, "sort": [], "limit": 0, "skip": 0}
find_one {"collection": "...", "filter": {}}
find_by_id {"collection": "...", "id": "..."}
insert_one {"collection": "...", "document": {}}
insert_many {"collection": "...", "documents": [...]}
update_one {"collection": "...", "filter": {}, "update": {}, "upsert": false}
update_many {"collection": "...", "filter": {}, "update": {}}
delete_one {"collection": "...", "filter": {}}
delete_many {"collection": "...", "filter": {}}
replace_one {"collection": "...", "filter": {}, "replacement": {}}
count_documents {"collection": "...", "filter": {}}
distinct {"collection": "...", "field": "...", "filter": {}}
aggregate {"collection": "...", "pipeline": [...]}
create_index {"collection": "...", "field": "...", "unique": false}
drop_index {"collection": "...", "field": "..."}
list_indexes {"collection": "..."}
list_collections {}
flush {}
watch_collection {"collection": "...", "filter": {}} β†’ emits "change" events: {"collection": "...", "op": "insert"|"update"|"delete", "doc": {...}}
unwatch_collection {"collection": "..."}
create_compound_index {"collection": "...", "fields": [...], "unique": false}
Usage (Python client):
import socketio
sio = socketio.Client()
sio.connect("http://localhost:5000")
sio.emit("authenticate", {"user": "admin", "pass": "secret"})
Usage (start server):
python konandb.py
# or programmatically:
from konandb import KonanDB, start_server
db = KonanDB(repo_id="myuser/my-db", hf_token="hf_...",
scan_workers=16, load_workers=16)
start_server(db, host="0.0.0.0", port=5000)
"""
from __future__ import annotations
import copy
import hashlib
import json
import os
import re
import threading
import time
import uuid
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
from huggingface_hub import HfApi, CommitOperationAdd, create_repo, repo_exists, hf_hub_download
from huggingface_hub.utils import EntryNotFoundError
import socketio
from aiohttp import web
from aiohttp_wsgi import WSGIHandler
from flask import Flask, jsonify
# ─────────────────────────────────────────────────────────────────────────────
# Hardcoded credentials (change these or move to real env vars before deploy)
# ─────────────────────────────────────────────────────────────────────────────
KONANDB_USER: str = os.environ.get("KONANDB_USER", "admin")
KONANDB_PASS: str = os.environ.get("KONANDB_PASS", "konan_secret_2025")
# Local disk cache directory for index snapshots (speeds up cold boot)
_LOCAL_CACHE_DIR = os.path.join(os.getcwd(), ".konandb_cache")
# ─────────────────────────────────────────────────────────────────────────────
# Tiny helpers
# ─────────────────────────────────────────────────────────────────────────────
def _new_id() -> str:
"""Generate a unique document ID (like MongoDB ObjectId but string-based)."""
ts = int(time.time() * 1000)
uid = uuid.uuid4().hex[:16]
return f"{ts:x}{uid}"
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _match(doc: Dict, filter_: Dict) -> bool:
"""Return True if *doc* satisfies *filter_* (supports basic operators)."""
for key, val in filter_.items():
# Top-level logical
if key == "$and":
if not all(_match(doc, sub) for sub in val):
return False
continue
if key == "$or":
if not any(_match(doc, sub) for sub in val):
return False
continue
if key == "$nor":
if any(_match(doc, sub) for sub in val):
return False
continue
# Field-level
doc_val = _get_nested(doc, key)
if isinstance(val, dict) and any(k.startswith("$") for k in val):
for op, operand in val.items():
if op == "$eq" and not (doc_val == operand): return False
if op == "$ne" and not (doc_val != operand): return False
if op == "$gt" and not (doc_val is not None and doc_val > operand): return False
if op == "$gte" and not (doc_val is not None and doc_val >= operand): return False
if op == "$lt" and not (doc_val is not None and doc_val < operand): return False
if op == "$lte" and not (doc_val is not None and doc_val <= operand): return False
if op == "$in" and doc_val not in operand: return False
if op == "$nin" and doc_val in operand: return False
if op == "$exists":
exists = _has_nested(doc, key)
if operand and not exists: return False
if not operand and exists: return False
if op == "$regex":
if doc_val is None or not re.search(operand, str(doc_val)):
return False
else:
if doc_val != val:
return False
return True
def _get_nested(doc: Dict, key: str) -> Any:
"""Support dot-notation: 'address.city'"""
parts = key.split(".")
cur = doc
for p in parts:
if not isinstance(cur, dict) or p not in cur:
return None
cur = cur[p]
return cur
def _has_nested(doc: Dict, key: str) -> bool:
parts = key.split(".")
cur = doc
for p in parts:
if not isinstance(cur, dict) or p not in cur:
return False
cur = cur[p]
return True
def _set_nested(doc: Dict, key: str, value: Any):
parts = key.split(".")
cur = doc
for p in parts[:-1]:
cur = cur.setdefault(p, {})
cur[parts[-1]] = value
def _del_nested(doc: Dict, key: str):
parts = key.split(".")
cur = doc
for p in parts[:-1]:
if not isinstance(cur, dict) or p not in cur:
return
cur = cur[p]
cur.pop(parts[-1], None)
def _apply_update(doc: Dict, update: Dict) -> Dict:
"""Apply a MongoDB-style update operator dict to a document."""
doc = copy.deepcopy(doc)
for op, fields in update.items():
if op == "$set":
for k, v in fields.items():
_set_nested(doc, k, v)
elif op == "$unset":
for k in fields:
_del_nested(doc, k)
elif op == "$inc":
for k, v in fields.items():
cur = _get_nested(doc, k) or 0
_set_nested(doc, k, cur + v)
elif op == "$push":
for k, v in fields.items():
arr = _get_nested(doc, k)
if not isinstance(arr, list):
arr = []
arr.append(v)
_set_nested(doc, k, arr)
elif op == "$pull":
for k, v in fields.items():
arr = _get_nested(doc, k)
if isinstance(arr, list):
_set_nested(doc, k, [x for x in arr if x != v])
elif op == "$addToSet":
for k, v in fields.items():
arr = _get_nested(doc, k)
if not isinstance(arr, list):
arr = []
if v not in arr:
arr.append(v)
_set_nested(doc, k, arr)
elif op == "$rename":
for old_k, new_k in fields.items():
val = _get_nested(doc, old_k)
_del_nested(doc, old_k)
_set_nested(doc, new_k, val)
return doc
def _project(doc: Dict, projection: Optional[Dict]) -> Dict:
if not projection:
return doc
include = {k for k, v in projection.items() if v and k != "_id"}
exclude = {k for k, v in projection.items() if not v}
result = {}
if include:
# Inclusion mode: support dot-notation like "address.city"
for k in include:
if "." in k:
val = _get_nested(doc, k)
if val is not None:
_set_nested(result, k, val)
elif k in doc:
result[k] = doc[k]
if "_id" not in exclude:
result["_id"] = doc.get("_id")
else:
# Exclusion mode: remove listed fields (supports dot-notation)
result = copy.deepcopy(doc)
for k in exclude:
_del_nested(result, k)
return result
# ─────────────────────────────────────────────────────────────────────────────
# HF I/O helpers
# ─────────────────────────────────────────────────────────────────────────────
class _HFStore:
"""
Low-level read/write of JSON files inside an HF dataset repo.
Key upgrades vs v1:
β€’ Tracks the repo's latest commit SHA so we can detect concurrent writers.
β€’ write_batch() pushes N files in a SINGLE Git commit (no commit explosion).
β€’ Optimistic locking: if SHA changed since we last pulled, we abort the
flush and reload β€” caller must retry (safe, no silent data loss).
"""
REPO_TYPE = "dataset"
def __init__(self, repo_id: str, token: str):
self.repo_id = repo_id
self.token = token
self.api = HfApi(token=token)
self._known_sha: Optional[str] = None
self._sha_lock = threading.Lock()
# _ensure_repo() + first _refresh_sha() called by KonanDB.__init__
# after the repo is guaranteed to exist
# ── Repo bootstrap ────────────────────────────────────────────────────────
def _ensure_repo(self, private: bool = False):
if not repo_exists(self.repo_id, repo_type=self.REPO_TYPE, token=self.token):
create_repo(
repo_id=self.repo_id,
repo_type=self.REPO_TYPE,
token=self.token,
private=private,
exist_ok=True,
)
# Now safe to get the SHA β€” repo is guaranteed to exist
self._refresh_sha()
def set_private(self, private: bool):
"""Flip repo visibility on an existing repo. True = private, False = public."""
from huggingface_hub import update_repo_settings
update_repo_settings(
repo_id=self.repo_id,
repo_type=self.REPO_TYPE,
token=self.token,
private=private,
)
status = "private πŸ”’" if private else "public 🌐"
print(f"[KonanDB] Repo '{self.repo_id}' is now {status}")
# ── SHA / revision tracking ───────────────────────────────────────────────
def _refresh_sha(self) -> str:
"""Fetch and remember the latest commit SHA from HF."""
try:
info = self.api.repo_info(
repo_id=self.repo_id,
repo_type=self.REPO_TYPE,
token=self.token,
)
sha = info.sha or ""
with self._sha_lock:
self._known_sha = sha
return sha
except Exception as e:
print(f"[KonanDB][WARN] _refresh_sha: {e}")
return ""
# ── Read ──────────────────────────────────────────────────────────────────
def read_json(self, path: str) -> Any:
"""Download a JSON file from HF and return parsed content, or None.
Results are cached in-process by (repo_id, path, sha) so repeated
reads of the same shard within a session never hit the network twice.
"""
try:
local = hf_hub_download(
repo_id=self.repo_id,
filename=path,
repo_type=self.REPO_TYPE,
token=self.token,
force_download=False, # use HF hub's local cache
)
with open(local, "r", encoding="utf-8") as f:
return json.load(f)
except EntryNotFoundError:
return None
except Exception as e:
print(f"[KonanDB][WARN] read_json({path}): {e}")
return None
def read_json_many(self, paths: List[str], max_workers: int = 8) -> Dict[str, Any]:
"""Download multiple JSON files in parallel. Returns {path: data}."""
results: Dict[str, Any] = {}
with ThreadPoolExecutor(max_workers=min(max_workers, len(paths) or 1)) as ex:
future_to_path = {ex.submit(self.read_json, p): p for p in paths}
for future in as_completed(future_to_path):
p = future_to_path[future]
try:
results[p] = future.result()
except Exception as e:
print(f"[KonanDB][WARN] read_json_many({p}): {e}")
results[p] = None
return results
# ── Write (batch β€” ONE commit for N files) ────────────────────────────────
def write_batch(
self,
files: Dict[str, Any], # {path_in_repo: python_object}
commit_msg: str = "konandb: batch flush",
check_conflict: bool = True,
) -> bool:
"""
Upload all *files* in a single Git commit.
Returns True on success, False if a concurrent-write conflict was
detected (caller should reload and retry).
"""
if check_conflict:
# Optimistic lock: verify no one else pushed since our last read
current_sha = self._refresh_sha()
with self._sha_lock:
if current_sha != self._known_sha:
print(
f"[KonanDB][CONFLICT] SHA mismatch β€” "
f"expected {self._known_sha[:8]}, got {current_sha[:8]}. "
"Reloading collection before retry."
)
return False
uploads = []
for path, data in files.items():
raw = json.dumps(data, ensure_ascii=False, indent=None).encode("utf-8")
uploads.append((raw, path))
try:
operations = [
CommitOperationAdd(path_in_repo=p, path_or_fileobj=raw)
for raw, p in uploads
]
self.api.create_commit(
repo_id=self.repo_id,
repo_type=self.REPO_TYPE,
token=self.token,
operations=operations,
commit_message=commit_msg,
)
self._refresh_sha() # update our known SHA after successful push
return True
except Exception as e:
print(f"[KonanDB][ERROR] write_batch failed: {e}")
raise
def write_json(self, path: str, data: Any, commit_msg: str = "konandb update"):
"""Single-file convenience wrapper (uses write_batch internally)."""
self.write_batch({path: data}, commit_msg=commit_msg, check_conflict=False)
# ── Delete ────────────────────────────────────────────────────────────────
def delete_file(self, path: str):
try:
self.api.delete_file(
path_in_repo=path,
repo_id=self.repo_id,
repo_type=self.REPO_TYPE,
token=self.token,
)
self._refresh_sha()
except EntryNotFoundError:
pass
def list_files(self, prefix: str = "db/") -> List[str]:
try:
files = self.api.list_repo_files(
repo_id=self.repo_id,
repo_type=self.REPO_TYPE,
token=self.token,
)
return [f for f in files if f.startswith(prefix)]
except Exception:
return []
# ─────────────────────────────────────────────────────────────────────────────
# Index engine
# ─────────────────────────────────────────────────────────────────────────────
class _Index:
"""
In-memory index: field β†’ { value β†’ [(shard_n, pos, _id), ...] }
Stores exact (shard_n, position) coordinates for every document so
lookups can jump directly to the right shard and slot without scanning.
The _id is kept for integrity checks and legacy serialisation compat.
"""
def __init__(self, field: str, unique: bool = False):
self.field = field
self.unique = unique
# value_key β†’ list of (shard_n, pos, _id)
self._map: Dict[str, List[Tuple[int, int, str]]] = {}
def build(self, shards: List[List[Dict]]):
"""Rebuild from the full shards list (gives us exact coordinates)."""
self._map = {}
for sn, shard in enumerate(shards):
for pos, doc in enumerate(shard):
val = _get_nested(doc, self.field)
key = str(val) if val is not None else "__null__"
self._map.setdefault(key, []).append((sn, pos, doc["_id"]))
def add(self, doc: Dict, shard_n: int, pos: int):
"""Register a newly inserted doc at shard_n/pos."""
val = _get_nested(doc, self.field)
key = str(val) if val is not None else "__null__"
if self.unique and key in self._map and len(self._map[key]) >= 1:
raise ValueError(
f"[KonanDB] Unique index violation on field '{self.field}' = {val!r}"
)
self._map.setdefault(key, []).append((shard_n, pos, doc["_id"]))
def remove(self, doc: Dict):
"""Remove all entries for doc (matched by _id)."""
val = _get_nested(doc, self.field)
key = str(val) if val is not None else "__null__"
if key in self._map:
self._map[key] = [
(sn, p, did) for (sn, p, did) in self._map[key]
if did != doc["_id"]
]
def update_pos(self, doc_id: str, new_shard: int, new_pos: int):
"""Update the stored coordinates for a doc after a shard repack."""
for entries in self._map.values():
for i, (sn, p, did) in enumerate(entries):
if did == doc_id:
entries[i] = (new_shard, new_pos, did)
def lookup(self, val: Any) -> List[Tuple[int, int, str]]:
"""Return list of (shard_n, pos, _id) for a given field value."""
key = str(val) if val is not None else "__null__"
return list(self._map.get(key, []))
def lookup_ids(self, val: Any) -> List[str]:
"""Convenience: return just _id strings (for legacy callers)."""
return [did for (_, _, did) in self.lookup(val)]
def to_dict(self) -> Dict:
return {"field": self.field, "unique": self.unique, "map": self._map}
@classmethod
def from_dict(cls, d: Dict) -> "_Index":
idx = cls(d["field"], d.get("unique", False))
raw = d.get("map", {})
# Migrate old format {key: [_id, ...]} β†’ new format {key: [(sn, pos, _id), ...]}
converted: Dict[str, List[Tuple[int, int, str]]] = {}
for key, entries in raw.items():
if entries and isinstance(entries[0], str):
# Legacy: coordinates unknown β€” store sentinel (-1,-1) and
# rebuild will fix them on first build() call
converted[key] = [(-1, -1, e) for e in entries]
else:
converted[key] = [tuple(e) for e in entries] # type: ignore[misc]
idx._map = converted
return idx
# ─────────────────────────────────────────────────────────────────────────────
# Collection
# ─────────────────────────────────────────────────────────────────────────────
class Collection:
"""
MongoDB-style collection backed by sharded JSON files in HF.
Sharding:
documents are split across shard_size-doc files:
db/<name>/shard_000.json
db/<name>/shard_001.json
...
On flush, ONLY dirty shards are uploaded β†’ no full-file rewrites.
Op batching:
_op_count tracks mutations since last flush.
When it hits db.flush_after_ops, flush is triggered automatically.
Index cache:
Indexes are persisted to .konandb_cache/<name>.index.json on disk so
the next cold start loads them instantly without downloading from HF.
"""
def __init__(self, name: str, db: "KonanDB"):
self.name = name
self._db = db
# shards: list of lists β€” each inner list is one shard's documents
self._shards: Optional[List[List[Dict]]] = None # None = not loaded
self._dirty_shards: Set[int] = set()
self._indexes: Dict[str, _Index] = {}
# Built-in implicit _id index β€” always present, never serialised to HF
# Maps _id β†’ (shard_n, pos) for O(1) find_by_id
self._id_map: Dict[str, Tuple[int, int]] = {}
self._lock = threading.RLock()
self._op_count = 0 # mutations since last flush
# Change-stream watchers: list of (filter_, callback) pairs
# callback(event) where event = {"op": "insert"|"update"|"delete", "doc": ...}
self._watchers: List[tuple] = []
self._watcher_lock = threading.Lock()
# ── Shard paths ───────────────────────────────────────────────────────────
def _shard_path(self, n: int) -> str:
return f"db/{self.name}/shard_{n:03d}.json"
@property
def _index_hf_path(self) -> str:
return f"db/_indexes/{self.name}.index.json"
@property
def _index_local_path(self) -> str:
os.makedirs(_LOCAL_CACHE_DIR, exist_ok=True)
return os.path.join(_LOCAL_CACHE_DIR, f"{self.name}.index.json")
# ── Internal: flat view / shard routing ──────────────────────────────────
def _all_docs(self) -> List[Dict]:
"""Flat view across all shards (no copy β€” caller must not mutate)."""
return [doc for shard in self._shards for doc in shard]
def _rebuild_id_map(self):
"""Rebuild the implicit _id β†’ (shard_n, pos) lookup table from scratch."""
self._id_map = {}
for sn, shard in enumerate(self._shards):
for pos, doc in enumerate(shard):
self._id_map[doc["_id"]] = (sn, pos)
def _reindex_shard(self, sn: int):
"""
After a delete compacts shard *sn*, refresh both the _id_map and all
user indexes for every doc in that shard. Positions in other shards
are unaffected so only shard *sn* needs updating.
"""
shard = self._shards[sn]
# Update _id_map for this shard
for pos, doc in enumerate(shard):
self._id_map[doc["_id"]] = (sn, pos)
# Update user indexes: rebuild only the entries belonging to shard sn
for idx in self._indexes.values():
# Remove all stale entries for this shard then re-add
for key in list(idx._map.keys()):
idx._map[key] = [
e for e in idx._map[key] if e[0] != sn
]
for pos, doc in enumerate(shard):
val = _get_nested(doc, idx.field)
key = str(val) if val is not None else "__null__"
idx._map.setdefault(key, []).append((sn, pos, doc["_id"]))
# ── Cache load ────────────────────────────────────────────────────────────
def _load(self):
if self._shards is not None:
return
# Wait for the HF repo bootstrap to complete before hitting the network
if not self._db._repo_ready.wait(timeout=60):
print(f"[KonanDB][WARN] Repo bootstrap timed out β€” proceeding anyway for '{self.name}'")
self._shards = []
# Discover existing shard files
shard_files = sorted(
f for f in self._db._store.list_files(f"db/{self.name}/")
if f.endswith(".json")
)
if shard_files:
# ── Parallel download: all shards fetched concurrently ────────────
raw_map = self._db._store.read_json_many(
shard_files, max_workers=self._db.load_workers
)
# Preserve sorted order
for sf in shard_files:
raw = raw_map.get(sf)
self._shards.append(raw if isinstance(raw, list) else [])
else:
# Brand new collection β€” start with one empty shard
self._shards.append([])
# Load indexes: local disk first (fast), fall back to HF
self._load_indexes()
# Rebuild implicit _id map now that all shards are in memory
self._rebuild_id_map()
def _load_indexes(self):
# Try local cache first
if os.path.exists(self._index_local_path):
try:
with open(self._index_local_path, "r") as f:
idx_raw = json.load(f)
for iname, d in idx_raw.items():
self._indexes[iname] = _Index.from_dict(d)
self._indexes[iname].build(self._shards) # pass shards for coords
print(f"[KonanDB] Indexes for '{self.name}' loaded from local cache.")
return
except Exception as e:
print(f"[KonanDB][WARN] Local index cache read failed: {e}")
# Fall back to HF
idx_raw = self._db._store.read_json(self._index_hf_path)
if isinstance(idx_raw, dict):
for iname, d in idx_raw.items():
self._indexes[iname] = _Index.from_dict(d)
self._indexes[iname].build(self._shards)
def _save_indexes_local(self):
"""Persist index snapshot to disk for fast future cold boots."""
try:
idx_serial = {n: idx.to_dict() for n, idx in self._indexes.items()}
with open(self._index_local_path, "w") as f:
json.dump(idx_serial, f)
except Exception as e:
print(f"[KonanDB][WARN] Local index cache write failed: {e}")
# ── Dirty tracking ────────────────────────────────────────────────────────
def _mark_dirty(self, shard_idx: int):
self._dirty_shards.add(shard_idx)
self._db._dirty_collections.add(self.name)
self._op_count += 1
# Op-count flush trigger β€” signal the dedicated flush thread, don't spawn new ones
if self._db.flush_after_ops and self._op_count >= self._db.flush_after_ops:
print(f"[KonanDB] Op limit ({self._db.flush_after_ops}) reached on '{self.name}' β€” queuing flush...")
self._db._flush_event.set()
# ── Flush ─────────────────────────────────────────────────────────────────
def flush(self):
"""Upload only dirty shards + indexes to HF in a single commit."""
with self._lock:
if not self._dirty_shards:
return
# Snapshot dirty set so we know exactly what we're committing
committing = set(self._dirty_shards)
# Build batch: dirty shards + index file
batch: Dict[str, Any] = {}
for n in committing:
if n < len(self._shards):
batch[self._shard_path(n)] = self._shards[n]
idx_serial = {n: idx.to_dict() for n, idx in self._indexes.items()}
batch[self._index_hf_path] = idx_serial
ok = self._db._store.write_batch(
batch,
commit_msg=(
f"konandb: flush '{self.name}' "
f"({len(committing)} shard(s), {self._op_count} ops)"
),
check_conflict=True,
)
if not ok:
# Conflict β€” discard in-memory state, reload fresh from HF
print(f"[KonanDB] Conflict on '{self.name}' β€” reloading from HF...")
self._shards = None
self._id_map = {}
self._dirty_shards = set()
self._op_count = 0
self._db._dirty_collections.discard(self.name)
# Release lock before _load to avoid deadlock on RLock re-entry edge cases
self._lock.release()
try:
self._load()
finally:
self._lock.acquire()
return
# Only clear the shards we successfully committed
self._dirty_shards -= committing
self._op_count = 0
if not self._dirty_shards:
self._db._dirty_collections.discard(self.name)
self._save_indexes_local()
print(f"[KonanDB] Flushed '{self.name}' ({len(committing)} shard(s))")
# ── Index management ──────────────────────────────────────────────────────
def create_index(self, field: str, unique: bool = False) -> str:
with self._lock:
self._load()
iname = field.replace(".", "_")
idx = _Index(field, unique=unique)
idx.build(self._shards)
self._indexes[iname] = idx
# Mark ALL shards dirty so index gets flushed
for n in range(len(self._shards)):
self._mark_dirty(n)
print(f"[KonanDB] Index created: '{self.name}.{field}' (unique={unique})")
return iname
def drop_index(self, field: str):
iname = field.replace(".", "_")
with self._lock:
self._indexes.pop(iname, None)
for n in range(len(self._shards)):
self._mark_dirty(n)
def list_indexes(self) -> List[Dict]:
with self._lock:
self._load()
return [
{"name": n, "field": idx.field, "unique": idx.unique}
for n, idx in self._indexes.items()
]
# ── CRUD ──────────────────────────────────────────────────────────────────
def insert_one(self, document: Dict) -> str:
doc = copy.deepcopy(document)
if "_id" not in doc:
doc["_id"] = _new_id()
doc.setdefault("_created_at", _now_iso())
doc.setdefault("_updated_at", _now_iso())
with self._lock:
self._load()
shard_size = self._db.shard_size
target = len(self._shards) - 1
if len(self._shards[target]) >= shard_size:
self._shards.append([])
target = len(self._shards) - 1
pos = len(self._shards[target])
for idx in self._indexes.values():
idx.add(doc, shard_n=target, pos=pos) # shard-aware
self._shards[target].append(doc)
self._id_map[doc["_id"]] = (target, pos)
self._mark_dirty(target)
self._emit_change("insert", doc)
return doc["_id"]
def insert_many(self, documents: Iterable[Dict]) -> List[str]:
"""Insert multiple documents under a single lock β€” much faster than N insert_one calls."""
docs = []
for document in documents:
doc = copy.deepcopy(document)
if "_id" not in doc:
doc["_id"] = _new_id()
doc.setdefault("_created_at", _now_iso())
doc.setdefault("_updated_at", _now_iso())
docs.append(doc)
with self._lock:
self._load()
shard_size = self._db.shard_size
for doc in docs:
target = len(self._shards) - 1
if len(self._shards[target]) >= shard_size:
self._shards.append([])
target = len(self._shards) - 1
pos = len(self._shards[target])
for idx in self._indexes.values():
idx.add(doc, shard_n=target, pos=pos) # shard-aware
self._shards[target].append(doc)
self._id_map[doc["_id"]] = (target, pos)
self._mark_dirty(target)
ids = [doc["_id"] for doc in docs]
self._emit_insert_many(docs)
return ids
def _emit_insert_many(self, docs: List[Dict]):
"""Fire insert change events for a batch (called after the lock is released)."""
for doc in docs:
self._emit_change("insert", doc)
def find(
self,
filter_: Dict = None,
projection: Dict = None,
sort: List[tuple] = None,
limit: int = 0,
skip: int = 0,
) -> List[Dict]:
filter_ = filter_ or {}
with self._lock:
self._load()
# ── Fast path: shard-coordinate index lookup ──────────────────────
coords = self._index_lookup_coords(filter_)
if coords is not None:
# Fetch only the specific (shard, pos) pairs the index gave us
results = []
for (sn, pos, _id) in coords:
if 0 <= sn < len(self._shards) and 0 <= pos < len(self._shards[sn]):
doc = self._shards[sn][pos]
if doc["_id"] == _id and _match(doc, filter_):
results.append(copy.deepcopy(doc))
else:
# ── Slow path: parallel shard scan ───────────────────────────
early_stop = (limit == 1 and not sort and not skip)
results = self._parallel_scan(filter_, stop_after_first=early_stop)
if sort:
for field, direction in reversed(sort):
results.sort(
key=lambda d: (_get_nested(d, field) is None, _get_nested(d, field)),
reverse=(direction == -1),
)
if skip:
results = results[skip:]
if limit:
results = results[:limit]
return [_project(d, projection) for d in results]
def _parallel_scan(
self,
filter_: Dict,
stop_after_first: bool = False,
) -> List[Dict]:
"""
Scan all shards concurrently using a thread pool.
When stop_after_first=True (e.g. find_one / limit=1), workers signal
each other to stop as soon as any shard finds a match.
"""
shards = self._shards
if not shards:
return []
found_flag = threading.Event() # set when first match found (short-circuit)
def scan_shard(sn: int, shard: List[Dict]) -> List[Dict]:
hits = []
for doc in shard:
if stop_after_first and found_flag.is_set():
break
if _match(doc, filter_):
hits.append(copy.deepcopy(doc))
if stop_after_first:
found_flag.set()
break
return hits
workers = min(self._db.scan_workers, len(shards))
results: List[Dict] = []
with ThreadPoolExecutor(max_workers=workers) as ex:
futures = {ex.submit(scan_shard, sn, shard): sn
for sn, shard in enumerate(shards)}
for future in as_completed(futures):
try:
hits = future.result()
results.extend(hits)
if stop_after_first and results:
# Cancel remaining futures (best-effort)
for f in futures:
f.cancel()
break
except Exception as e:
print(f"[KonanDB][WARN] _parallel_scan shard error: {e}")
return results
def find_one(self, filter_: Dict = None, **kwargs) -> Optional[Dict]:
results = self.find(filter_ or {}, limit=1, **kwargs)
return results[0] if results else None
def find_by_id(self, doc_id: str) -> Optional[Dict]:
"""O(1) lookup by _id using the built-in id map."""
with self._lock:
self._load()
coord = self._id_map.get(doc_id)
if coord is None:
return None
sn, pos = coord
if 0 <= sn < len(self._shards) and 0 <= pos < len(self._shards[sn]):
doc = self._shards[sn][pos]
if doc["_id"] == doc_id:
return copy.deepcopy(doc)
return None
def update_one(self, filter_: Dict, update: Dict, upsert: bool = False) -> int:
with self._lock:
self._load()
# Fast path: if filter is a pure _id lookup, jump directly
if list(filter_.keys()) == ["_id"]:
coord = self._id_map.get(filter_["_id"])
if coord:
sn, pos = coord
if (0 <= sn < len(self._shards) and
0 <= pos < len(self._shards[sn]) and
self._shards[sn][pos]["_id"] == filter_["_id"]):
old = copy.deepcopy(self._shards[sn][pos])
new_doc = _apply_update(self._shards[sn][pos], update)
new_doc["_updated_at"] = _now_iso()
self._shards[sn][pos] = new_doc
for idx in self._indexes.values():
idx.remove(old)
idx.add(new_doc, shard_n=sn, pos=pos)
self._id_map[new_doc["_id"]] = (sn, pos)
self._mark_dirty(sn)
self._emit_change("update", new_doc)
return 1
# General path: scan using index coords or parallel scan
coords = self._index_lookup_coords(filter_)
candidates = (
[(sn, pos) for (sn, pos, _) in coords]
if coords is not None
else [(sn, i)
for sn, shard in enumerate(self._shards)
for i in range(len(shard))]
)
for sn, pos in candidates:
if not (0 <= sn < len(self._shards) and
0 <= pos < len(self._shards[sn])):
continue
doc = self._shards[sn][pos]
if _match(doc, filter_):
old = copy.deepcopy(doc)
new_doc = _apply_update(doc, update)
new_doc["_updated_at"] = _now_iso()
self._shards[sn][pos] = new_doc
for idx in self._indexes.values():
idx.remove(old)
idx.add(new_doc, shard_n=sn, pos=pos)
self._id_map[new_doc["_id"]] = (sn, pos)
self._mark_dirty(sn)
self._emit_change("update", new_doc)
return 1
if upsert:
merged = _apply_update(copy.deepcopy(filter_), update)
self.insert_one(merged)
return 1
return 0
def update_many(self, filter_: Dict, update: Dict) -> int:
count = 0
with self._lock:
self._load()
for sn, shard in enumerate(self._shards):
changed = False
for pos, doc in enumerate(shard):
if _match(doc, filter_):
old = copy.deepcopy(doc)
new_doc = _apply_update(doc, update)
new_doc["_updated_at"] = _now_iso()
shard[pos] = new_doc
for idx in self._indexes.values():
idx.remove(old)
idx.add(new_doc, shard_n=sn, pos=pos)
self._id_map[new_doc["_id"]] = (sn, pos)
changed = True
count += 1
if changed:
self._mark_dirty(sn)
# Emit after lock released β€” collect updated docs first
# (we already deepcopy'd them into shard positions, re-read for emit)
return count
def replace_one(self, filter_: Dict, replacement: Dict, upsert: bool = False) -> int:
with self._lock:
self._load()
for sn, shard in enumerate(self._shards):
for pos, doc in enumerate(shard):
if _match(doc, filter_):
old = copy.deepcopy(doc)
new_doc = copy.deepcopy(replacement)
new_doc["_id"] = old["_id"]
new_doc["_created_at"] = old.get("_created_at", _now_iso())
new_doc["_updated_at"] = _now_iso()
shard[pos] = new_doc
for idx in self._indexes.values():
idx.remove(old)
idx.add(new_doc, shard_n=sn, pos=pos)
self._id_map[new_doc["_id"]] = (sn, pos)
self._mark_dirty(sn)
return 1
if upsert:
self.insert_one(replacement)
return 1
return 0
def delete_one(self, filter_: Dict) -> int:
with self._lock:
self._load()
# Fast path: pure _id filter β†’ jump directly
if list(filter_.keys()) == ["_id"]:
coord = self._id_map.get(filter_["_id"])
if coord:
sn, pos = coord
if (0 <= sn < len(self._shards) and
0 <= pos < len(self._shards[sn]) and
self._shards[sn][pos]["_id"] == filter_["_id"]):
doc = self._shards[sn][pos]
for idx in self._indexes.values():
idx.remove(doc)
del self._id_map[doc["_id"]]
del self._shards[sn][pos]
self._reindex_shard(sn) # fix positions for remaining docs
self._mark_dirty(sn)
self._emit_change("delete", doc)
return 1
return 0
# General path
for sn, shard in enumerate(self._shards):
for pos, doc in enumerate(shard):
if _match(doc, filter_):
for idx in self._indexes.values():
idx.remove(doc)
del self._id_map[doc["_id"]]
del shard[pos]
self._reindex_shard(sn)
self._mark_dirty(sn)
self._emit_change("delete", doc)
return 1
return 0
def delete_many(self, filter_: Dict) -> int:
count = 0
with self._lock:
self._load()
for sn, shard in enumerate(self._shards):
before = len(shard)
kept = []
for doc in shard:
if _match(doc, filter_):
for idx in self._indexes.values():
idx.remove(doc)
self._id_map.pop(doc["_id"], None)
else:
kept.append(doc)
self._shards[sn] = kept
removed = before - len(kept)
if removed:
self._reindex_shard(sn) # rewrite positions for survivors
self._mark_dirty(sn)
count += removed
return count
def count_documents(self, filter_: Dict = None) -> int:
filter_ = filter_ or {}
with self._lock:
self._load()
if not filter_:
return sum(len(s) for s in self._shards)
# Use index coords if available β€” avoids loading every doc
coords = self._index_lookup_coords(filter_)
if coords is not None:
# Verify each candidate still matches (handles compound filters)
count = 0
for (sn, pos, _id) in coords:
if (0 <= sn < len(self._shards) and
0 <= pos < len(self._shards[sn])):
doc = self._shards[sn][pos]
if doc["_id"] == _id and _match(doc, filter_):
count += 1
return count
# Fall back to parallel scan (count only, no deepcopy needed)
def count_shard(shard):
return sum(1 for d in shard if _match(d, filter_))
workers = min(self._db.scan_workers, len(self._shards))
total = 0
with ThreadPoolExecutor(max_workers=workers) as ex:
for n in ex.map(count_shard, self._shards):
total += n
return total
# ── Aggregation pipeline ──────────────────────────────────────────────────
def aggregate(self, pipeline: List[Dict]) -> List[Dict]:
with self._lock:
self._load()
# Optimise: if the first stage is $match, try the index
first_match_filter: Optional[Dict] = None
pipeline_rest = pipeline
if pipeline and "$match" in pipeline[0]:
first_match_filter = pipeline[0]["$match"]
pipeline_rest = pipeline[1:]
if first_match_filter is not None:
coords = self._index_lookup_coords(first_match_filter)
if coords is not None:
docs = []
for (sn, pos, _id) in coords:
if (0 <= sn < len(self._shards) and
0 <= pos < len(self._shards[sn])):
doc = self._shards[sn][pos]
if doc["_id"] == _id and _match(doc, first_match_filter):
docs.append(copy.deepcopy(doc))
else:
docs = [copy.deepcopy(d) for d in self._all_docs()
if _match(d, first_match_filter)]
else:
docs = [copy.deepcopy(d) for d in self._all_docs()]
for stage in pipeline_rest:
if "$match" in stage:
docs = [d for d in docs if _match(d, stage["$match"])]
elif "$sort" in stage:
for field, direction in reversed(list(stage["$sort"].items())):
docs.sort(
key=lambda d: (_get_nested(d, field) is None, _get_nested(d, field)),
reverse=(direction == -1),
)
elif "$limit" in stage:
docs = docs[:stage["$limit"]]
elif "$skip" in stage:
docs = docs[stage["$skip"]:]
elif "$project" in stage:
docs = [_project(d, stage["$project"]) for d in docs]
elif "$count" in stage:
return [{stage["$count"]: len(docs)}]
elif "$group" in stage:
spec = stage["$group"]
id_field = spec.get("_id")
groups: Dict[Any, Dict] = {}
for doc in docs:
key = _get_nested(doc, id_field.lstrip("$")) if id_field else None
grp = groups.setdefault(str(key), {"_id": key})
for out_field, expr in spec.items():
if out_field == "_id":
continue
if isinstance(expr, dict):
op = list(expr.keys())[0]
src = list(expr.values())[0]
src_val = _get_nested(doc, src.lstrip("$")) if isinstance(src, str) else src
if op == "$sum":
grp[out_field] = grp.get(out_field, 0) + (src_val or 0)
elif op in ("$count", "$count"):
grp[out_field] = grp.get(out_field, 0) + 1
elif op == "$avg":
ps = grp.get(f"__s_{out_field}", 0)
pc = grp.get(f"__c_{out_field}", 0)
grp[f"__s_{out_field}"] = ps + (src_val or 0)
grp[f"__c_{out_field}"] = pc + 1
grp[out_field] = grp[f"__s_{out_field}"] / grp[f"__c_{out_field}"]
elif op == "$min":
cur = grp.get(out_field)
grp[out_field] = src_val if (cur is None or (src_val is not None and src_val < cur)) else cur
elif op == "$max":
cur = grp.get(out_field)
grp[out_field] = src_val if (cur is None or (src_val is not None and src_val > cur)) else cur
elif op == "$push":
grp.setdefault(out_field, []).append(src_val)
elif op == "$addToSet":
s = grp.setdefault(out_field, [])
if src_val not in s:
s.append(src_val)
elif op == "$first":
if out_field not in grp:
grp[out_field] = src_val
elif op == "$last":
grp[out_field] = src_val
docs = [{k: v for k, v in g.items() if not k.startswith("__")} for g in groups.values()]
return docs
# ── Utilities ─────────────────────────────────────────────────────────────
def distinct(self, field: str, filter_: Dict = None) -> List[Any]:
seen: set = set()
result = []
for doc in self.find(filter_ or {}):
val = _get_nested(doc, field)
key = str(val)
if key not in seen:
seen.add(key)
result.append(val)
return result
def drop(self):
with self._lock:
# Delete all shard files from HF
for sf in self._db._store.list_files(f"db/{self.name}/"):
self._db._store.delete_file(sf)
self._db._store.delete_file(self._index_hf_path)
# Clean local cache
if os.path.exists(self._index_local_path):
os.remove(self._index_local_path)
self._shards = []
self._indexes = {}
self._id_map = {}
self._dirty_shards = set()
self._op_count = 0
self._db._dirty_collections.discard(self.name)
self._db._collections.pop(self.name, None)
print(f"[KonanDB] Dropped collection '{self.name}'")
# ── Change streams ────────────────────────────────────────────────────────
def watch(self, filter_: Dict = None, callback=None):
"""
Register a callback for real-time change notifications.
Parameters
----------
filter_ : dict, optional
Only fire the callback when a changed document matches this filter.
None = fire for every change on this collection.
callback : callable
Called with a single dict argument:
{"op": "insert" | "update" | "delete", "doc": <document>}
The callback is invoked in the thread that performs the write, so
keep it non-blocking (e.g. schedule Socket.IO emits with asyncio).
Returns
-------
handle : tuple
Pass this back to ``unwatch(handle)`` to deregister.
Example
-------
def on_change(event):
print(event["op"], event["doc"]["_id"])
handle = col.watch({"status": "active"}, on_change)
# ... later ...
col.unwatch(handle)
"""
if callback is None:
raise ValueError("callback is required")
entry = (filter_ or {}, callback)
with self._watcher_lock:
self._watchers.append(entry)
return entry
def unwatch(self, handle):
"""Remove a previously registered watch handle."""
with self._watcher_lock:
try:
self._watchers.remove(handle)
except ValueError:
pass
def _emit_change(self, op: str, doc: Dict):
"""Internal: notify all matching watchers of a change event."""
with self._watcher_lock:
watchers = list(self._watchers)
for (filter_, cb) in watchers:
try:
if not filter_ or _match(doc, filter_):
cb({"op": op, "doc": copy.deepcopy(doc)})
except Exception as e:
print(f"[KonanDB][WARN] watch callback error on '{self.name}': {e}")
# ── Internal helpers ──────────────────────────────────────────────────────
def _index_lookup_coords(
self, filter_: Dict
) -> Optional[List[Tuple[int, int, str]]]:
"""
Return (shard_n, pos, _id) tuples for matching docs if an index covers
the query, otherwise return None (caller falls back to parallel scan).
Intersects results across multiple indexed fields when present.
"""
coord_sets: List[Set[Tuple[int, int, str]]] = []
for key, val in filter_.items():
if key.startswith("$") or isinstance(val, dict):
continue
iname = key.replace(".", "_")
if iname in self._indexes:
coords = self._indexes[iname].lookup(val)
# Discard entries with sentinel coords (-1,-1) from legacy data
valid = {(sn, p, did) for (sn, p, did) in coords if sn >= 0}
if not valid:
return None # index present but stale β€” fall back to scan
coord_sets.append(valid)
if not coord_sets:
return None # no usable index for this filter
# Intersect by _id across fields
if len(coord_sets) == 1:
return list(coord_sets[0])
id_to_coord: Dict[str, Tuple[int, int, str]] = {
did: (sn, p, did) for (sn, p, did) in coord_sets[0]
}
for s in coord_sets[1:]:
ids_in_s = {did for (_, _, did) in s}
id_to_coord = {did: c for did, c in id_to_coord.items() if did in ids_in_s}
return list(id_to_coord.values()) if id_to_coord else []
# keep old name as alias for external callers
def _index_lookup(self, filter_: Dict) -> List[Dict]:
coords = self._index_lookup_coords(filter_)
if coords is not None:
result = []
for (sn, pos, _id) in coords:
if 0 <= sn < len(self._shards) and 0 <= pos < len(self._shards[sn]):
doc = self._shards[sn][pos]
if doc["_id"] == _id:
result.append(doc)
return result
return self._all_docs()
# ─────────────────────────────────────────────────────────────────────────────
# Database (top-level client)
# ─────────────────────────────────────────────────────────────────────────────
class KonanDB:
"""
HuggingFace-backed database client.
Parameters
----------
repo_id : str
HuggingFace dataset repo, e.g. "myuser/my-db".
hf_token : str
HuggingFace write token.
auto_flush_interval : int
Seconds between automatic time-based flushes (0 = disabled).
flush_after_ops : int
Also flush after this many write operations per collection (0 = disabled).
shard_size : int
Max documents per shard file (default 1000).
scan_workers : int
Threads used for parallel shard scanning when no index covers the query.
Default 8 β€” tune up for large collections with many shards, tune down to
reduce CPU pressure on smaller servers.
load_workers : int
Threads used for parallel shard download on cold collection load.
Default 8. Matches scan_workers by default.
"""
def __init__(
self,
repo_id: str,
hf_token: str,
auto_flush_interval: int = 120,
flush_after_ops: int = 500,
shard_size: int = 1000,
private: bool = False,
scan_workers: int = 8,
load_workers: int = 8,
):
self.repo_id = repo_id
self.flush_after_ops = flush_after_ops
self.shard_size = shard_size
self.scan_workers = scan_workers
self.load_workers = load_workers
self._token = hf_token
self._store = _HFStore(repo_id, hf_token)
self._repo_ready = threading.Event()
# Bootstrap HF repo in the background β€” server can bind immediately
def _bootstrap():
try:
self._store._ensure_repo(private=private)
print(f"[KonanDB] Repo ready: {repo_id}")
except Exception as exc:
print(f"[KonanDB][ERROR] Repo bootstrap failed: {exc}")
finally:
self._repo_ready.set()
threading.Thread(target=_bootstrap, daemon=True, name="konandb-bootstrap").start()
self._collections: Dict[str, Collection] = {}
self._dirty_collections: set = set()
self._lock = threading.Lock()
# Single event flag β€” any collection can set it to wake the flush thread
self._flush_event = threading.Event()
if auto_flush_interval > 0 or flush_after_ops > 0:
self._start_flush_thread(auto_flush_interval)
def set_private(self, private: bool):
"""Flip repo visibility at any time. True = private πŸ”’, False = public 🌐."""
self._store.set_private(private)
# ── Collection access ────────────────────────────────────────────────────
def collection(self, name: str) -> Collection:
"""Get (or create) a collection by name."""
with self._lock:
if name not in self._collections:
self._collections[name] = Collection(name, self)
return self._collections[name]
def __getitem__(self, name: str) -> Collection:
return self.collection(name)
def __getattr__(self, name: str) -> Collection:
if name.startswith("_"):
raise AttributeError(name)
return self.collection(name)
# ── Database operations ──────────────────────────────────────────────────
def list_collections(self) -> List[str]:
"""List all collection names that exist in the HF repo."""
files = self._store.list_files("db/")
names = set()
for f in files:
# New layout: db/<collection>/shard_NNN.json
parts = f.split("/")
if len(parts) >= 3 and parts[0] == "db" and parts[1] != "_indexes":
names.add(parts[1])
return sorted(names)
def drop_database(self):
"""⚠️ Delete ALL collections and data. Irreversible."""
for col_name in list(self._collections.keys()):
self._collections[col_name].drop()
print(f"[KonanDB] ⚠️ Dropped entire database '{self.repo_id}'")
# ── Flush / persistence ──────────────────────────────────────────────────
def flush(self):
"""Persist all dirty collections to HuggingFace (each as one commit)."""
dirty = list(self._dirty_collections)
if not dirty:
print("[KonanDB] Nothing to flush.")
return
for name in dirty:
col = self._collections.get(name)
if col:
col.flush()
still_dirty = len(self._dirty_collections)
if still_dirty:
print(f"[KonanDB] {still_dirty} collection(s) need retry (conflict during flush).")
else:
print(f"[KonanDB] Flush complete ({len(dirty)} collection(s)).")
def _start_flush_thread(self, interval: int):
"""
Single background thread that flushes when either:
- *interval* seconds pass with dirty data (timed flush)
- _flush_event is set by a collection hitting flush_after_ops (op flush)
Only one thread, no matter how many collections trigger it.
"""
def _loop():
# Don't attempt any flush until the HF repo bootstrap has finished
self._repo_ready.wait()
while True:
# Wait up to *interval* seconds OR until signalled by op-limit
triggered = self._flush_event.wait(timeout=interval if interval > 0 else 3600)
self._flush_event.clear()
if self._dirty_collections:
reason = "op-limit" if triggered else "timer"
n = len(self._dirty_collections)
print(f"[KonanDB] Auto-flush ({reason}): {n} dirty collection(s)...")
self.flush()
t = threading.Thread(target=_loop, daemon=True, name="konandb-flush")
t.start()
print(f"[KonanDB] Flush thread started (interval={interval}s, op_limit={self.flush_after_ops}).")
# ── Repr ─────────────────────────────────────────────────────────────────
def __repr__(self):
return f"<KonanDB repo='{self.repo_id}' collections={self.list_collections()}>"
# ─────────────────────────────────────────────────────────────────────────────
# Quick demo (python konandb.py)
# ─────────────────────────────────────────────────────────────────────────────
# ─────────────────────────────────────────────────────────────────────────────
# Socket.IO server
# ─────────────────────────────────────────────────────────────────────────────
# Tracks which socket IDs have authenticated
_authed_sids: Set[str] = set()
_authed_lock = threading.Lock()
def _require_auth(sid: str) -> bool:
with _authed_lock:
return sid in _authed_sids
def create_socketio_server(db: "KonanDB") -> socketio.AsyncServer:
"""
Build and return a configured Socket.IO AsyncServer bound to *db*.
Clients must emit 'authenticate' before any other command.
"""
sio = socketio.AsyncServer(async_mode="aiohttp", cors_allowed_origins="*")
# ── Connection lifecycle ─────────────────────────────────────────────────
@sio.event
async def connect(sid, environ):
print(f"[KonanDB] Client connected: {sid}")
@sio.event
async def disconnect(sid):
with _authed_lock:
_authed_sids.discard(sid)
print(f"[KonanDB] Client disconnected: {sid}")
# ── Authentication ───────────────────────────────────────────────────────
@sio.event
async def authenticate(sid, data):
user = data.get("user", "")
pwd = data.get("pass", "")
if user == KONANDB_USER and pwd == KONANDB_PASS:
with _authed_lock:
_authed_sids.add(sid)
print(f"[KonanDB] Authenticated: {sid}")
return {"ok": True}
print(f"[KonanDB] Auth failed for sid={sid} user={user!r}")
return {"ok": False, "error": "Invalid credentials"}
# ── Helpers ──────────────────────────────────────────────────────────────
def _col(data: dict):
return db.collection(data["collection"])
def _unauth():
return {"ok": False, "error": "Not authenticated β€” send 'authenticate' first"}
# ── Search / read events ─────────────────────────────────────────────────
@sio.event
async def find(sid, data):
if not _require_auth(sid): return _unauth()
try:
results = _col(data).find(
filter_ = data.get("filter", {}),
projection = data.get("projection"),
sort = data.get("sort"),
limit = data.get("limit", 0),
skip = data.get("skip", 0),
)
return {"ok": True, "result": results}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def find_one(sid, data):
if not _require_auth(sid): return _unauth()
try:
doc = _col(data).find_one(
filter_ = data.get("filter", {}),
projection = data.get("projection"),
)
return {"ok": True, "result": doc}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def find_by_id(sid, data):
if not _require_auth(sid): return _unauth()
try:
doc = _col(data).find_by_id(data["id"])
return {"ok": True, "result": doc}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def count_documents(sid, data):
if not _require_auth(sid): return _unauth()
try:
n = _col(data).count_documents(data.get("filter", {}))
return {"ok": True, "result": n}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def distinct(sid, data):
if not _require_auth(sid): return _unauth()
try:
vals = _col(data).distinct(data["field"], data.get("filter", {}))
return {"ok": True, "result": vals}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def aggregate(sid, data):
if not _require_auth(sid): return _unauth()
try:
result = _col(data).aggregate(data.get("pipeline", []))
return {"ok": True, "result": result}
except Exception as e:
return {"ok": False, "error": str(e)}
# ── Write events ─────────────────────────────────────────────────────────
@sio.event
async def insert_one(sid, data):
if not _require_auth(sid): return _unauth()
try:
doc_id = _col(data).insert_one(data["document"])
return {"ok": True, "result": {"_id": doc_id}}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def insert_many(sid, data):
if not _require_auth(sid): return _unauth()
try:
ids = _col(data).insert_many(data["documents"])
return {"ok": True, "result": {"ids": ids}}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def update_one(sid, data):
if not _require_auth(sid): return _unauth()
try:
n = _col(data).update_one(
data.get("filter", {}),
data["update"],
upsert=data.get("upsert", False),
)
return {"ok": True, "result": {"matched": n}}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def update_many(sid, data):
if not _require_auth(sid): return _unauth()
try:
n = _col(data).update_many(data.get("filter", {}), data["update"])
return {"ok": True, "result": {"matched": n}}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def delete_one(sid, data):
if not _require_auth(sid): return _unauth()
try:
n = _col(data).delete_one(data.get("filter", {}))
return {"ok": True, "result": {"deleted": n}}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def delete_many(sid, data):
if not _require_auth(sid): return _unauth()
try:
n = _col(data).delete_many(data.get("filter", {}))
return {"ok": True, "result": {"deleted": n}}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def replace_one(sid, data):
if not _require_auth(sid): return _unauth()
try:
n = _col(data).replace_one(
data.get("filter", {}),
data["replacement"],
upsert=data.get("upsert", False),
)
return {"ok": True, "result": {"matched": n}}
except Exception as e:
return {"ok": False, "error": str(e)}
# ── Index events ─────────────────────────────────────────────────────────
@sio.event
async def create_index(sid, data):
if not _require_auth(sid): return _unauth()
try:
name = _col(data).create_index(data["field"], unique=data.get("unique", False))
return {"ok": True, "result": {"name": name}}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def drop_index(sid, data):
if not _require_auth(sid): return _unauth()
try:
_col(data).drop_index(data["field"])
return {"ok": True}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def list_indexes(sid, data):
if not _require_auth(sid): return _unauth()
try:
idxs = _col(data).list_indexes()
return {"ok": True, "result": idxs}
except Exception as e:
return {"ok": False, "error": str(e)}
# ── DB-level events ──────────────────────────────────────────────────────
@sio.event
async def list_collections(sid, data):
if not _require_auth(sid): return _unauth()
try:
cols = db.list_collections()
return {"ok": True, "result": cols}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def flush(sid, data):
if not _require_auth(sid): return _unauth()
try:
db.flush()
return {"ok": True}
except Exception as e:
return {"ok": False, "error": str(e)}
# ── Change stream / watch events ─────────────────────────────────────────
# sid β†’ {col_name β†’ watch_handle}
_watch_registry: Dict[str, Dict[str, Any]] = {}
@sio.event
async def watch_collection(sid, data):
"""
Subscribe to real-time change events for a collection.
Client sends: {"collection": "...", "filter": {}}
Server emits: "change" event β†’ {"collection": "...", "op": "insert"|"update"|"delete", "doc": {...}}
Multiple watches on the same collection from the same client replace
the previous watch for that collection.
"""
if not _require_auth(sid): return _unauth()
try:
col_name = data["collection"]
filter_ = data.get("filter") or {}
col = db.collection(col_name)
# Remove stale watch for this sid+collection if any
if sid in _watch_registry and col_name in _watch_registry[sid]:
col.unwatch(_watch_registry[sid][col_name])
import asyncio
def _on_change(event):
# Fire-and-forget: schedule the Socket.IO emit on the event loop
async def _emit():
try:
await sio.emit(
"change",
{"collection": col_name, **event},
to=sid,
)
except Exception as exc:
print(f"[KonanDB][WARN] watch emit error: {exc}")
loop = asyncio.get_event_loop()
if loop.is_running():
asyncio.run_coroutine_threadsafe(_emit(), loop)
handle = col.watch(filter_, _on_change)
_watch_registry.setdefault(sid, {})[col_name] = handle
print(f"[KonanDB] watch registered: sid={sid} col={col_name} filter={filter_}")
return {"ok": True}
except Exception as e:
return {"ok": False, "error": str(e)}
@sio.event
async def unwatch_collection(sid, data):
"""
Unsubscribe from change events for a collection.
Client sends: {"collection": "..."}
"""
if not _require_auth(sid): return _unauth()
try:
col_name = data["collection"]
if sid in _watch_registry and col_name in _watch_registry[sid]:
db.collection(col_name).unwatch(_watch_registry[sid].pop(col_name))
return {"ok": True}
except Exception as e:
return {"ok": False, "error": str(e)}
# Clean up watches on disconnect
_original_disconnect = None
@sio.on("disconnect") # type: ignore[misc]
async def _cleanup_watches(sid):
with _authed_lock:
_authed_sids.discard(sid)
if sid in _watch_registry:
for col_name, handle in _watch_registry.pop(sid, {}).items():
try:
db.collection(col_name).unwatch(handle)
except Exception:
pass
print(f"[KonanDB] Client disconnected: {sid}")
# ── Compound index events ─────────────────────────────────────────────────
@sio.event
async def create_compound_index(sid, data):
"""
Create a compound index on multiple fields.
Client sends: {"collection": "...", "fields": ["field1", "field2", ...], "unique": false}
Compound indexes are stored as individual field indexes combined with
an intersection strategy at query time (same as the existing multi-field
_index_lookup_coords intersection). This event is a convenience wrapper
that creates a single-field index for each listed field so compound
queries on those fields automatically use index-intersection.
Note: true single-pass compound indexes are a planned enhancement.
For now this gives O(index) intersection across the listed fields.
"""
if not _require_auth(sid): return _unauth()
try:
col = _col(data)
fields = data.get("fields", [])
unique = data.get("unique", False)
if len(fields) < 2:
return {"ok": False, "error": "compound index requires at least 2 fields"}
names = []
for field in fields:
names.append(col.create_index(field, unique=unique))
return {"ok": True, "result": {"names": names, "note": "per-field indexes created for intersection"}}
except Exception as e:
return {"ok": False, "error": str(e)}
return sio
def _make_flask_app(db: "KonanDB") -> Flask:
"""
Build the Flask app that handles HTTP health/status routes.
Mounted under /api/* alongside the Socket.IO server.
Routes
------
GET /api/health β€” liveness probe
GET /api/status β€” repo info + dirty collection count
GET /api/collections β€” list all collection names
"""
flask_app = Flask(__name__)
@flask_app.route("/api/health")
def health():
return jsonify({"ok": True, "status": "running"})
@flask_app.route("/api/status")
def status():
repo_ready = db._repo_ready.is_set()
dirty = list(db._dirty_collections)
return jsonify({
"ok": True,
"repo": db.repo_id,
"repo_ready": repo_ready,
"dirty_collections": dirty,
"dirty_count": len(dirty),
})
@flask_app.route("/api/collections")
def collections():
if not db._repo_ready.is_set():
return jsonify({"ok": False, "error": "repo not ready yet"}), 503
try:
cols = db.list_collections()
return jsonify({"ok": True, "collections": cols})
except Exception as exc:
return jsonify({"ok": False, "error": str(exc)}), 500
return flask_app
def start_server(
db: "KonanDB",
host: str = "0.0.0.0",
port: int = 7860,
):
"""
Start KonanDB: Socket.IO (real-time DB) + Flask (HTTP health routes)
on a single port using aiohttp as the outer server.
Socket.IO handles /socket.io/* automatically.
Flask handles /api/* via aiohttp-wsgi.
Everything else returns 404.
Parameters
----------
db : KonanDB instance to serve
host : bind address (default all interfaces)
port : TCP port (default 7860 β€” standard for HuggingFace Spaces)
"""
sio = create_socketio_server(db)
flask_app = _make_flask_app(db)
# Wrap Flask as an aiohttp-compatible WSGI handler
wsgi_handler = WSGIHandler(flask_app)
# aiohttp app β€” Socket.IO attaches its own routes (/socket.io/*)
aio_app = web.Application()
sio.attach(aio_app)
# Route /api/* to Flask; everything else is handled by Socket.IO / aiohttp
async def _flask_route(request: web.Request) -> web.StreamResponse:
return await wsgi_handler(request)
aio_app.router.add_route("*", "/api/{path_info:.*}", _flask_route)
aio_app.router.add_route("*", "/api", _flask_route)
print(f"[KonanDB] Server starting on http://{host}:{port}")
print(f"[KonanDB] Socket.IO : ws://{host}:{port}/socket.io/")
print(f"[KonanDB] Health : http://{host}:{port}/api/health")
print(f"[KonanDB] Status : http://{host}:{port}/api/status")
print(f"[KonanDB] User: {KONANDB_USER!r} | Pass: {'*' * len(KONANDB_PASS)}")
web.run_app(aio_app, host=host, port=port)
# ─────────────────────────────────────────────────────────────────────────────
# Entry point (python konandb.py)
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import sys
token = os.getenv("HF_TOKEN")
username = os.getenv("HF_USERNAME", "myuser")
repo_name = os.getenv("HF_REPO", "konandb-data")
if not token:
print("[KonanDB] ERROR: Set the HF_TOKEN environment variable first.")
print(" export HF_TOKEN=hf_...")
sys.exit(1)
repo = f"{username}/{repo_name}"
host = os.getenv("KONANDB_HOST", "0.0.0.0")
port = int(os.getenv("KONANDB_PORT", "7860"))
print(f"[KonanDB] Initialising β€” repo: {repo}")
print(f"[KonanDB] Server will listen on http://{host}:{port}")
print(f"[KonanDB] HF repo bootstrap running in background...")
# Initialise KonanDB in a background thread so the Socket.IO server
# can bind its port immediately while the HF repo check / creation
# happens concurrently. The server accepts connections straight away;
# the first collection access will block only until the background init
# finishes (the Collection._load() path already handles lazy loading).
db = KonanDB(
repo_id = repo,
hf_token = token,
auto_flush_interval = int(os.getenv("KONANDB_FLUSH_INTERVAL", "120")),
flush_after_ops = int(os.getenv("KONANDB_FLUSH_OPS", "500")),
shard_size = int(os.getenv("KONANDB_SHARD_SIZE", "1000")),
private = os.getenv("KONANDB_PRIVATE", "false").lower() == "true",
scan_workers = int(os.getenv("KONANDB_SCAN_WORKERS", "8")),
load_workers = int(os.getenv("KONANDB_LOAD_WORKERS", "8")),
)
start_server(db, host=host, port=port)