Skip to content

MTM-6: bookmark persistence foundation (article_bookmarks)#181

Merged
devin-ai-integration[bot] merged 2 commits into
feat/reading-listfrom
devin/1782442112-mtm-6-bookmark-foundation
Jun 26, 2026
Merged

MTM-6: bookmark persistence foundation (article_bookmarks)#181
devin-ai-integration[bot] merged 2 commits into
feat/reading-listfrom
devin/1782442112-mtm-6-bookmark-foundation

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 26, 2026

Copy link
Copy Markdown

Summary

Sequential foundation for the Reading List epic (MTM-1). Adds the bookmark persistence layer by mirroring the existing favorite feature so the pattern is immediately recognizable. Pure persistence — no REST/GraphQL/ArticleData/ArticleQueryService changes (those land in later stories on top of this).

Design decision D1 (pre-decided): the reading list cursors on article_bookmarks.created_at (bookmark time), not the article timestamp. D1 fixes the cursor source; the asc/desc-per-direction mechanics follow the existing repo pattern (see read-service note below). Query-service cursor wiring is MTM-3.

What changed

  • MTM-7 — V3__create_article_bookmarks.sql (additive; V1/V2 untouched):

    create table article_bookmarks (
      article_id varchar(255) not null,
      user_id    varchar(255) not null,
      created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
      primary key (article_id, user_id)        -- DB-level idempotency
    );
    create index idx_article_bookmarks_user_created
      on article_bookmarks (user_id, created_at desc);

    created_at is new vs article_favorites to support "most-recent-bookmarked first" ordering. No FKs (consistent with current schema style).

  • MTM-8 — core/bookmark/: ArticleBookmark(articleId, userId) entity (same Lombok as ArticleFavorite) + ArticleBookmarkRepository (save, find, remove).

  • MTM-9 — persistence impl: ArticleBookmarkMapper (@Mapper) + ArticleBookmarkMapper.xml; MyBatisArticleBookmarkRepository (@Repository) with idempotent save:

    if (mapper.find(b.getArticleId(), b.getUserId()) == null) mapper.insert(b);

    Insert omits created_at so the DB default applies.

  • MTM-10 — ArticleBookmarksReadService (@Mapper) + XML:

    • isUserBookmark(userId, articleId)count(1) mapped to boolean
    • userBookmarks(ids, currentUser) → only the current user's ids (privacy), mirrors userFavorites
    • findUserBookmarkedArticleIdsWithCursor(userId, page) → cursor on created_at, direction-dependent ordering (see below), limit #{page.queryLimit}
  • MTM-11 — integration tests (src/test/java/io/spring/infrastructure/bookmark/, extend DbTestBase):

    • MyBatisArticleBookmarkRepositoryTest: save→find present; save twice→exactly one row (idempotent, no exception); remove→empty; remove non-existent→no-op.
    • ArticleBookmarksReadServiceTest: isUserBookmark true/false; userBookmarks excludes other users' ids; cursor query ordering NEXT/PREV honoring limit; PREV when results exceed page size returns the nearest, non-overlapping page; empty list→empty. Rows inserted with explicit distinct created_at for deterministic ordering.

Cursor ordering (Devin Review follow-up, orchestrator-approved)

The first revision hardcoded order by created_at desc for both directions. That returns the wrong page for PREV once results above the cursor exceed the page size. Now it matches ArticleReadService/CommentReadService:

NEXT:  created_at < cursor   order by created_at desc
PREV:  created_at > cursor   order by created_at asc
limit #{page.queryLimit}

MTM-3 can then apply the same trim extra → Collections.reverse() on PREV handling used by ArticleQueryService.findRecentArticlesWithCursor to always return a stable newest-bookmarked-first page. D1 (cursor keyed on bookmark created_at) is unchanged.

Test setup note

The test profile caps Flyway at V1 (spring.flyway.target=1) to exclude V2 seed data, so V3's table isn't auto-created in @MybatisTest DBs. Bumping the target would load seed data and break existing read-model count assertions, so the two bookmark tests create article_bookmarks programmatically (same DDL as V3) in @BeforeEach. The real V3 migration is verified separately (see below).

Verification gate (green)

$ ./gradlew clean build -x test
BUILD SUCCESSFUL

$ ./gradlew clean test -x jacocoTestCoverageVerification   # CI command (.github/workflows/gradle.yml)
BUILD SUCCESSFUL   # incl. 9 new bookmark tests, 0 failures

$ ./gradlew spotlessCheck
BUILD SUCCESSFUL

Migration verified on a fresh DB via bootRun:

Migrating schema "main" to version "1 - create tables"
Migrating schema "main" to version "2 - seed data"
Migrating schema "main" to version "3 - create article bookmarks"
Successfully applied 3 migrations to schema "main", now at version v3
Started RealWorldApplication

Note: bare ./gradlew clean test fails only on jacocoTestCoverageVerification (repo-wide 33%, pre-existing baseline unrelated to this change); CI excludes that task via -x jacocoTestCoverageVerification.

Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/922cc2316251497186672e6b2f01c386


Open in Devin Review

Mirror the favorite feature across the persistence layer:
- V3 Flyway migration creating article_bookmarks (composite PK + created_at)
- core/bookmark domain entity + repository interface
- MyBatis mapper/XML + MyBatis-backed repository impl (idempotent save)
- ArticleBookmarksReadService (isUserBookmark, userBookmarks, cursor query
  ordered by bookmark created_at desc)
- persistence integration tests for repository and read service
@mbatchelor81 mbatchelor81 self-assigned this Jun 26, 2026
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

devin-ai-integration[bot]

This comment was marked as resolved.

Per orchestrator decision (Option 2): order article_bookmarks.created_at
DESC for NEXT and ASC for PREV (cursor predicate unchanged: NEXT < cursor,
PREV > cursor), matching ArticleReadService/CommentReadService so MTM-3 can
wire it with the standard Collections.reverse-on-PREV handling. Cursor stays
keyed on bookmark created_at (D1 unchanged).

Update PREV assertion and add test covering PREV paging when results exceed
the page size (asserts the nearest, non-overlapping page).

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

Open in Devin Review

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🚩 No API controller or application service wires up the new bookmark feature

This PR adds domain model, repository, read service, migration, and tests for bookmarks, but no REST controller, GraphQL mutation, or application-layer service consumes these components. The bookmark feature is not yet exposed to any client. This is presumably intentional (incremental delivery of the persistence layer first), but worth confirming that a follow-up PR is planned to wire up API endpoints.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Intentional. MTM-6 is the persistence-only foundation for the Reading List epic (MTM-1); REST controller and GraphQL mutation/query are explicitly out of scope here and are delivered in later stories (the query-service cursor wiring is MTM-3). No client-facing wiring in this PR by design.

@devin-ai-integration devin-ai-integration Bot merged commit 371434e into feat/reading-list Jun 26, 2026
3 checks passed
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.

1 participant