feat: drop legacy boards table — boards ARE notes #245
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/pal-e-api!245
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "199-drop-legacy-boards"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Removes the legacy
boardstable and theboard_idcolumn fromboard_items. Boards are now exclusively notes (note_type='board').board_note_idis the sole FK linking board items to their parent board note. API responses includeboard_idas a deprecated alias forboard_note_idwith anX-Deprecated-Fieldresponse header for backward compat with pal-e-app.Changes
src/pal_e_docs/models.py— removedBoardmodel class, removedboard_idcolumn fromBoardItem, madeboard_note_idnon-nullable and the sole FKsrc/pal_e_docs/routes/boards.py— rewrote all CRUD to resolve boards viaNote(note_type='board') instead of theBoardmodel. All queries useboard_note_idinstead ofboard_id. AddedX-Deprecated-Fieldresponse header on item endpoints.src/pal_e_docs/schemas.py—BoardItemOut.board_idis now a deprecated alias populated fromboard_note_id. Removednote_idfromBoardOut(the board IS the note, soidis the note id).tests/conftest.py— removedBoardimportalembic/versions/t0o1p2q3r4s5_drop_legacy_boards_table.py— migration that backfillsboard_note_id, dropsboard_idcolumn, dropsboardstablescripts/migrate_boards_to_notes.py— removed (obsolete transitional script)Test Plan
board_note_idboard_note_idexclusivelyboard_idin API responses equalsboard_note_id(backward compat)Review Checklist
ruff formatandruff checkpassboard_idalias +X-Deprecated-FieldheaderRelated Notes
board_idtoboard_note_idQA Review — PR #245
Scope Verification
PR correctly addresses all acceptance criteria from issue #199:
Findings
1. Migration edge case — orphaned board_items (medium)
The upgrade backfill SQL:
If any board row has no matching note (e.g.,
migrate_boards_to_notes.pywas never run or a board note was manually deleted), this subquery returns NULL, leavingboard_note_idas NULL. The subsequentalter_column("board_note_id", nullable=False)would then fail with an IntegrityError. The issue constraints say "Only run after ALL consumers confirmed working with note-backed boards," so this is guarded by process, but a safety check in the migration (e.g.,SELECT COUNT(*) FROM board_items WHERE board_note_id IS NULLafter backfill, raising if non-zero) would make it fail with a clear message instead of a cryptic constraint error.2. BoardOut.id semantics changed (low)
Previously
BoardOut.idwas theboards.idPK. Now it isnotes.id. Consumers using board IDs as stable references would see different values. Since the API is slug-based and the MCP SDK uses slugs for all board operations, this is low risk. Worth noting in deployment runbook.3. No test for X-Deprecated-Field header (nit)
The deprecation header is added to item endpoints but no test asserts its presence. Informational only — not blocking.
Code Quality
scripts/migrate_boards_to_notes.pycorrectly deleted_item_to_outcorrectly aliasesboard_note_idto bothboard_idandboard_note_idfieldsVERDICT: APPROVED
Finding #1 is a process-guarded edge case (migration only runs after board notes are confirmed backfilled). Findings #2 and #3 are low/nit severity. No blockers.
PR #245 Review
DOMAIN REVIEW
Stack: Python / FastAPI / SQLAlchemy / Alembic / Pydantic V2
Migration safety (alembic/versions/t0o1p2q3r4s5_drop_legacy_boards_table.py)
down_revision = "s9n0o1p2q3r4"correctly chains after the retype migration. Verified against the existing migration directory.batch_alter_tableis used for SQLite compatibility (test DB). Correct pattern.ALTER COLUMN board_note_id NOT NULLwill fail. This is actually correct behavior -- it forces you to fix data before migrating rather than silently losing references.Model changes (models.py)
Boardclass removed cleanly.BoardItem.board_idcolumn removed.board_note_idchanged fromMapped[int | None](nullable) toMapped[int](non-nullable). Clean.board_noterelationship changed fromMapped["Note | None"]toMapped["Note"]. Consistent with the non-nullable FK.ForeignKey("notes.id")does NOT includeondelete="CASCADE". This is different from the oldForeignKey("boards.id", ondelete="CASCADE"). This is acceptable because: (a)delete_boardexplicitly rejects deletion when items exist, and (b)delete_notecatchesIntegrityErrorand returns 409. However, this is a deliberate behavior change -- board note deletion via the notes API will now be blocked by FK rather than cascading. Worth documenting.Routes rewrite (routes/boards.py)
_resolve_boardnow queriesNotewithnote_type='board'filter. Clean single-query resolution._get_board_note_idhelper removed entirely. No more fallback/warning paths for pre-migration boards. Correct for a post-migration world.BoardItem.board_id == board.idfilters replaced withBoardItem.board_note_id == board_note.id. Consistent across all endpoints: list, create, update, delete, bulk_move, backlog, activity, sync, sync-issues.Noteinstead ofBoard, thenProjectviaNote.project_id. Correct join path._board_to_outmapsboard_note.titletonameand usesboard_note.idas the board id. Since the board IS the note now, this is semantically correct._item_to_outpopulatesboard_idfromitem.board_note_id(deprecated alias). Good backward compat._set_deprecation_headeradded to all item-returning endpoints.X-Deprecated-Fieldheader is a clean signal to consumers.project.slug if project else ""in_board_to_outandboard_note.project and not board_note.project.is_publicin auth checks. Defensive against board notes without projects (shouldn't happen but safe).sync_issuesandupdate_board_item. Correct -- no more transitional backfill needed.create_boardduplicate check now queries Note directly (slug-based) instead of Board (project_id-based). The slug is derived from project slug (board-{project.slug}), so uniqueness is preserved. The check also handles slug collisions with non-board notes (409 with descriptive message).Schema changes (schemas.py)
BoardItemOut.board_idtype unchanged (stillint), now documented as deprecated alias.board_note_idchanged fromint | Nonetoint. Good.BoardOut.note_idfield REMOVED. This is a breaking change for consumers.BLOCKERS
1. Tests assert removed
note_idfield but test file not updated in diffThe PR removes
note_idfromBoardOutschema (schemas.py line 279 in the old code), buttests/test_boards.pyis NOT included in the diff. The following tests onmainassertnote_idis present:assert data["note_id"] is not Noneassert data["note_id"] is not Noneassert "note_id" in boardassert "note_id" in dataIf these tests were updated on the branch, the diff should show those changes. If they were not updated, these 4 tests will fail, contradicting the "685 tests pass" claim. This needs verification -- either the test updates are missing from the diff, or the test suite was not actually run against the final code.
Resolution: Update these tests to remove
note_idassertions (the board IS the note now, soidis the note id --note_idis redundant). Or if intentionally kept for backward compat, re-addnote_idtoBoardOutas an alias forid.2.
conftest.pystill importsBoardon main -- diff removes it, but verify no other test files import Board directlyThe diff correctly removes
Boardfromconftest.pyimports. Verified via grep: no other test files importBoardfrom models. This is clean -- NOT a blocker after verification.Downgrading item 2 -- only item 1 remains as a blocker.
NITS
Missing
ondelete="CASCADE"documentation: The FK behavior change (from CASCADE to RESTRICT-by-default) onboard_note_idshould be called out in the migration docstring. If someone deletes a board note via the/notes/{slug}endpoint, the IntegrityError path handles it, but the behavior is different from the old Board model where cascading was explicit._board_to_outdoes N+1 query: The helper queriesdb.query(BoardItem).filter(...)to count items. Inlist_boards, this runs once per board. For a small number of boards this is fine, but afunc.countaggregate with GROUP BY would be more efficient. Non-blocking -- current board count is low.Responseparameter ordering: Some endpoints haveresponse: Responseas the first param afterslug, others have it afterbody. Consistency would help readability. Minor.Test docstrings reference old model:
test_delete_board_deletes_notedocstring says "removes both the board row and the board note" -- after this PR there is only the note. Should be updated to "removes the board note." Same fortest_update_board_syncs_note_title("updates both board name and note title" -> just "updates the board note title").SOP COMPLIANCE
199-drop-legacy-boardsmatches issue #199PROCESS OBSERVATIONS
board_idtoboard_note_id. TheX-Deprecated-Fieldheader gives consumers a signal. A Forgejo issue should be created for the pal-e-app migration if not already tracked.VERDICT: NOT APPROVED
One blocker: the
note_idfield removal fromBoardOutis not reflected in the test file changes. Either the tests need updating (and should be in this diff), ornote_idshould be preserved as a backward compat alias. Please verify and resubmit.PR #245 Re-Review
This is a re-review after fix commit
305a973addressed the blocker from the initial review (4 tests asserting the removednote_idfield onBoardOut).BLOCKER RESOLUTION
The previous review identified 1 blocker: 4 tests in
TestBoardNoteIntegrationassertednote_idwas present inBoardOutresponses, but the schema change removed that field. The fix commit updates all 4:test_create_board_returns_no_note_id-- asserts"note_id" not in data(was:data["note_id"] is not None)test_get_board_returns_no_note_id-- asserts"note_id" not in data(was:data["note_id"] is not None)test_list_boards_returns_no_note_id-- asserts"note_id" not in board(was:"note_id" in board+board["note_id"] is not None)test_existing_fields_preserved-- asserts"note_id" not in data(was:"note_id" in data)All 4 assertions now match the schema:
BoardOutno longer hasnote_idbecause the board IS the note (itsidis the note id). Test names and docstrings updated to reflect the new semantics. Correct.DOMAIN REVIEW
Python/FastAPI/SQLAlchemy -- all findings from the initial review still hold and are clean:
batch_alter_tableused for SQLite compat.Boardclass removed.BoardItem.board_note_idpromoted toMapped[int](non-nullable). Relationship simplified toMapped["Note"].Boardjoins toNotejoins withnote_type == "board"filter._resolve_boardreturnsNotedirectly. Backfill logic removed (no longer needed).BoardOut.note_idremoved (redundant --idIS the note id).BoardItemOut.board_idkept as deprecated alias populated fromboard_note_id.board_note_idpromoted fromint | Nonetoint.X-Deprecated-Fieldheader set on all item-returning endpoints.board_idalias preserved inBoardItemOut.Boardremoved fromconftest.pyandroutes/boards.py. The only other import was in the deletedscripts/migrate_boards_to_notes.py.No new findings. No regressions introduced by the fix commit.
BLOCKERS
None. Previous blocker resolved.
NITS
Carried forward from initial review (non-blocking, tracked for follow-up):
t0o1p2q3r4s5). Works but diverges from Alembic's default hex hash convention. Cosmetic only.project_slugempty string fallback in_board_to_out:project_slug=project.slug if project else ""-- defensive, but a board note without a project would be a data integrity issue. Consider logging a warning ifproject is None.SOP COMPLIANCE
199-drop-legacy-boardsmatches issue #199PROCESS OBSERVATIONS
board_idtoboard_note_id). TheX-Deprecated-Fieldheader provides a clean transition path.VERDICT: APPROVED