CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Failing after 7s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 7s
CI & Build / Python tests (push) Successful in 11s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Failing after 31s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 6m45s
Operator (note 2897): "notes shouldn't have a title field." The concept of a NAME stays — search results, export filenames and the command palette all need one — but nothing is typed into it any more. `display_title` is now the first non-empty line of the body, falling back to the first checklist item. That fallback is what step 2 bought, and the reason this could not go first: a checklist had no body to be named from, so the title was its only name. Now every note has a body, and a note that is only a checklist is named by its first item. Gone everywhere: the column and note_revisions.title (0026), the field on the core's Note/NoteCreateInput/NoteRevision and its SQLite columns (user_version 7), `normalize_title`, the wire field, the FFI record and `NoteEdit::Title` / `ClearTitle`, the web editor's "Title (optional)" input and the card's <h3>, and the Android title field in both the compose sheet and the editor. **The search vector had to be rebuilt, not just left alone.** `notes.search_vector` is a STORED GENERATED column whose expression names `title` — Postgres refuses to drop a column another generated column depends on. It is dropped and recreated over `display_title` at weight A, which keeps the original intent: a note's NAME ranks above the rest of its body. **An imported title becomes the note's first body line.** Keep notes carry one, and so does any ThoughtSync export taken before this. Dropping it would silently lose text someone wrote; folding it in puts it exactly where a name now lives, so the note arrives named as it was. Skipped when the body already opens with that line, so re-importing an export this code produced doesn't stack duplicates. Two smaller things fell out. The Android editor loses its bold first field — one weight throughout, because the first line is the note's name but not a different KIND of text, which is most of step 4 arriving early. And `ClearTitle`'s justification comment moved to `ClearRemindAt`, which is now the surviving example of why NoteEdit is a list rather than a struct of options. Protocol note corrected to say what actually shipped: v2 is "no kind, no title", one bump for the pair. Verified with the local Rust gate this time, not by CI: fmt, clippy and 116 tests all green before pushing. It caught four things — orphaned serde attributes where fields were removed, a `wire::Preview.title` I deleted by mistake (a link preview still has one), nine retention fixtures inserting a dropped column, and four rustfmt diffs.
83 lines
3.3 KiB
Python
83 lines
3.3 KiB
Python
"""drop notes.title and note_revisions.title — a note's name is its first line
|
|
|
|
Revision ID: 0026
|
|
Revises: 0025
|
|
Create Date: 2026-08-22
|
|
|
|
M13 step 3. A note is a body plus optional checkable items; its NAME is the first
|
|
non-empty line of that body, falling back to its first checklist item. There is no
|
|
separate field to type into, and `display_title` (already persisted, already what
|
|
search results and export filenames read) carries the name.
|
|
|
|
## The search vector has to be rebuilt, not just left alone
|
|
|
|
`notes.search_vector` is a STORED GENERATED column whose expression names `title`
|
|
(migration 0005, weight A) — Postgres will refuse to drop a column another generated
|
|
column depends on, and even if it didn't, the weighting would be wrong. So it is
|
|
dropped and recreated over `display_title` instead, which keeps the original
|
|
intent: the note's NAME ranks above the rest of its body.
|
|
|
|
Rebuilding a stored generated column re-computes every row, and the GIN index is
|
|
rebuilt with it. On a personal instance that is milliseconds; it is worth knowing
|
|
before running this against something large.
|
|
|
|
## What happens to existing titles
|
|
|
|
Nothing preserves them, deliberately: `display_title` was already derived from the
|
|
title when one was set, so every note keeps the NAME it had. What is lost is the
|
|
distinction between "this note has an explicit title" and "this note's first line is
|
|
its name" — which is the distinction being removed.
|
|
|
|
Imports are the exception and are handled in code, not here: a Keep note's title, or
|
|
one in an export taken before this, is folded in as the note's first body line rather
|
|
than dropped (see `_create_imported_note`).
|
|
"""
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
revision = "0026"
|
|
down_revision = "0025"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# Order matters: the generated column depends on `title`, so it goes first.
|
|
op.execute("DROP INDEX IF EXISTS ix_notes_search")
|
|
op.execute("ALTER TABLE notes DROP COLUMN IF EXISTS search_vector")
|
|
|
|
op.drop_column("notes", "title")
|
|
op.drop_column("note_revisions", "title")
|
|
|
|
op.execute(
|
|
"""
|
|
ALTER TABLE notes ADD COLUMN search_vector tsvector
|
|
GENERATED ALWAYS AS (
|
|
setweight(to_tsvector('english', coalesce(display_title, '')), 'A') ||
|
|
setweight(to_tsvector('english', coalesce(body, '')), 'B')
|
|
) STORED
|
|
"""
|
|
)
|
|
op.execute("CREATE INDEX ix_notes_search ON notes USING GIN (search_vector)")
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.execute("DROP INDEX IF EXISTS ix_notes_search")
|
|
op.execute("ALTER TABLE notes DROP COLUMN IF EXISTS search_vector")
|
|
|
|
# Comes back empty. The text is not gone — it is the first line of every body —
|
|
# but which notes once had an explicit title is not recorded anywhere.
|
|
op.add_column("notes", sa.Column("title", sa.Text(), nullable=True))
|
|
op.add_column("note_revisions", sa.Column("title", sa.Text(), nullable=True))
|
|
|
|
op.execute(
|
|
"""
|
|
ALTER TABLE notes ADD COLUMN search_vector tsvector
|
|
GENERATED ALWAYS AS (
|
|
setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
|
|
setweight(to_tsvector('english', coalesce(body, '')), 'B')
|
|
) STORED
|
|
"""
|
|
)
|
|
op.execute("CREATE INDEX ix_notes_search ON notes USING GIN (search_vector)")
|