Skip to content

Use database cache for mg-context#1611

Merged
PaulAdamDavis merged 13 commits intomainfrom
dev-mg-context-db
Mar 29, 2026
Merged

Use database cache for mg-context#1611
PaulAdamDavis merged 13 commits intomainfrom
dev-mg-context-db

Conversation

@PaulAdamDavis
Copy link
Copy Markdown
Member

WIP

coderabbitai[bot]

This comment was marked as outdated.

@TryGhost TryGhost deleted a comment from coderabbitai bot Mar 29, 2026
@TryGhost TryGhost deleted a comment from cursor bot Mar 29, 2026
@TryGhost TryGhost deleted a comment from coderabbitai bot Mar 29, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 29, 2026

Warning

Rate limit exceeded

@PaulAdamDavis has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 16 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 2 minutes and 16 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 51dd8f28-3e99-4f91-a4b0-cf5936b53887

📥 Commits

Reviewing files that changed from the base of the PR and between 4f035a3 and a2eec8c.

📒 Files selected for processing (9)
  • packages/mg-context/.gitignore
  • packages/mg-context/README.md
  • packages/mg-context/examples/README.md
  • packages/mg-context/examples/foreach-ghost-post.ts
  • packages/mg-context/examples/generate-and-export.ts
  • packages/mg-context/examples/generate-large-export.ts
  • packages/mg-context/src/lib/MigrateContext.ts
  • packages/mg-context/src/lib/db-helpers.ts
  • packages/mg-context/src/test/migrate-context.test.ts

Walkthrough

This PR adds SQLite-backed persistence to mg-context: a new database module, prepared statements, caches, transactional helpers, and DB lifecycle methods (init, close). MigrateContext is converted to async, exposing DB access, async post/tag/author iteration, filtering, batching, transaction support, Ghost JSON streaming exports (writeGhostJson, writeGhostTagsJson, writeGhostUsersJson), and forEachGhostPost. PostContext/TagContext/AuthorContext gain persistence (save, fromRow/toGhostPost), lookupKey deduplication, content-conversion caching, and DB-backed reconstruction. Tests, scripts, README examples, and mg-json slug-reset are updated for the new flow.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description is vague ('WIP') and does not convey meaningful information about the changeset despite being technically related to the PR. Replace 'WIP' with a brief description of the key changes, such as 'Add SQLite database persistence to mg-context, replacing in-memory storage with database-backed operations' and status.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: introducing database persistence (cache) to mg-context, shifting from in-memory storage to SQLite-backed storage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev-mg-context-db

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

🧹 Nitpick comments (1)
packages/mg-context/src/test/tasks.test.ts (1)

50-63: Avoid double-saving inside forEachPost().

MigrateContext.forEachPost() in packages/mg-context/src/lib/MigrateContext.ts:178-199 already calls post.save(this.db) after await callback(post). Saving again here adds an extra write cycle and can become visible if save() ever stops being fully idempotent for relations.

💡 Suggested simplification
                 await ctx.MGContext.forEachPost(async (post: any) => {
                     post.addTag({
                         name: 'My Tag',
                         slug: 'my-tag'
                     });

                     post.addAuthor({
                         name: 'Jane',
                         slug: 'jane',
                         email: 'jane@example.com'
                     });
-
-                    await post.save(ctx.MGContext.db);
                 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/mg-context/src/test/tasks.test.ts` around lines 50 - 63, The test is
double-saving posts: remove the redundant await post.save(ctx.MGContext.db) in
packages/mg-context/src/test/tasks.test.ts because MigrateContext.forEachPost()
already calls post.save(this.db) after awaiting the callback; instead, keep all
mutations inside the provided callback (the post.addTag/addAuthor calls) and
rely on forEachPost to persist them, ensuring you do not introduce an extra
write by calling post.save again.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/mg-context/README.md`:
- Around line 187-199: The README currently says "Export all tags/users" but
writeGhostTagsJson() and writeGhostUsersJson() only export referenced (used)
entities; update the documentation text and examples to clarify that these
functions export only tags/users that are referenced by posts (i.e., "used"
queries), not orphan/standalone tags or authors. Mention the filtering behavior
explicitly next to the examples and, if helpful, add a short note pointing to
the "used" query behavior or function names (writeGhostTagsJson,
writeGhostUsersJson) so readers know why some tags/users may be omitted.
- Around line 74-78: Update the README paragraph to state that setting HTML is
lazy: calling post.set('html', ...) only invalidates cached lexical/mobiledoc
and defers regeneration until conversion is required (e.g., when
convertContent(), getFinal(), or writeGhostJson() are invoked), rather than
performing immediate/eager conversion on set()/save(); mention that the default
contentFormat remains 'lexical' but conversion happens lazily on those explicit
operations.

In `@packages/mg-context/scripts/foreach-ghost-post.ts`:
- Line 5: The hardcoded machine-local DB path in the MigrateContext constructor
(const ctx = new MigrateContext(...)) makes the script non-portable; change it
to accept a configurable path (e.g., read from an environment variable like
process.env.DB_PATH or a CLI argument) and fall back to a repo-relative default
using path.join(process.cwd(), 'packages/mg-context/scripts/yolo.db') or
similar. Update the script’s argument parsing (or add a small env check) so
MigrateContext is instantiated with the resolved dbPath variable instead of the
literal '/Users/paul/...'.
- Around line 4-25: The code currently calls ctx.init() then awaits
ctx.forEachGhostPost(...) and finally ctx.close(), but if forEachGhostPost
throws the ctx.close() call is skipped; modify main so that after creating and
awaiting ctx.init() you wrap the work (the forEachGhostPost call and any
post-processing) in a try/finally block and move ctx.close() into the finally to
guarantee the MigrateContext.close() is always invoked; keep references to
MigrateContext, main, ctx.init(), ctx.forEachGhostPost, and ctx.close() to
locate where to apply the try/finally.
- Around line 10-18: The callback passed to ctx.forEachGhostPost currently
triggers lint errors: add a suppression for the unused parameter by inserting
"void post;" at the top of the async callback body, change the sleep Promise
from an implicit-return executor to a block executor (e.g., new Promise(r => {
setTimeout(r, 250); });) to avoid no-promise-executor-return, and add "//
eslint-disable-next-line no-console" immediately above each console.log to
silence no-console errors; these edits should be applied inside the async (json,
post) => { ... } callback used with ctx.forEachGhostPost.

In `@packages/mg-context/scripts/generate-large-export.ts`:
- Around line 55-56: The hard-coded absolute DB path passed to the
MigrateContext constructor makes the script non-portable; change the code that
creates new MigrateContext(...) (and the const ctx variable) to derive dbPath
from either a command-line flag or environment variable with a sensible
repo-relative default (e.g., path.join(__dirname, '..', 'scripts', 'yolo.db'))
so that if no flag/env is provided the script uses the repo-relative DB,
otherwise it uses the provided path; ensure the rest of the flow (ctx.init())
continues to use that dbPath variable.
- Around line 16-19: The script uses direct console logging inside the
process.on('exit', ...) handler and other spots (references: process.on('exit',
_maxMemoryConsumption, _dtOfMaxMemoryConsumption, and the other console
occurrences around lines noted) which violates eslint-plugin-ghost; either
replace these console.* calls with the approved logger used in this repo or, if
CLI/intentional, add targeted comments like "// eslint-disable-next-line
no-console" immediately above each console invocation (also apply the same
change to the other console uses you flagged at ranges 58-64, 95-99, 112-115,
119-143) so lint passes while keeping the intent and location of the logs
unchanged.
- Around line 47-139: The main function currently initializes a MigrateContext
(ctx) but only calls ctx.close() on the happy path, so exceptions leave the DB
open; wrap the lifecycle so ctx.close() always runs (use a try/finally around
the body after await ctx.init() or ensure ctx.close() is invoked in a finally
block), keeping the existing await ctx.init(), all loops/transactions (the for
loop with ctx.transaction, ctx.forEachPost, and the ctx.writeGhostJson call)
inside the try and calling ctx.close() in finally; reference MigrateContext,
main, ctx.init(), ctx.transaction, ctx.forEachPost, ctx.writeGhostJson and
ctx.close() when applying the change.

In `@packages/mg-context/src/lib/AuthorContext.ts`:
- Around line 53-60: The save() method currently returns early on an
author-cache hit which prevents the later upsert/update logic from running;
modify the slug-cache branch in save() (where db.authorCache is used) to still
set this.dbId and this.ghostId from the cached entry but do NOT return early —
let execution fall through so updateAuthorById (or the existing upsert path)
runs when this.dbId exists; in short: keep the dedupe cache lookup to populate
ids but remove the early return so save() performs the upsert/update
(updateAuthorById) to persist richer name/email/metadata when present.

In `@packages/mg-context/src/lib/database.ts`:
- Around line 139-152: The SELECTs for findUsedTags and findUsedAuthors lack
deterministic ordering, causing nondeterministic exports; update the prepared
statements for findUsedTags and findUsedAuthors to append an ORDER BY clause
(e.g., ORDER BY slug ASC or ORDER BY id ASC) so rows are returned in a stable,
deterministic order—locate the findUsedTags and findUsedAuthors prepared
statements in database.ts and modify their SQL to include the chosen ORDER BY.

In `@packages/mg-context/src/lib/db-helpers.ts`:
- Around line 4-17: withTransaction currently issues BEGIN/COMMIT/ROLLBACK
unconditionally causing nested BEGIN failures in SQLite; modify withTransaction
(in packages/mg-context/src/lib/db-helpers.ts) to detect db.inTransaction and
only call db.db.exec('BEGIN') / 'COMMIT' / 'ROLLBACK' when not already in a
transaction, and only toggle db.inTransaction for the outermost invocation
(i.e., set inTransaction = true before BEGIN when entering outer transaction and
restore it to false on outer completion in finally), while still running the
callback and propagating errors as before so nested callers (e.g.,
MigrateContext code that marks an outer transaction) reuse the existing
transaction.

In `@packages/mg-context/src/lib/MigrateContext.ts`:
- Around line 204-210: When handling a prebuilt PostContext instance in
MigrateContext.addPost (the branch where post instanceof PostContext), the code
only copies warnOnLookupKeyDuplicate and omits applying this.#contentFormat,
allowing the post to retain a different contentFormat; update that branch to
assign the enclosing context's content format to the instance (e.g., set
newPost.contentFormat = this.#contentFormat) before calling
newPost.save(this.db) so PostContext instances are consistent with the other
branch that constructs a new PostContext with contentFormat.
- Around line 90-93: The init() method currently overwrites this.#db by calling
createDatabase(storage) without handling prior connections; update init() to
guard against repeated calls by checking if this.#db already exists and either
return early (no-op) or throw to fail fast, or if reinitialization is required,
call this.#db.close() (or equivalent DatabaseSync close method) before assigning
a new connection; reference init(), this.#db, createDatabase(), DatabaseSync,
this.#ephemeral and this.#dbPath when making the change.

In `@packages/mg-context/src/lib/PostContext.ts`:
- Around line 313-394: Wrap the whole PostContext.save(db) body in a single DB
transaction: call BEGIN (or use a db.transaction helper) at the start of save
and COMMIT at the end, and ensure any early returns (e.g. duplicate-skip after
findPostByLookupKey) still COMMIT/ROLLBACK appropriately; on exception ROLLBACK
and rethrow. Specifically, start a transaction before calling
db.stmts.updatePost.run / db.stmts.insertPost.run and before the subsequent
deletePostTagsByPostId/insertPostTag and
deletePostAuthorsByPostId/insertPostAuthor calls, commit after all these
statements succeed, and rollback if any statement fails so partial writes cannot
persist. Use the existing db object (the same one passed to save) and target the
statements insertPost.run, updatePost.run, deletePostTagsByPostId.run,
insertPostTag.run, deletePostAuthorsByPostId.run, insertPostAuthor.run and the
findPostByLookupKey.get path when implementing the transaction boundaries.

In `@packages/mg-context/src/test/migrate-context.test.ts`:
- Around line 948-969: The test currently stubs console.warn with warnMock but
restores it only at the end, risking leaked stubs if an assertion throws; wrap
the test body so warnMock.mock.restore() always runs (use a try/finally) and
ensure instance.close() is also called in the finally block; apply the same
change to the similar test around the other block (lines noted in review) so
both tests always restore the console stub and close the MigrateContext instance
even on failure.

---

Nitpick comments:
In `@packages/mg-context/src/test/tasks.test.ts`:
- Around line 50-63: The test is double-saving posts: remove the redundant await
post.save(ctx.MGContext.db) in packages/mg-context/src/test/tasks.test.ts
because MigrateContext.forEachPost() already calls post.save(this.db) after
awaiting the callback; instead, keep all mutations inside the provided callback
(the post.addTag/addAuthor calls) and rely on forEachPost to persist them,
ensuring you do not introduce an extra write by calling post.save again.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b815c2cb-5e0e-4e4b-a133-2dc9059ea816

📥 Commits

Reviewing files that changed from the base of the PR and between 9852740 and 6fa9628.

📒 Files selected for processing (20)
  • .gitignore
  • packages/mg-context/README.md
  • packages/mg-context/package.json
  • packages/mg-context/scripts/foreach-ghost-post.ts
  • packages/mg-context/scripts/generate-large-export.ts
  • packages/mg-context/src/index.ts
  • packages/mg-context/src/lib/AuthorContext.ts
  • packages/mg-context/src/lib/MigrateBase.ts
  • packages/mg-context/src/lib/MigrateContext.ts
  • packages/mg-context/src/lib/PostContext.ts
  • packages/mg-context/src/lib/TagContext.ts
  • packages/mg-context/src/lib/database.ts
  • packages/mg-context/src/lib/db-helpers.ts
  • packages/mg-context/src/test/migrate-context.test.ts
  • packages/mg-context/src/test/post-context.test.ts
  • packages/mg-context/src/test/tasks.test.ts
  • packages/mg-json/lib/to-ghost-json/index.js
  • packages/mg-json/lib/to-ghost-json/meta-to-ghost.js
  • packages/mg-json/lib/to-ghost-json/process-post-relations.js
  • packages/mg-json/test/to-ghost-json.test.js
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/mg-json/lib/to-ghost-json/meta-to-ghost.js
  • packages/mg-context/package.json
  • packages/mg-context/src/lib/MigrateBase.ts
  • packages/mg-json/lib/to-ghost-json/index.js
  • packages/mg-context/src/lib/TagContext.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
packages/mg-context/src/lib/MigrateContext.ts (1)

207-210: ⚠️ Potential issue | 🟠 Major

Keep addPost(postInstance) aligned with the other branches.

This branch saves the passed PostContext without normalizing its contentFormat, while the object/no-arg branches always force this.#contentFormat. A prebuilt PostContext can therefore persist/export in a different format than the enclosing MigrateContext.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/mg-context/src/lib/MigrateContext.ts` around lines 207 - 210, The
addPost branch that handles an existing PostContext (in MigrateContext) must
normalize its contentFormat before persisting; update the branch that detects
post instanceof PostContext to set the PostContext's contentFormat to
this.#contentFormat (and preserve warnOnLookupKeyDuplicate) prior to calling
save(this.db) so it matches the object/no-arg branches' behavior and avoids
persisting a mismatched contentFormat.
packages/mg-context/src/lib/database.ts (1)

77-97: ⚠️ Potential issue | 🟠 Major

Make tag/author slugs unique at the DB layer.

TagContext.save() and AuthorContext.save() both treat slug as the stable lookup key, but findTagBySlug/findAuthorBySlug here read a single row from tables that only have non-unique indexes. Once duplicate slugs exist, saves and lookups become nondeterministic and can update the wrong record. Add unique constraints on Tags.slug and Authors.slug (nullable-safe if needed).

Also applies to: 134-152

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/mg-context/src/lib/database.ts` around lines 77 - 97, Add a
uniqueness constraint on the slug columns so TagContext.save/AuthorContext.save
and the lookup helpers findTagBySlug/findAuthorBySlug are deterministic: modify
the table DDL for Tags and Authors to make slug unique (e.g., specify slug TEXT
UNIQUE or add a UNIQUE index like CREATE UNIQUE INDEX IF NOT EXISTS ux_tags_slug
ON Tags(slug)), and do the same for Authors (ux_authors_slug); ensure the change
is applied both where Tags/Authors are created (current block and the similar
block at lines ~134-152) so nullable slugs remain allowed but duplicate non-null
slugs are rejected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/mg-context/src/lib/database.ts`:
- Around line 168-180: createDatabase currently constructs DatabaseSync(dbPath)
then runs PRAGMAs, executes SCHEMA and calls prepareStatements, but if any of
those steps throw the SQLite handle (db) is leaked; modify createDatabase to
wrap the initialization steps (PRAGMA calls, db.exec(SCHEMA),
prepareStatements(db)) in a try/catch, and in the catch call db.close() (or the
appropriate close on DatabaseSync) before rethrowing the error so the
DatabaseSync handle is always closed on failure; reference createDatabase,
DatabaseSync, SCHEMA, prepareStatements, and the returned DatabaseModels when
applying the change.

In `@packages/mg-context/src/lib/MigrateContext.ts`:
- Around line 181-200: forEachPost currently paginates the mutable filtered
query and then saves each post inside the loop, which allows the filter to
change between batches and causes skipped/duplicated processing; fix by
snapshotting a stable list of matching post IDs up front (use `#buildFilterWhere`
to build the filter and run a single query to collect all matching post IDs) and
then iterate those IDs in batches: for each id call find post by id (or
findPostsWhere filtered to id list), construct PostContext.fromRow, run the
callback, and save with withTransaction/post.save; ensure you no longer re-run
the original mutable filtered query between batches so changes to
tags/authors/created_at/published_at cannot shift pagination.
- Around line 105-117: The transaction method should short-circuit when a
transaction is already open: if this.db.inTransaction is true, simply await and
return callback() without calling BEGIN/COMMIT/ROLLBACK or changing the
inTransaction flag; otherwise run the existing BEGIN, set inTransaction=true,
await callback, COMMIT on success, ROLLBACK on error, and reset
inTransaction=false in finally. Update the transaction method (function name
transaction in MigrateContext / this.db.inTransaction usage) to branch on
existing inTransaction before executing any SQL or toggling the flag.

---

Duplicate comments:
In `@packages/mg-context/src/lib/database.ts`:
- Around line 77-97: Add a uniqueness constraint on the slug columns so
TagContext.save/AuthorContext.save and the lookup helpers
findTagBySlug/findAuthorBySlug are deterministic: modify the table DDL for Tags
and Authors to make slug unique (e.g., specify slug TEXT UNIQUE or add a UNIQUE
index like CREATE UNIQUE INDEX IF NOT EXISTS ux_tags_slug ON Tags(slug)), and do
the same for Authors (ux_authors_slug); ensure the change is applied both where
Tags/Authors are created (current block and the similar block at lines ~134-152)
so nullable slugs remain allowed but duplicate non-null slugs are rejected.

In `@packages/mg-context/src/lib/MigrateContext.ts`:
- Around line 207-210: The addPost branch that handles an existing PostContext
(in MigrateContext) must normalize its contentFormat before persisting; update
the branch that detects post instanceof PostContext to set the PostContext's
contentFormat to this.#contentFormat (and preserve warnOnLookupKeyDuplicate)
prior to calling save(this.db) so it matches the object/no-arg branches'
behavior and avoids persisting a mismatched contentFormat.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 570a2395-1d95-4e6d-96e6-6684f4bd0e48

📥 Commits

Reviewing files that changed from the base of the PR and between 6fa9628 and 4f035a3.

📒 Files selected for processing (6)
  • packages/mg-context/README.md
  • packages/mg-context/scripts/foreach-ghost-post.ts
  • packages/mg-context/scripts/generate-large-export.ts
  • packages/mg-context/src/lib/MigrateContext.ts
  • packages/mg-context/src/lib/database.ts
  • packages/mg-context/src/lib/db-helpers.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/mg-context/README.md
  • packages/mg-context/src/lib/db-helpers.ts

@PaulAdamDavis PaulAdamDavis merged commit b21e139 into main Mar 29, 2026
2 checks passed
@PaulAdamDavis PaulAdamDavis deleted the dev-mg-context-db branch March 29, 2026 20:08
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