Skip to content

Commit 7ab388c

Browse files
committed
Add nlink column to fs_inode for O(1) link count lookups
Previously, the link count was computed on-demand via: SELECT COUNT(*) FROM fs_dentry WHERE ino = ? This query becomes expensive as the number of directory entries grows. By storing nlink directly in the inode, we get O(1) lookups.
1 parent e62c4f1 commit 7ab388c

4 files changed

Lines changed: 157 additions & 82 deletions

File tree

SPEC.md

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Agent Filesystem Specification
22

3-
**Version:** 0.1
3+
**Version:** 0.2
44

55
## Introduction
66

@@ -164,6 +164,7 @@ Stores file and directory metadata.
164164
CREATE TABLE fs_inode (
165165
ino INTEGER PRIMARY KEY AUTOINCREMENT,
166166
mode INTEGER NOT NULL,
167+
nlink INTEGER NOT NULL DEFAULT 0,
167168
uid INTEGER NOT NULL DEFAULT 0,
168169
gid INTEGER NOT NULL DEFAULT 0,
169170
size INTEGER NOT NULL DEFAULT 0,
@@ -177,6 +178,7 @@ CREATE TABLE fs_inode (
177178

178179
- `ino` - Inode number (unique identifier)
179180
- `mode` - File type and permissions (Unix mode bits)
181+
- `nlink` - Number of hard links pointing to this inode
180182
- `uid` - Owner user ID
181183
- `gid` - Owner group ID
182184
- `size` - Total file size in bytes
@@ -238,7 +240,7 @@ CREATE INDEX idx_fs_dentry_parent ON fs_dentry(parent_ino, name)
238240

239241
- Root directory (ino=1) has no dentry (no parent)
240242
- Multiple dentries MAY point to the same inode (hard links)
241-
- Link count = `SELECT COUNT(*) FROM fs_dentry WHERE ino = ?`
243+
- Link count is stored in `fs_inode.nlink` and must be incremented/decremented when dentries are added/removed
242244

243245
#### Table: `fs_data`
244246

@@ -316,13 +318,17 @@ To resolve a path to an inode:
316318
INSERT INTO fs_dentry (name, parent_ino, ino)
317319
VALUES (?, ?, ?)
318320
```
319-
5. Split data into chunks and insert each:
321+
5. Increment link count:
322+
```sql
323+
UPDATE fs_inode SET nlink = nlink + 1 WHERE ino = ?
324+
```
325+
6. Split data into chunks and insert each:
320326
```sql
321327
INSERT INTO fs_data (ino, chunk_index, data)
322328
VALUES (?, ?, ?)
323329
```
324330
Where `chunk_index` starts at 0 and increments for each chunk.
325-
6. Update inode size:
331+
7. Update inode size:
326332
```sql
327333
UPDATE fs_inode SET size = ?, mtime = ? WHERE ino = ?
328334
```
@@ -378,13 +384,18 @@ To read `length` bytes starting at byte offset `offset`:
378384
```sql
379385
DELETE FROM fs_dentry WHERE parent_ino = ? AND name = ?
380386
```
381-
3. Check if last link:
387+
3. Decrement link count:
388+
```sql
389+
UPDATE fs_inode SET nlink = nlink - 1 WHERE ino = ?
390+
```
391+
4. Check if last link:
382392
```sql
383-
SELECT COUNT(*) FROM fs_dentry WHERE ino = ?
393+
SELECT nlink FROM fs_inode WHERE ino = ?
384394
```
385-
4. If count = 0, delete inode (CASCADE deletes data):
395+
5. If nlink = 0, delete inode and data:
386396
```sql
387397
DELETE FROM fs_inode WHERE ino = ?
398+
DELETE FROM fs_data WHERE ino = ?
388399
```
389400

390401
#### Creating a Hard Link
@@ -396,19 +407,19 @@ To read `length` bytes starting at byte offset `offset`:
396407
INSERT INTO fs_dentry (name, parent_ino, ino)
397408
VALUES (?, ?, ?)
398409
```
410+
4. Increment link count:
411+
```sql
412+
UPDATE fs_inode SET nlink = nlink + 1 WHERE ino = ?
413+
```
399414

400415
#### Reading File Metadata (stat)
401416

402417
1. Resolve path to inode
403-
2. Query inode:
418+
2. Query inode (includes link count):
404419
```sql
405-
SELECT ino, mode, uid, gid, size, atime, mtime, ctime
420+
SELECT ino, mode, nlink, uid, gid, size, atime, mtime, ctime
406421
FROM fs_inode WHERE ino = ?
407422
```
408-
3. Count links:
409-
```sql
410-
SELECT COUNT(*) as nlink FROM fs_dentry WHERE ino = ?
411-
```
412423

413424
### Initialization
414425

@@ -419,13 +430,13 @@ When creating a new agent database, initialize the filesystem configuration and
419430
INSERT INTO fs_config (key, value) VALUES ('chunk_size', '4096');
420431

421432
-- Initialize root directory
422-
INSERT INTO fs_inode (ino, mode, uid, gid, size, atime, mtime, ctime)
423-
VALUES (1, 16877, 0, 0, 0, unixepoch(), unixepoch(), unixepoch());
433+
INSERT INTO fs_inode (ino, mode, nlink, uid, gid, size, atime, mtime, ctime)
434+
VALUES (1, 16877, 1, 0, 0, 0, unixepoch(), unixepoch(), unixepoch());
424435
```
425436

426437
Where `16877` = `0o040755` (directory with rwxr-xr-x permissions)
427438

428-
**Note:** The `chunk_size` value can be customized at filesystem creation time but MUST NOT be changed afterward.
439+
**Note:** The `chunk_size` value can be customized at filesystem creation time but MUST NOT be changed afterward. The root directory has `nlink=1` as it has no parent directory entry.
429440

430441
### Consistency Rules
431442

@@ -633,6 +644,8 @@ Such extensions SHOULD use separate tables to maintain referential integrity.
633644

634645
- Added Overlay Filesystem section with `fs_whiteout` table for copy-on-write semantics
635646
- Whiteout table includes `parent_path` column with index for efficient O(1) child lookups
647+
- Added `nlink` column to `fs_inode` table to store link count directly
648+
- Link count is now maintained in the inode rather than computed via COUNT(*) on `fs_dentry`
636649

637650
### Version 0.1
638651

sdk/python/agentfs_sdk/filesystem.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ async def _initialize(self) -> None:
101101
CREATE TABLE IF NOT EXISTS fs_inode (
102102
ino INTEGER PRIMARY KEY AUTOINCREMENT,
103103
mode INTEGER NOT NULL,
104+
nlink INTEGER NOT NULL DEFAULT 0,
104105
uid INTEGER NOT NULL DEFAULT 0,
105106
gid INTEGER NOT NULL DEFAULT 0,
106107
size INTEGER NOT NULL DEFAULT 0,
@@ -161,8 +162,8 @@ async def _ensure_root(self) -> int:
161162
now = int(time.time())
162163
await self._db.execute(
163164
"""
164-
INSERT INTO fs_inode (ino, mode, uid, gid, size, atime, mtime, ctime)
165-
VALUES (?, ?, 0, 0, 0, ?, ?, ?)
165+
INSERT INTO fs_inode (ino, mode, nlink, uid, gid, size, atime, mtime, ctime)
166+
VALUES (?, ?, 1, 0, 0, 0, ?, ?, ?)
166167
""",
167168
(self._root_ino, DEFAULT_DIR_MODE, now, now, now),
168169
)
@@ -266,6 +267,11 @@ async def _create_dentry(self, parent_ino: int, name: str, ino: int) -> None:
266267
""",
267268
(name, parent_ino, ino),
268269
)
270+
# Increment link count
271+
await self._db.execute(
272+
"UPDATE fs_inode SET nlink = nlink + 1 WHERE ino = ?",
273+
(ino,),
274+
)
269275
await self._db.commit()
270276

271277
async def _ensure_parent_dirs(self, path: str) -> None:
@@ -301,7 +307,7 @@ async def _ensure_parent_dirs(self, path: str) -> None:
301307

302308
async def _get_link_count(self, ino: int) -> int:
303309
"""Get link count for an inode"""
304-
cursor = await self._db.execute("SELECT COUNT(*) FROM fs_dentry WHERE ino = ?", (ino,))
310+
cursor = await self._db.execute("SELECT nlink FROM fs_inode WHERE ino = ?", (ino,))
305311
result = await cursor.fetchone()
306312
return result[0] if result else 0
307313

@@ -477,6 +483,12 @@ async def delete_file(self, path: str) -> None:
477483
(parent_ino, name),
478484
)
479485

486+
# Decrement link count
487+
await self._db.execute(
488+
"UPDATE fs_inode SET nlink = nlink - 1 WHERE ino = ?",
489+
(ino,),
490+
)
491+
480492
# Check if this was the last link to the inode
481493
link_count = await self._get_link_count(ino)
482494
if link_count == 0:
@@ -508,7 +520,7 @@ async def stat(self, path: str) -> Stats:
508520

509521
cursor = await self._db.execute(
510522
"""
511-
SELECT ino, mode, uid, gid, size, atime, mtime, ctime
523+
SELECT ino, mode, nlink, uid, gid, size, atime, mtime, ctime
512524
FROM fs_inode
513525
WHERE ino = ?
514526
""",
@@ -519,16 +531,14 @@ async def stat(self, path: str) -> Stats:
519531
if not row:
520532
raise ValueError(f"Inode not found: {ino}")
521533

522-
nlink = await self._get_link_count(ino)
523-
524534
return Stats(
525535
ino=row[0],
526536
mode=row[1],
527-
nlink=nlink,
528-
uid=row[2],
529-
gid=row[3],
530-
size=row[4],
531-
atime=row[5],
532-
mtime=row[6],
533-
ctime=row[7],
537+
nlink=row[2],
538+
uid=row[3],
539+
gid=row[4],
540+
size=row[5],
541+
atime=row[6],
542+
mtime=row[7],
543+
ctime=row[8],
534544
)

0 commit comments

Comments
 (0)