Skip to content

Replace SQLite storage with file-based Markdown + JSON layout#51

Merged
TheZupZup merged 1 commit intomainfrom
claude/redesign-storage-system-dusDL
May 3, 2026
Merged

Replace SQLite storage with file-based Markdown + JSON layout#51
TheZupZup merged 1 commit intomainfrom
claude/redesign-storage-system-dusDL

Conversation

@TheZupZup
Copy link
Copy Markdown
Owner

Summary

This PR replaces the legacy SQLite database backend with a file-based storage system modeled after Obsidian. Notes are now stored as plain Markdown files with YAML frontmatter, drawings as separate JSON files, and notebooks as YAML metadata files. An automatic migration tool copies all data from pre-v1.0 SQLite databases into the new layout on first startup.

Key Changes

  • New FileNoteStore class (nexanote/storage/file_store.py): Primary storage backend implementing the same public API as the legacy NexaNoteDB so REST API, WebDAV provider, and sync engine work unchanged. Features:

    • Notes stored as notes/<id>.md with YAML frontmatter + Markdown body
    • Drawings stored as drawings/<id>.json (only created if note has strokes)
    • Notebooks stored as notebooks/<id>.yaml
    • Single-page typed notes have clean Markdown bodies with no NexaNote markers (Obsidian-compatible)
    • Multi-page notes use minimal <!-- nexanote:page N --> HTML comment markers for splitting
    • Per-path threading locks + atomic writes (tmp + os.replace) for concurrency safety
  • Automatic SQLite → file migration (nexanote/storage/migration.py):

    • needs_migration(data_dir) detects pre-v1.0 nexanote.db
    • run_migration(data_dir) copies all notebooks, notes, pages, and strokes into new layout
    • Original SQLite file renamed to nexanote.db.legacy_backup (never deleted)
    • .nexanote_migrated marker prevents re-running migration
    • Soft-deleted notes preserved with is_deleted flag intact
  • Comprehensive test coverage (tests/test_file_store.py, tests/test_migration.py):

    • On-disk layout validation
    • Markdown + frontmatter round-trip serialization
    • Multi-page splitting and joining
    • Metadata-only updates preserving drawings
    • Migration from SQLite with various note types and states
  • Updated all storage consumers:

    • nexanote/api/routes.py: Changed type hint from NexaNoteDB to FileNoteStore
    • nexanote/sync/webdav_provider.py: Updated imports
    • nexanote/sync/client.py: Updated imports
    • nexanote/sync/server.py: Added migration call on startup
    • main.py: Added migration call before initializing storage
  • Legacy SQLite layer preserved (nexanote/storage/legacy_db.py): Kept read-only for migration tool; no longer used for normal operation

  • Dependencies: Added PyYAML==6.0.3 for YAML frontmatter serialization

  • Documentation & versioning:

    • Updated README.md to reflect file-based storage
    • Updated docs/docker.md to describe new directory structure
    • Bumped version to 1.0.0 in app/pubspec.yaml, routes.py, and metadata/org.nexanote.app.yml
    • Added comprehensive CHANGELOG.md documenting the v1.0.0 release

Notable Implementation Details

  • Frontmatter parsing: Robust YAML extraction with fallback to plain text if parsing fails
  • Page splitting: Regex-based marker detection with prefix preservation to avoid data loss on round-trip
  • Atomic writes: Temporary file + os.fsync() + os.replace() prevents corruption if process is killed mid-write
  • Lock registry: Reusable per-path RLock instances for thread-safe concurrent access
  • Obsidian compatibility: Single-page notes have zero NexaNote-specific markers in the Markdown body, making them directly editable in any text editor

https://claude.ai/code/session_01NgyB5ckJecLX3v25tPTV3d

Replaces the SQLite database with a plain-file layout so notes are
self-describing on disk and editable in any Markdown tool (Obsidian,
VS Code, etc.).

Layout:
  <data_dir>/
    notebooks/<id>.yaml      # Notebook metadata
    notes/<id>.md            # Markdown body + YAML frontmatter
    drawings/<id>.json       # Stylus strokes (one file per note)

- New FileNoteStore is the primary storage; mirrors the old NexaNoteDB
  public API so the REST routes, WebDAV provider, and sync engine
  required only import/type updates.
- Single-page typed notes have a clean Markdown body with no NexaNote
  markers, so they round-trip cleanly through external editors.
- Atomic writes (tmp + os.replace) and per-path RLocks keep concurrent
  API + WebDAV traffic safe inside the backend process.
- run_migration() detects pre-v1 nexanote.db on startup, copies every
  notebook/note/page/stroke into the new layout, renames the original
  to nexanote.db.legacy_backup, and writes a .nexanote_migrated marker
  so it never re-runs. Soft-deleted notes are preserved.
- legacy_db.py is the old SQLite layer kept read-only for migration.
- Bumps backend + Flutter app to v1.0.0; adds CHANGELOG and updates
  README/docker.md to describe the new layout. REST API contract is
  unchanged so existing Flutter clients keep working.
- 36 new tests cover file-store roundtrip, multi-page splitting,
  concurrency, and end-to-end SQLite -> file migration. Suite is
  102 passing.
@TheZupZup TheZupZup merged commit 2457079 into main May 3, 2026
1 check passed
@TheZupZup TheZupZup deleted the claude/redesign-storage-system-dusDL branch May 3, 2026 05:50
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 704f571e18

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +153 to +155
report.ran = True
marker.write_text(
_marker_payload(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Abort migration when conversion errors occur

The migration is marked as successful and finalized even when per-record conversion failures were already captured in report.errors. In the failure scenario (e.g., one or more notes fail to write due to malformed data or filesystem errors), this still writes the migration marker and skips future runs, leaving the file store incomplete and requiring manual backup recovery. Migration finalization should be gated on zero conversion errors (or raise and keep migration retryable).

Useful? React with 👍 / 👎.

id=meta["id"],
notebook_id=meta.get("notebook_id"),
title=meta.get("title", "Sans titre"),
note_type=NoteType(meta.get("note_type", "typed")),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle invalid frontmatter enums without crashing

A single note file with an unexpected note_type (or sync_status) value raises ValueError during deserialization, and callers like list_notes() do not catch it. In the file-based model where notes can be edited externally, one malformed frontmatter value can make /notes and /notes/{id} fail with 500 instead of skipping or defaulting the bad record.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants