Brainstorm initial API and modify db to be more FUSE friendly

This commit is contained in:
Andrew Mulbrook 2026-03-16 01:14:07 -05:00
parent 0a444a55c4
commit 96c9e62354
3 changed files with 183 additions and 105 deletions

View file

@ -3,58 +3,49 @@ Python API for accessing underlying SQlite3 File Database
A Sqlite3 database provides the basic underlying interface to the underlying
notes interface. The low level database is an extremely simple model using
similar ideas to get storage. However, UUID are utilized for persistent object
references.
similar ideas to git storage.
"""
import argparse
import sqlite3
import sys
import uuid as _uuid
from datetime import datetime, timezone
from pathlib import Path
from .trove import NODE_ROOT_ID
_SCHEMA = """
CREATE TABLE IF NOT EXISTS objects (
uuid TEXT PRIMARY KEY,
id INTEGER PRIMARY KEY,
type TEXT NOT NULL CHECK(type IN ('blob', 'tree')),
data BLOB,
created TEXT NOT NULL,
modified TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS metadata (
uuid TEXT NOT NULL REFERENCES objects(uuid) ON DELETE CASCADE,
id INTEGER NOT NULL REFERENCES objects(id) ON DELETE CASCADE,
key TEXT NOT NULL,
value BLOB,
PRIMARY KEY (uuid, key)
PRIMARY KEY (id, key)
);
CREATE TABLE IF NOT EXISTS labels
(
label
TEXT
PRIMARY
KEY,
uuid
TEXT
NOT
NULL
REFERENCES
objects
(
uuid
) ON DELETE CASCADE
); \
"""
label TEXT PRIMARY KEY,
id INTEGER NOT NULL REFERENCES objects(id) ON DELETE CASCADE
);
"""
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
class TroveDB:
def _initialize_db(con: sqlite3.Connection):
con.executescript(_SCHEMA)
con.commit()
class Sqlite3Trove:
def __init__(self, con: sqlite3.Connection):
self._con = con
self._con.execute("PRAGMA foreign_keys = ON")
@ -65,24 +56,22 @@ class TroveDB:
# ------------------------------------------------------------------
@classmethod
def init(cls, path: str | Path) -> "TroveDB":
"""Create a new Trove database at path. Fails if file already exists."""
p = Path(path)
if p.exists():
raise FileExistsError(f"Database already exists: {p}")
con = sqlite3.connect(str(p))
con.executescript(_SCHEMA)
con.commit()
return cls(con)
@classmethod
def open(cls, path: str | Path) -> "TroveDB":
def open(cls, path: str | Path, create: bool = False) -> "Sqlite3Trove":
"""Open an existing Trove database."""
p = Path(path)
initialize = False
if not p.exists():
if not create:
raise FileNotFoundError(f"Database not found: {p}")
initialize = True
con = sqlite3.connect(str(p))
return cls(con)
if initialize:
con.executescript(_SCHEMA)
con.commit()
obj = cls(con)
if initialize:
obj.write_blob(b"", NODE_ROOT_ID)
return obj
def close(self):
self._con.close()
@ -97,77 +86,84 @@ class TroveDB:
# CRUD operations
# ------------------------------------------------------------------
def read_blob(self, uuid: str) -> bytes | None:
def read_object(self, object_id: int) -> bytes | None:
"""Return raw data for a blob object, or None if not found."""
row = self._con.execute(
"SELECT data, type FROM objects WHERE uuid = ?", (uuid,)
"SELECT data, type FROM objects WHERE id = ?", (object_id,)
).fetchone()
if row is None:
return None
if row["type"] != "blob":
raise TypeError(f"Object {uuid} is type '{row['type']}', not 'blob'")
return bytes(row["data"]) if row["data"] is not None else b""
def read_metadata(self, uuid: str, key: str) -> bytes | None:
def read_metadata(self, object_id: int, key: str) -> bytes | None:
"""Return raw metadata value for (uuid, key), or None if not found."""
row = self._con.execute(
"SELECT value FROM metadata WHERE uuid = ? AND key = ?", (uuid, key)
"SELECT value FROM metadata WHERE id = ? AND key = ?", (object_id, key)
).fetchone()
if row is None:
return None
return bytes(row["value"]) if row["value"] is not None else b""
def write_blob(self, data: bytes, existing_uuid: str | None = None) -> str:
def _write_object(self, data: bytes, dtype: str, object_id: int | None = None) -> int:
"""
Insert or replace a blob. Returns the uuid.
Pass existing_uuid to update an existing object.
Insert or replace an object. Returns the id.
If object_id is None, creates a new object with auto-assigned ID.
If object_id is provided, updates or creates the object with that ID.
"""
now = _now()
uid = existing_uuid or str(_uuid.uuid4())
if existing_uuid:
self._con.execute(
"UPDATE objects SET data = ?, modified = ? WHERE uuid = ? AND type = 'blob'",
(data, now, uid),
)
else:
self._con.execute(
"INSERT INTO objects (uuid, type, data, created, modified) VALUES (?, 'blob', ?, ?, ?)",
(uid, data, now, now),
modified = _now()
if object_id is None:
cur = self._con.execute(
"INSERT INTO objects (type, data, modified) VALUES (?, ?, ?)",
(dtype, data, modified)
)
self._con.commit()
return uid
return cur.lastrowid
else:
self._con.execute(
"INSERT OR REPLACE INTO objects (id, type, data, modified) VALUES (?, ?, ?, ?)",
(object_id, dtype, data, modified)
)
self._con.commit()
return object_id
def delete_blob(self, uuid: str) -> bool:
def write_blob(self, data: bytes, existing_id: int | None = None) -> int:
"""
Insert or replace a blob. Returns the id.
Pass existing_id to update an existing object.
"""
return self._write_object(data, "blob", existing_id)
def delete_object(self, object_id: int) -> bool:
"""
Delete a blob and all its metadata rows.
Returns True if an object was deleted, False if uuid not found.
Returns True if an object was deleted, False if id not found.
Foreign key cascade handles the metadata rows.
"""
cur = self._con.execute(
"DELETE FROM objects WHERE uuid = ?", (uuid,)
"DELETE FROM objects WHERE id = ?", (object_id,)
)
self._con.commit()
return cur.rowcount > 0
def get_label(self, label: str) -> str | None:
def get_label(self, label: str) -> int | None:
"""
Return the UUID associated with a label, or None if not found.
Return the ID associated with a label, or None if not found.
"""
row = self._con.execute(
"SELECT uuid FROM labels WHERE label = ?", (label,)
"SELECT id FROM labels WHERE label = ?", (label,)
).fetchone()
if row is None:
return None
return row["uuid"]
return row["id"]
def set_label(self, label: str, uuid: str) -> None:
def set_label(self, label: str, object_id: int) -> None:
"""
Set a label to point to a UUID. Creates or updates the label.
The UUID must exist in the objects table.
Set a label to point to an ID. Creates or updates the label.
The ID must exist in the objects table.
"""
self._con.execute(
"INSERT OR REPLACE INTO labels (label, uuid) VALUES (?, ?)",
(label, uuid),
"INSERT OR REPLACE INTO labels (label, id) VALUES (?, ?)",
(label, object_id),
)
self._con.commit()
@ -181,14 +177,14 @@ class TroveDB:
self._con.commit()
return cur.rowcount > 0
def list_labels(self) -> list[tuple[str, str]]:
def list_labels(self) -> list[tuple[str, int]]:
"""
Return all labels as a list of (label, uuid) tuples.
Return all labels as a list of (label, id) tuples.
"""
rows = self._con.execute(
"SELECT label, uuid FROM labels ORDER BY label"
"SELECT label, id FROM labels ORDER BY label"
).fetchall()
return [(row["label"], row["uuid"]) for row in rows]
return [(row["label"], row["id"]) for row in rows]
def main():
@ -202,22 +198,22 @@ def main():
subparsers.add_parser("create", help="Create a new database")
# Get blob
get_parser = subparsers.add_parser("get", help="Get a blob object by UUID")
get_parser.add_argument("uuid", help="UUID of the blob to retrieve")
get_parser = subparsers.add_parser("get", help="Get a blob object by ID")
get_parser.add_argument("id", type=int, help="ID of the blob to retrieve")
# Write blob
write_parser = subparsers.add_parser("write", help="Write data to a blob")
write_parser.add_argument("data", help="Data to write (as string, will be encoded as UTF-8)")
write_parser.add_argument("--uuid", help="UUID of existing blob to update (optional)")
write_parser.add_argument("--id", type=int, help="ID of existing blob to update (optional)")
# Delete blob
delete_parser = subparsers.add_parser("delete", help="Delete a blob by UUID")
delete_parser.add_argument("uuid", help="UUID of the blob to delete")
delete_parser = subparsers.add_parser("delete", help="Delete a blob by ID")
delete_parser.add_argument("id", type=int, help="ID of the blob to delete")
# Set label
setlabel_parser = subparsers.add_parser("setlabel", help="Create or update a label to point to a UUID")
setlabel_parser = subparsers.add_parser("setlabel", help="Create or update a label to point to an ID")
setlabel_parser.add_argument("label", help="Label name")
setlabel_parser.add_argument("uuid", help="UUID to associate with the label")
setlabel_parser.add_argument("id", type=int, help="ID to associate with the label")
# Remove label
rmlabel_parser = subparsers.add_parser("rmlabel", help="Delete a label")
@ -231,59 +227,62 @@ def main():
try:
match args.operation:
case "create":
db = TroveDB.init(args.database)
if Path(args.database).exists():
print(f"Database already exists: {args.database}", file=sys.stderr)
sys.exit(1)
db = Sqlite3Trove.open(args.database, create=True)
db.close()
print(f"Database created: {args.database}")
case "get":
db = TroveDB.open(args.database)
data = db.read_blob(args.uuid)
db = Sqlite3Trove.open(args.database)
data = db.read_object(args.id)
db.close()
if data is None:
print(f"Blob not found: {args.uuid}", file=sys.stderr)
print(f"Blob not found: {args.id}")
sys.exit(1)
sys.stdout.buffer.write(data)
case "write":
data_bytes = args.data.encode("utf-8")
db = TroveDB.open(args.database)
uuid = db.write_blob(data_bytes, args.uuid)
db = Sqlite3Trove.open(args.database)
object_id = db.write_blob(data_bytes, args.id)
db.close()
print(uuid)
print(object_id)
case "delete":
db = TroveDB.open(args.database)
deleted = db.delete_blob(args.uuid)
db = Sqlite3Trove.open(args.database)
deleted = db.delete_object(args.id)
db.close()
if deleted:
print(f"Deleted blob: {args.uuid}")
print(f"Deleted blob: {args.id}")
else:
print(f"Blob not found: {args.uuid}", file=sys.stderr)
print(f"Blob not found: {args.id}")
sys.exit(1)
case "setlabel":
db = TroveDB.open(args.database)
db.set_label(args.label, args.uuid)
db = Sqlite3Trove.open(args.database)
db.set_label(args.label, args.id)
db.close()
print(f"Label '{args.label}' set to UUID: {args.uuid}")
print(f"Label '{args.label}' set to ID: {args.id}")
case "rmlabel":
db = TroveDB.open(args.database)
db = Sqlite3Trove.open(args.database)
deleted = db.delete_label(args.label)
db.close()
if deleted:
print(f"Deleted label: {args.label}")
else:
print(f"Label not found: {args.label}", file=sys.stderr)
print(f"Label not found: {args.label}")
sys.exit(1)
case "labels":
db = TroveDB.open(args.database)
db = Sqlite3Trove.open(args.database)
labels = db.list_labels()
db.close()
if labels:
for label, uuid in labels:
print(f"{label}: {uuid}")
for label, id in labels:
print(f"{label}: {id}")
else:
print("No labels found.")

View file

@ -15,8 +15,8 @@ class Tree:
Initialize a Tree. If data is provided, deserialize from UTF-8 JSON bytes.
An empty Tree is created if data is None.
"""
if data is None:
self._entries: dict[str, str] = {}
if not data:
self._entries: dict[str, int] = {}
else:
self._entries = json.loads(data.decode("utf-8"))
@ -24,14 +24,18 @@ class Tree:
"""Serialize the tree to UTF-8 JSON bytes."""
return json.dumps(self._entries).encode("utf-8")
def set_entry(self, name: str, uuid: str) -> None:
def set_entry(self, name: str, object_id: int) -> None:
"""Add or update an entry mapping name -> uuid."""
self._entries[name] = uuid
self._entries[name] = object_id
def get_entry(self, name: str) -> int:
"""Get the uuid associated with a name, or raise KeyError if not found."""
return self._entries[name]
def rm_entry(self, name: str) -> None:
"""Remove an entry by name. Raises KeyError if not found."""
del self._entries[name]
def list(self) -> dict[str, str]:
def list(self) -> dict[str, int]:
"""Return a shallow copy of all entries as {name: uuid}."""
return dict(self._entries)

75
trovedb/trove.py Normal file
View file

@ -0,0 +1,75 @@
from typing import Protocol, runtime_checkable, Optional, Dict, List, Self
from uuid import UUID
from pathlib import PurePosixPath
NODE_ROOT_ID = 1
@runtime_checkable
class Note(Protocol):
"""
Protocol for a Note item.
Represents access to an individual note's content and metadata.
"""
@property
def object_id(self) -> int:
"""The unique identifier for this note."""
...
def get_raw_metadata(self, key: str) -> Optional[bytes]:
"""Retrieve metadata value for the given key."""
...
def set_raw_metadata(self, key: str, value: bytes) -> None:
"""Set metadata value for the given key."""
...
@runtime_checkable
class Blob(Protocol):
def read(self) -> bytes:
"""Read the raw content of the note."""
...
def write(self, data: bytes) -> None:
"""Write new content to the note."""
...
@runtime_checkable
class Tree(Protocol):
def link(self, name: str, note: Note):
"""Link name to a given note."""
...
def unlink(self, name: str):
"""Remove name from the tree."""
...
def mkdir(self, name: str) -> Self:
"""Create a new Tree with the given name."""
...
class BlobNote(Note, Blob):
"""Blob Note"""
class TreeNote(Note, Tree):
"""Tree Note"""
@runtime_checkable
class Trove(Protocol):
"""
Protocol for the Trove database API.
Provides high-level access to notes and trees.
"""
def get_raw_note(self, note: int) -> Optional[Note]:
"""Retrieve a note by a UUID"""
...
def create_blob(self, data: bytes | None = None) -> BlobNote:
"""Create a new blob node at the given path with content"""
...
def get_root(self) -> TreeNote:
"""Get Tree Node at the given path"""
...