Skip to content

fix: use synchronous transaction callbacks for better-sqlite3#200

Merged
ng merged 2 commits intodevfrom
fix/sync-transactions-199
Mar 16, 2026
Merged

fix: use synchronous transaction callbacks for better-sqlite3#200
ng merged 2 commits intodevfrom
fix/sync-transactions-199

Conversation

@ng
Copy link
Copy Markdown
Contributor

@ng ng commented Mar 16, 2026

Summary

  • Convert all 10 db.transaction(async (tx) => { await tx... }) calls to synchronous db.transaction((tx) => { tx... }) — better-sqlite3 doesn't support async transaction callbacks
  • Fixes schedules.ts (9 transactions: create/update/delete for temperature, power, alarm)
  • Fixes settings.ts (1 transaction: update device settings)

Closes #199

Test plan

  • POST /api/trpc/schedules.createTemperatureSchedule returns 200 instead of 500
  • createPowerSchedule and createAlarmSchedule also work
  • Update and delete operations for all three schedule types
  • Device settings update still works

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor
    • Optimized internal database transaction handling for schedule and device settings operations, improving code consistency and reliability across create, update, and delete functions.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 16, 2026

Warning

Rate limit exceeded

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

⌛ 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5a58ec35-3fa1-47c6-a573-da19a420fe2f

📥 Commits

Reviewing files that changed from the base of the PR and between d71abf0 and d4eff45.

📒 Files selected for processing (2)
  • src/server/routers/schedules.ts
  • src/server/routers/settings.ts
📝 Walkthrough

Walkthrough

This PR fixes transaction handling in schedule and settings routers by converting async transaction callbacks to synchronous-style handlers and explicitly calling .all() on query results. This resolves the "Transaction function cannot return a promise" error that was blocking all schedule creation and modification operations.

Changes

Cohort / File(s) Summary
Schedule Transactions
src/server/routers/schedules.ts
Converted async transaction callbacks to synchronous handlers across temperature, power, and alarm schedule operations (create, update, delete). Added .all() calls to explicitly collect query results within transactions. Maintains existing validation and error handling logic.
Settings Transactions
src/server/routers/settings.ts
Updated device update mutation to use synchronous transaction handler instead of async callback. Added .all() calls after select and update operations to ensure consistent array returns.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Poem

🐰 Async callbacks caused a fright,
Promises returned weren't quite right,
Now synchronous flows through the tx chain,
With .all() collecting—schedules work again!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: use synchronous transaction callbacks for better-sqlite3' directly describes the main change—converting async transaction callbacks to synchronous ones for better-sqlite3 compatibility.
Linked Issues check ✅ Passed The PR resolves issue #199 by converting all 10 async transaction callbacks to synchronous callbacks with .all() for query execution, directly fixing the 'Transaction function cannot return a promise' error affecting schedule write operations.
Out of Scope Changes check ✅ Passed All changes are focused on fixing transaction callback patterns in schedules.ts and settings.ts, directly addressing the linked issue with no extraneous modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/sync-transactions-199
📝 Coding Plan
  • Generate coding plan for human review comments

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

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes schedule and device-settings write endpoints by removing async transaction callbacks, which are not supported by Drizzle’s better-sqlite3 driver (and currently cause “Transaction function cannot return a promise” 500s).

Changes:

  • Convert schedule create/update/delete mutations to use synchronous db.transaction((tx) => ...) callbacks.
  • Convert device settings update mutation to use a synchronous transaction callback.
  • Keep scheduler reloads occurring after the transaction scope (intended to run post-commit).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/server/routers/schedules.ts Reworks all schedule write transactions to be synchronous.
src/server/routers/settings.ts Reworks device settings update transaction to be synchronous.
Comments suppressed due to low confidence (8)

src/server/routers/settings.ts:101

  • Within a better-sqlite3 synchronous transaction callback, tx.select() should be executed with .all()/.get() (see other in-repo uses of .all() in transactions). const [current] = tx.select()...limit(1) risks destructuring a query object rather than rows.
        const updated = db.transaction((tx) => {
          // Fetch current settings to validate final computed state
          const [current] = tx
            .select()
            .from(deviceSettings)
            .where(eq(deviceSettings.id, 1))
            .limit(1)

src/server/routers/schedules.ts:155

  • Same issue for updates: in a sync transaction callback you can't await, so you need a synchronous execution method. tx.update(...).returning() should be executed via .returning().get()/.all() before destructuring, otherwise [result] may throw or be undefined.
        const updated = db.transaction((tx) => {
          const [result] = tx
            .update(temperatureSchedules)
            .set({ ...updates, updatedAt: new Date() })
            .where(eq(temperatureSchedules.id, id))
            .returning()
          if (!result) {

src/server/routers/schedules.ts:342

  • Same execution issue for the update: tx.update(...).returning() needs .returning().get()/.all() in a sync transaction callback; otherwise [result] may fail or not reflect the DB write.
          const [result] = tx
            .update(powerSchedules)
            .set({ ...updates, updatedAt: new Date() })
            .where(eq(powerSchedules.id, id))
            .returning()
          if (!result) {

src/server/routers/schedules.ts:388

  • Delete path has the same issue: tx.delete(...).returning() should be executed synchronously via .returning().get()/.all() (or use .run() plus a separate select) so the NOT_FOUND check is based on real results.
        db.transaction((tx) => {
          const [deleted] = tx
            .delete(powerSchedules)
            .where(eq(powerSchedules.id, input.id))
            .returning()
          if (!deleted) {

src/server/routers/schedules.ts:537

  • tx.delete(alarmSchedules)...returning() should be executed via .returning().get()/.all() in this sync transaction callback; without that, the NOT_FOUND logic may not run against actual DB results.
        db.transaction((tx) => {
          const [deleted] = tx
            .delete(alarmSchedules)
            .where(eq(alarmSchedules.id, input.id))
            .returning()
          if (!deleted) {

src/server/routers/settings.ts:134

  • tx.update(deviceSettings)...returning() inside this sync transaction callback needs an execution step (.returning().get()/.all()). Otherwise [result] may not be a real row and could throw at runtime.
          const [result] = tx
            .update(deviceSettings)
            .set({ ...input, updatedAt: new Date() })
            .where(eq(deviceSettings.id, 1))
            .returning()

src/server/routers/schedules.ts:318

  • This select is inside a synchronous transaction callback; without await you should execute it with .all()/.get(). As written, const [existing] = tx.select(...).limit(1) may attempt to destructure a query object rather than actual rows.
            const [existing] = tx
              .select({ onTime: powerSchedules.onTime, offTime: powerSchedules.offTime })
              .from(powerSchedules)
              .where(eq(powerSchedules.id, id))
              .limit(1)

src/server/routers/schedules.ts:491

  • tx.update(alarmSchedules)...returning() needs a synchronous execution call (.returning().get()/.all()) inside this sync transaction callback; otherwise [result] may be invalid at runtime.
        const updated = db.transaction((tx) => {
          const [result] = tx
            .update(alarmSchedules)
            .set({ ...updates, updatedAt: new Date() })
            .where(eq(alarmSchedules.id, id))
            .returning()
          if (!result) {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread src/server/routers/schedules.ts Outdated
Comment on lines 196 to 200
db.transaction((tx) => {
const [deleted] = tx
.delete(temperatureSchedules)
.where(eq(temperatureSchedules.id, input.id))
.returning()
Comment thread src/server/routers/schedules.ts Outdated
const created = await db.transaction(async (tx) => {
const [result] = await tx.insert(alarmSchedules).values(input).returning()
const created = db.transaction((tx) => {
const [result] = tx.insert(alarmSchedules).values(input).returning()
Comment thread src/server/routers/schedules.ts Outdated
const created = await db.transaction(async (tx) => {
const [result] = await tx.insert(temperatureSchedules).values(input).returning()
const created = db.transaction((tx) => {
const [result] = tx.insert(temperatureSchedules).values(input).returning()
Comment thread src/server/routers/schedules.ts Outdated
const created = await db.transaction(async (tx) => {
const [result] = await tx.insert(powerSchedules).values(input).returning()
const created = db.transaction((tx) => {
const [result] = tx.insert(powerSchedules).values(input).returning()
better-sqlite3 transactions are synchronous and cannot accept async
callbacks. Remove async/await from all db.transaction() calls and add
.all() for sync query execution in schedules and settings routers.

Closes #199

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ng ng force-pushed the fix/sync-transactions-199 branch from ad1a546 to d71abf0 Compare March 16, 2026 05:01
Copy link
Copy Markdown
Contributor Author

@ng ng left a comment

Choose a reason for hiding this comment

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

Adversarial Review — Standard Depth (Sonnet Optimizer + Sonnet Skeptic)

Recommendation: Approve

The core fix is correct, complete, and mechanically verified. All 10 broken db.transaction(async ...) calls converted to synchronous callbacks with .all() terminators. TypeScript, lint, and all 400 tests pass.

Confirmed findings (no blockers)

Severity Finding Status
🟢 Minor reloadScheduler() inside try — if scheduler reload fails after committed write, caller gets misleading 500 Deferred
🟢 Minor Delete handlers silently drop db.transaction() return value Deferred
🟢 Minor createPowerSchedule/updatePowerSchedule missing .meta() and .output() — unreachable via REST Deferred
⚪ Nit No comment explaining better-sqlite3 rollback-on-throw guarantee Noted

Pre-existing issues surfaced

Finding
Promise.all on sync better-sqlite3 queries — no actual parallelism
settings.ts other mutations still use await pattern (works but inconsistent)
getJobManager() singleton race on failed loadSchedules()
validateTimeRange rejects midnight-crossing schedules

Full review artifacts: .claude/reviews/fix-sync-transactions-199/


🤖 adversarial-review — standard depth

- Isolate reloadScheduler() in its own try/catch so scheduler failures
  don't surface as misleading mutation errors after committed writes
- Add void cast to delete transaction calls for clarity
- Add missing .meta() and .output() to createPowerSchedule and
  updatePowerSchedule for OpenAPI/REST exposure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version 1.1.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

schedules.createTemperatureSchedule fails: Transaction function cannot return a promise

2 participants