Skip to content

0.0.13 - React Query, Drizzle, db logic in renderer#645

Merged
AlexDumo merged 67 commits intomainfrom
prerelease/0.0.13
Sep 29, 2025
Merged

0.0.13 - React Query, Drizzle, db logic in renderer#645
AlexDumo merged 67 commits intomainfrom
prerelease/0.0.13

Conversation

@AlexDumo
Copy link
Collaborator

@AlexDumo AlexDumo commented Sep 23, 2025

Almost a complete re-write of all database logic so that it is defined in the renderer using Drizzle ORM, rather than raw sql in the main process. Additionally, all the massive zustand stores that stored db data (e.g. marcherPages, beats, etc) are now using React Query

Database rewrite

The problem

  • OpenMarch's database interactions were done with a "custom" ORM that I wrote out of raw SQL
    • While this worked, adding new functionality took a ton of time
  • History grouping was very inconsistent from function-to-function
    • It was never too clear how to make database actions belong to the same "history group"
  • Having the database logic in the main process meant there was a lot of code that needed to be written to get new database interactions to go to the renderer.
    • I.e. even adding a simple SQL filter could create a completely new IPC endpoint

The solution

  • Use Drizzle ORM to create maintainable, flexible, and easy-to-write db interactions
  • Create a transactionWithHistory wrapper that can be accessed in the renderer that allows developers to create custom batch functions without needing to write new db functions
  • Move all the database code to the renderer, connected by an SQLite proxy (made by @jakepusateri, who inspired much of this transition) so db functions can quickly be written with just a few lines of code in the renderer

Bonus

  • OpenMarch on the web
    • With all the database logic in the renderer, this significantly reduces the barrier-to-entry to creating a web version
    • The only big change would be swapping out better-sqlite3 with sqljs, which drizzle supports

Better testing

  • Automatically testing many better-sqlite3 and sqljs
    • The describeDbTest function and VITEST_ENABLE_BETTER_SQLITE env variable will run the test suite with both database drivers
    • This will be crucial if we find ourselves using both drivers, since better-sqlite3 is more performant, but we can't use it in a web version
    • Drizzle makes this easy, because the ORM syntax is the same
  • Baked-in history testing
    • The testWithHistory function wraps the tests you use it with in a history check
    • It will:
      1. Record the db state before the test
      2. After the test, count how many actions were performed
      3. Undo that many actions
      4. Validate the data after undo is the same as before the test
      5. Redo the undos, validate again
      6. Repeat 3 times (to check for strange grouping errors from going between undo and redo)
    • You can also explicitly define how many actions you expected to occur in the test with expectNumberOfChanges
    • Look at src/test/history.ts for more info

Tanstack Query

Formerly React Query

The problem

  • Data fetching in OpenMarch was very greedy
  • Stores like the marcherPageStore or timingObjectsStore would hold the data for the entire show
    • The lookup times in these stores could be mitigated by making records and maps (something @dhruvachakravarthi did great work with) but the memory usage gets worse and worse the larger the show
  • When an update is made, all data in the applicable store is re-fetched
    • E.g. one marcher is moved on page 1, coordinates for all marchers and all pages are refetched

The soltuion

  • Use React Query's key magic to have a more intelligent storage system
  • This lets us store data, for example, by page id and cache nearby pages
  • Example
    1. Fetch data for the first few pages
    2. When the selected page changes, always have +/- 2 pages cached and ready
    3. When data is modified, only re-fetch the relevant data for the current and surrounding pages

Bonuses

  • Updates to db data are now hook-based using mutations.
    • Mutations are defined with the queries they need to invalidate.
    • This means, the components calling the mutation don't need to worry about refetching any data
  • We get more concise caching and loading states
  • Tanstack provides an awesome devtool experience where you can view the data of every query and simulate refetching, errors, etc
image

close #305
close #302

Summary by CodeRabbit

  • New Features

    • MusicXML import with transactional import hook; editable pathway support via a new hook.
  • Changes

    • Core UI now uses query-backed cached loading for more reliable canvas, inspector, and export flows.
    • Database schema migrations add/normalize tables, timestamps, constraints.
    • Undo/Redo menu and related history IPCs removed.
  • Localization

    • Placeholder format updated and new page/export status messages added.
  • Documentation

    • New guidelines for database interaction patterns and multi-query hooks.
  • Chores

    • VSCode debug config, lint updates, gitignore tweaks.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 23, 2025

Walkthrough

Replaces large parts of the legacy DB layer and tests with a Drizzle + React Query-driven data flow, adds an SQL.js driver and unsafe SQL IPC proxy, migrates many UI components to query/mutation hooks, adds migrations/snapshots, and removes numerous legacy table modules, exports, and tests.

Changes

Cohort / File(s) Summary
Docs / Rules
.cursor/rules/*
Adds new guidelines for database/hook patterns (basic-database-interaction.mdc, component-database-interaction.mdc, multi-query-hooks.mdc, command-syntax.mdc), removes database-interaction.mdc, and toggles unit-testing.mdc applicability.
ESLint / CI / IDE / gitignore
/.eslintrc.cjs, /.gitignore, .vscode/launch.json, .github/workflows/test.yaml
Adds TanStack Query ESLint preset, parserOptions, ignore patterns, Vitest debug launch config, CI workflow adjustments and a desktop history test step.
Migrations & Schema Snapshots
apps/desktop/electron/database/migrations/*, .../meta/*, .../_journal.json
Adds/updates migrations 0006–0009, snapshots 0006–0009 and journal entries; rebuilds many tables (marcher_pages, midsets, shape-related tables, timestamps, rotation_degrees), constraints, and indexes.
ORM, Driver & Migration Service
.../services/sqljs-driver.ts, .../services/DrizzleMigrationService.ts, .../migrations/schema.ts, .../migrations/triggers.ts
Adds SQL.js Drizzle driver, broadens DB typing to include DbConnection, makes trigger functions async/DB-agnostic, and adapts migration initialization to use ORM/drizzle patterns.
Electron DB services & IPC
apps/desktop/electron/database/database.services.ts, apps/desktop/electron/database/DatabaseActions.ts
Introduces unsafe SQL proxy handler and direct unsafe path; removes many legacy IPC handlers and deletes DatabaseActions.ts. Adjusts history imports and removes some undo-group instrumentation.
Legacy table modules removed
apps/desktop/electron/database/tables/*
Deletes many legacy table modules and their public APIs/types (BeatTable, MarcherTable, PageTable, Shape*, ShapePageMarcherTable, MarcherPageTable, UtilityTable, etc.).
Tests & Test utils
apps/desktop/electron/database/__test__/*, apps/desktop/electron/database/tables/__test__/*, apps/desktop/electron/database/tables/__test__/testUtils.ts
Removes extensive table/history test suites, consolidates some imports, and adds ORM-based test init (DbConnection / initTestDatabaseOrm).
Preload / Main / Menus
apps/desktop/electron/preload/index.ts, apps/desktop/electron/main/index.ts, apps/desktop/electron/main/application-menu.ts
Narrows preload APP_API (removes many CRUD IPC endpoints), adds unsafeSqlProxy and log, removes undo/redo and triggerFetch IPCs, and comments out Undo/Redo menu items.
App boot & history wiring
apps/desktop/src/App.tsx
Subscribes to mutation cache events to invalidate history queries and invokes createAllUndoTriggers(db) when DB ready.
React Query migration (UI)
apps/desktop/src/components/**/*
Replaces many store/direct DB calls with TanStack Query queries/mutations across canvas, inspector, field, marcher, exporting, music, tempo groups; adds hooks (useEditablePath, useImportMusicXml, useSplitPage); updates data shapes (e.g., path_start_position/path_end_position) and keys.
MusicXML import
apps/desktop/src/components/music/*
Adds transactional MusicXML import implementation and useImportMusicXml hook; selector delegates to the hook.
Mocks & visuals
apps/desktop/src/__mocks__/*, apps/desktop/src/components/canvas/__test__/MocksForCanvas.ts
Updates mocks to new DB shapes (remove id_for_html, rename path fields), replaces class-instantiated marchers with plain objects, updates marcher visuals construction.
Frontend public API removals
apps/desktop/src/api/api.ts, apps/desktop/electron/preload/index.ts
Removes performUndo / performRedo and many renderer DB IPC endpoints; exposes unsafeSqlProxy and log on preload surface.
i18n
apps/desktop/i18n/*.json
Replaces Mustache {{}} placeholders with single-brace {} syntax and adds several locale keys.
Misc UI changes & props
apps/desktop/src/components/*
Adds/refactors hooks and mutations (e.g., useEditablePath, useSplitPage), updates components to React Query patterns, changes some imports/props (e.g., TempoGroupCard prop).
Packaging & e2e
apps/desktop/package.json, apps/desktop/e2e/*
Adjusts migrate script, moves rimraf to devDependencies, and updates e2e blank fixture path handling.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Renderer
  participant Preload as Preload (ipcRenderer)
  participant Main as Main (ipcMain)
  participant DB as Drizzle DB

  rect rgb(230,242,255)
    note right of Renderer: Unsafe SQL proxy (new)
    Renderer->>Preload: APP_API.unsafeSqlProxy(sql)
    Preload->>Main: ipc.invoke("unsafeSql:proxy", sql)
    Main->>DB: handleUnsafeSqlProxyWithDb(sql)
    DB-->>Main: rows / error
    Main-->>Preload: { rows } / error
    Preload-->>Renderer: { rows } / error
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant UI as MusicXmlSelector
  participant Hook as useImportMusicXml (mutation)
  participant DB as Drizzle DB
  participant Cache as QueryClient

  User->>UI: select MusicXML file
  UI->>Hook: mutate({ file, allPages, measures, allBeats })
  Hook->>DB: begin transaction: replace measures/beats, create beats, update pages
  DB-->>Hook: success / error
  alt success
    Hook->>Cache: invalidate measures/beats/pages
    Hook-->>UI: success message
  else error
    Hook-->>UI: error message
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • dukcc

Poem

I nibble at the schema's root,
New queries sprout where old code snoozed.
Drizzle rains and migrations hop,
Unsafe whispers reach the top.
Pages sing and marchers trot—🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues Check ⚠️ Warning The changes in this PR do not include any targeted updates or new triggers for the field_properties table to record redo entries in history_redo nor explicit query invalidation or refetch logic for field_properties on undo or redo events as required by issues #305 and #302. Please add specific history triggers for updates to the field_properties table to ensure redo entries are recorded and include explicit invalidation or refetch calls for the field_properties query so the UI is refreshed immediately after undo or redo operations.
Out of Scope Changes Check ⚠️ Warning This PR contains extensive refactoring and new features well beyond undo/redo and field property refresh requirements, including wholesale removal of multiple database modules, application-wide React Query integration, migration snapshots, new documentation rules, and UI changes across unrelated components. Consider restricting this pull request to the specific fixes for the field_properties undo/redo and refresh behavior, and move the broader database and UI refactors to separate, focused PRs for clearer review and scope alignment.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly highlights the core updates of this release by mentioning React Query integration, Drizzle ORM adoption, and relocating database logic to the renderer, matching the main changes in the PR.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch prerelease/0.0.13

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
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: 30

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (18)
apps/desktop/src/components/inspector/PageEditor.tsx (1)

61-66: Fix first-page detection: compare IDs, not object refs.

Object identity is brittle; different instances representing the same page will fail this check, causing incorrect disabling and display.

-        if (pages.length > 0) {
-            const firstPage = pages[0];
-            setIsFirstPage(selectedPage === firstPage);
-        }
+        if (pages.length > 0) {
+            const firstPage = pages[0];
+            setIsFirstPage(!!selectedPage && selectedPage.id === firstPage.id);
+        }
apps/desktop/i18n/es.json (2)

196-199: Placeholder name mismatch: use {status}, not {estado}.

This will throw at runtime or during i18n compile/validation.

Apply this diff:

-        "marcher": "Marcher: {estado}",
+        "marcher": "Marcher: {status}",

1-756: Add missing export keys and fix ICU/placeholder issues in es.json

  • Missing keys — add exportCoordinates.marchersNotLoaded and exportCoordinates.marcherPagesNotLoaded to apps/desktop/i18n/es.json (these are present in en.json).
  • Invalid ICU plural — apps/desktop/i18n/es.json:27 (setSelectedToPreviousSuccess) contains a malformed double-brace plural ("{{count, plural...}"); fix to a valid ICU plural pattern ({count, plural, one {…} other {…}}). Also check apps/desktop/i18n/pt-BR.json:27.
  • Placeholder name mismatch — apps/desktop/i18n/es.json:197 ("marcher": "Marcher: {estado}") uses {estado}; rename to the placeholder used in en.json (e.g., {status}) so runtime interpolation matches.
apps/desktop/i18n/en.json (1)

1-762: Fix ICU plural syntax and placeholder name mismatch in i18n locales

  • Invalid ICU plural syntax (extra "{") in apps/desktop/i18n/es.json and apps/desktop/i18n/pt-BR.json at actions.batchEdit.setSelectedToPreviousSuccess — contains "{{count, plural,...}}". Replace the double "{{" with a single "{" so it's "{count, plural,...}".
  • Placeholder mismatch: apps/desktop/i18n/es.json exportCoordinates.marcher uses {estado} while en.json (and other locales) use {status}. Rename {estado} → {status} so placeholder names are consistent.
apps/desktop/i18n/pt-BR.json (1)

1-756: Add missing exportCoordinates keys and fix ICU plural formatting

  • Add exportCoordinates.marchersNotLoaded and exportCoordinates.marcherPagesNotLoaded to apps/desktop/i18n/pt-BR.json to match en.json.
  • Fix the invalid double-brace ICU plural occurrences at:
    • apps/desktop/i18n/pt-BR.json — actions.batchEdit.setSelectedToPreviousSuccess (line ~27)
    • apps/desktop/i18n/es.json — actions.batchEdit.setSelectedToPreviousSuccess (line ~27)
      Replace the malformed "{{count, plural…}}" insertion with a proper ICU plural block.
apps/desktop/src/components/marcher/NewMarcherForm.tsx (3)

80-112: Guard against NaN drillOrder; prevents invalid insert

If the user clears the drill order field, parseInt('') yields NaN. That flows into Set/loop math and can reach the DB as NaN (violates integer schema).

Apply this diff to short‑circuit invalid input before building payloads:

     const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
-        setContent(<MarcherListContents />, "marchers");
         event.preventDefault();
+        // Navigate after we know we submitted (optional)
+        setContent(<MarcherListContents />, "marchers");
+        if (!Number.isFinite(drillOrder)) {
+            setDrillOrderError(t("marchers.drillOrderError.invalid"));
+            return;
+        }
         let newDrillOrderOffset = 0;
         const existingDrillOrders = new Set<number>(
             (marchers || [])
                 .filter(
                     (marcher: Marcher) => marcher.drill_prefix === drillPrefix,
                 )
                 .map((marcher: Marcher) => marcher.drill_order),
         );

Additionally, update validation to treat non‑numeric values as invalid (see separate comment below).


138-154: Validation gap: treat non-numeric drill orders as errors

Without this, NaN passes validation and enables submit.

     const validateDrillOrder = useCallback(
         (drillOrder: number) => {
+            if (!Number.isFinite(drillOrder)) {
+                setDrillOrderError(t("marchers.drillOrderError.required"));
+                return;
+            }
             const existingMarchers = (marchers || []).filter(
                 (marcher: Marcher) => marcher.drill_prefix === drillPrefix,
             );

Also consider disabling submit if drillOrder is not finite (see setSubmitIsDisabled effect).


85-90: Don’t store translated section labels in the DB

On first load you set section to a translated label (defaultSection(t)), but you persist section as-is in the payload. If the user never changes the section, you’ll store localized strings (e.g., “Otro”) instead of canonical keys (“Other”).

Recommended approach: keep state as canonical section keys; translate only for display. Example changes (outside the exact ranges shown):

// near line 33
const DEFAULT_SECTION_KEY = "Other";

// lines 64 and 68
setSection(DEFAULT_SECTION_KEY);

// line 245 (label)
<SelectTriggerButton label={getTranslatedSectionName(section || DEFAULT_SECTION_KEY, t)} />

// line 206 (submit disabled)
section === DEFAULT_SECTION_KEY || section === undefined

// payload (lines 103-107)
newMarchers.push({
  section: section || DEFAULT_SECTION_KEY,
  drill_prefix: drillPrefix,
  drill_order: newDrillOrder,
});

Also applies to: 103-107, 63-69

apps/desktop/electron/main/services/export-utility-process.ts (1)

5-5: Fix build blockers: remove unused import/variable.

path is imported but never used; organizeBySection is destructured but unused. This can fail builds with noUnusedLocals.

-import path from "path";
...
-    const { dbPath, filePath, quarterPages, organizeBySection, ...options } =
-        payload;
+    const { dbPath, filePath, quarterPages, ...options } = payload;

Also applies to: 156-158

apps/desktop/src/components/marcher/__test__/NewMarcherForm.test.tsx (1)

55-65: Fix tests that mock a removed Marcher store

  • Tests and mocks still import/vi.mock "@/stores/MarcherStore" and "@/stores/marcher/useMarcherStore" but there is no Marcher store implementation under apps/desktop/src/stores — this will break test compilation. Affected files: apps/desktop/src/components/marcher/test/NewMarcherForm.test.tsx, apps/desktop/src/components/marcher/test/MarcherList.test.tsx, apps/desktop/src/mocks/components/ChangeMarchersSelection.mock.tsx.
  • Replace these mocks with the new React Query hooks (mock the query/mutation hooks) and wrap affected tests with a QueryClientProvider — or restore/fix the module path before re‑enabling the suites.
apps/desktop/electron/database/tables/__test__/testUtils.ts (1)

1-61: Move test helper out of legacy electron/database/tables/test

This path is legacy — tests/utilities under apps/desktop/electron/database/tables are being removed. Move apps/desktop/electron/database/tables/test/testUtils.ts to apps/desktop/src/global/classes/test/ or a shared test-utils module and update all imports accordingly. Can draft a follow-up PR to move the file and update imports.

apps/desktop/electron/main/index.ts (2)

585-615: Guard dialog cancellation before accessing filePaths[0]

Accessing path.filePaths[0] when the dialog is canceled throws. The check is currently commented out.

 .then(async (path) => {
-    // If the user cancels the dialog, and there is no previous path, return -1
-    // if (path.canceled || !path.filePaths[0]) return -1;
+    if (path.canceled || !path.filePaths[0]) return -1;
 
     DatabaseServices.setDbPath(path.filePaths[0]);

709-737: Fix order: check cancellation before reading file

readFileSync(path.filePaths[0]) runs even when the dialog is canceled.

 .then(async (path) => {
-    const fileContents = fs.readFileSync(path.filePaths[0]);
-    const jsonStr = fileContents.toString();
-    await updateFieldsPropertiesJSON(jsonStr);
-
-    // If the user cancels the dialog, and there is no previous path, return -1
-    if (path.canceled || !path.filePaths[0]) return -1;
+    if (path.canceled || !path.filePaths[0]) return -1;
+    const fileContents = fs.readFileSync(path.filePaths[0]);
+    const jsonStr = fileContents.toString();
+    await updateFieldsPropertiesJSON(jsonStr);
 
     win?.webContents.send("field_properties:onImport");
apps/desktop/electron/database/database.services.ts (2)

146-184: Implement the "values" method for the Drizzle proxy

Drizzle can call the "values" method. Not handling it will throw at runtime.

     switch (method) {
       case "all":
         rows = result.all(...params);
         return {
           rows: rows
             ? rows.map((row: { [key: string]: any }) => Object.values(row))
             : [],
         };
       case "get":
         rows = result.get(...params);
         return {
           rows: rows ? Object.values(rows as Record<string, any>) : undefined,
         };
+      case "values":
+        rows = result.all(...params);
+        return {
+          rows: rows
+            ? rows.map((row: { [key: string]: any }) => Object.values(row))
+            : [],
+        };
       case "run":
         rows = result.run(...params);
         return { rows: [] };
       default:
         throw new Error(`Unknown method: ${method}`);
     }

245-264: Gate the unsafe SQL IPC in non‑production builds

Exposing arbitrary exec() to the renderer increases risk if the renderer is compromised. Restrict to test/dev via an env flag.

 export function initHandlers() {
   // Generic SQL proxy handler for Drizzle ORM
   ipcMain.handle("sql:proxy", handleSqlProxy);
-  ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy);
+  if (process.env.ENABLE_UNSAFE_SQL_PROXY === "1") {
+    ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy);
+  }

Also ensure preload only calls unsafeSqlProxy when the flag is enabled.

apps/desktop/electron/preload/index.ts (1)

220-243: History IPC handlers missing — implement handlers or remove exposed APIs

Preload exposes history:* methods (apps/desktop/electron/preload/index.ts lines 220-243) but no ipcMain handlers for history:* were found; these ipcRenderer invocations will reject at runtime. Add matching ipcMain.handle/on implementations in the main/database service or remove these methods from preload.

apps/desktop/electron/database/migrations/schema.ts (2)

265-271: Use browser-safe blob type for audio_files.data

blob() maps to Buffer in Drizzle; Buffer isn’t available in the browser/SQL.js. You already defined browserSafeBinaryBlob; use it here to avoid web build issues.

Apply:

 export const audio_files = sqliteTable("audio_files", {
   id: integer().primaryKey(),
   path: text().notNull(),
   nickname: text(),
-  data: blob(),
+  data: browserSafeBinaryBlob(),
   selected: integer().default(0).notNull(),

Note: If this is the last use of blob(), remove blob from the sqlite-core import to avoid unused import warnings.


316-333: Schema/migration drift — make position_order NOT NULL in the migration

apps/desktop/electron/database/migrations/schema.ts (lines 316–333) declares shape_page_marchers.position_order as .notNull(), but apps/desktop/electron/database/migrations/0007_misty_zemo.sql (CREATE TABLE __new_shape_page_marchers around lines ~135–153) defines position_order as nullable; this allows multiple NULLs despite the UNIQUE (shape_page_id, position_order) index. Update 0007 (and any other SQL that recreates this table) to declare position_order INTEGER NOT NULL and backfill existing NULLs (e.g., assign sequential position_order per shape_page_id) before enforcing NOT NULL — or, if NULL is intended, remove .notNull() from schema.ts.

Comment on lines +197 to +203
const updatedItem = await tx
.update(schema.table_name)
.set(updateData)
.where(eq(schema.table_name.id, id))
.returning()
.get();
updatedItems.push(
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Drizzle SQLite: .returning().get() is invalid; destructure the returned rows instead.

After .returning(), Drizzle returns an array. Use array destructuring to get the single updated row.

-        const updatedItem = await tx
-            .update(schema.table_name)
-            .set(updateData)
-            .where(eq(schema.table_name.id, id))
-            .returning()
-            .get();
+        const [updatedItem] = await tx
+            .update(schema.table_name)
+            .set(updateData)
+            .where(eq(schema.table_name.id, id))
+            .returning();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const updatedItem = await tx
.update(schema.table_name)
.set(updateData)
.where(eq(schema.table_name.id, id))
.returning()
.get();
updatedItems.push(
const [updatedItem] = await tx
.update(schema.table_name)
.set(updateData)
.where(eq(schema.table_name.id, id))
.returning();
updatedItems.push(
🤖 Prompt for AI Agents
In .cursor/rules/basic-database-interaction.mdc around lines 197-203, the call
chain uses `.returning().get()` which is invalid for Drizzle SQLite because
`.returning()` yields an array of rows; change the code to await the update with
`.returning()` (no `.get()`), destructure the resulting array (e.g., `const
[updatedItem] = await ...returning()`), and then push that `updatedItem` into
`updatedItems`.

Comment on lines 352 to 359
qc.invalidateQueries({
queryKey: Array.from(itemIds).map((id) =>
{tableName}Keys.byId(id),
),
});
qc.invalidateQueries({
queryKey: [KEY_BASE],
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

React Query v5 invalidateQueries: don’t pass an array of keys to queryKey.

queryKey accepts a single key (array), not an array of keys. Invalidate per id or use a predicate.

-            qc.invalidateQueries({
-                queryKey: Array.from(itemIds).map((id) =>
-                    {tableName}Keys.byId(id),
-                ),
-            });
+            for (const id of itemIds) {
+                qc.invalidateQueries({ queryKey: {tableName}Keys.byId(id) });
+            }

Alternative: use a predicate that matches id-specific keys if you have a stable key shape.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
qc.invalidateQueries({
queryKey: Array.from(itemIds).map((id) =>
{tableName}Keys.byId(id),
),
});
qc.invalidateQueries({
queryKey: [KEY_BASE],
});
for (const id of itemIds) {
qc.invalidateQueries({ queryKey: {tableName}Keys.byId(id) });
}
qc.invalidateQueries({
queryKey: [KEY_BASE],
});
🤖 Prompt for AI Agents
In .cursor/rules/basic-database-interaction.mdc around lines 352 to 359, the
calls to qc.invalidateQueries are passing an array-of-keys (an array of arrays)
as queryKey which is invalid in React Query v5; change this to either call
invalidateQueries once per id with the single queryKey shape (e.g., loop
Array.from(itemIds) and call qc.invalidateQueries({ queryKey:
{tableName}Keys.byId(id) }) ), or replace the multi-key call with a single
invalidateQueries using a predicate that matches id-specific keys
(qc.invalidateQueries({ predicate: q => /* match ids by q.queryKey shape */ }))
and for the base key call invalidateQueries with the single base key
(qc.invalidateQueries({ queryKey: KEY_BASE })) so each invalidateQuery receives
one key/array rather than an array of keys.

Comment on lines +3 to +8
extends: [
"eslint:recommended",
"react-app",
"plugin:astro/recommended",
"plugin:@tanstack/query/recommended",
],
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Good add: TanStack Query linting; verify plugin is installed across workspaces

Extends entry is correct. Please ensure @tanstack/eslint-plugin-query is present in all relevant package.json files.

Run:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Packages missing @tanstack/eslint-plugin-query:"
fd -H -a package.json | while read -r f; do
  has=$(jq -r '(.devDependencies["@tanstack/eslint-plugin-query"] // .dependencies["@tanstack/eslint-plugin-query"]) // empty' "$f")
  if [[ -z "$has" ]]; then
    echo " - $f"
  fi
done

echo
echo "Places extending plugin:@tanstack/query/recommended:"
rg -nP 'plugin:@tanstack/query/recommended' -C2

Length of output: 742


Add @tanstack/eslint-plugin-query to the workspace (missing in multiple package.json)

Extends entry plugin:@tanstack/query/recommended is fine. @tanstack/eslint-plugin-query is not declared in these package.json files:

  • actions/desktop-deploy/package.json
  • actions/desktop-deploy/test/app-in-root/package.json
  • actions/desktop-deploy/test/app-in-subdirectory/package.json
  • apps/desktop/package.json
  • apps/website/package.json
  • packages/config/package.json
  • packages/core/package.json
  • packages/musicxml-parser/package.json
  • packages/path-utility/example/package.json
  • packages/path-utility/package.json
  • packages/ui/package.json

Add @tanstack/eslint-plugin-query as a devDependency (preferably at the monorepo root or in packages that run ESLint) to avoid broken linting.

🤖 Prompt for AI Agents
.eslintrc.cjs around lines 3 to 8: the config extends
"plugin:@tanstack/query/recommended" but the corresponding plugin package
(@tanstack/eslint-plugin-query) is missing from multiple package.json files; add
"@tanstack/eslint-plugin-query" as a devDependency—preferably in the monorepo
root package.json (or in each workspace package that runs ESLint:
actions/desktop-deploy, its test app packages, apps/desktop, apps/website,
packages/config, packages/core, packages/musicxml-parser, packages/path-utility
and packages/ui)—then run your workspace package manager install (pnpm/yarn/npm)
so ESLint can resolve the plugin.

Comment on lines +48 to +50
# metronome test output
packages/metronome/

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Anchor path to repo root; verify we won’t ignore a real package unintentionally

Using a leading slash avoids matching nested paths and makes intent explicit. Also, confirm this directory isn’t (or won’t be) a real package with tracked sources.

Apply this diff to anchor to the repo root:

-packages/metronome/
+/packages/metronome/

Run to verify no tracked files would be hidden:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Tracked files under packages/metronome:"
git ls-files 'packages/metronome' | wc -l
echo
echo "Show any tracked files (should be empty):"
git ls-files 'packages/metronome' || true
echo
echo "Ignored status for the path (sanity check):"
git check-ignore -v packages/metronome/ || true

Length of output: 714


Remove or narrow the .gitignore entry for packages/metronome/ — it contains tracked files

git ls-files shows 15 tracked files under packages/metronome (package.json, src/, tsconfig.json, etc.); don’t ignore the whole package directory. Either remove the line or replace it with a root-anchored, specific ignore for only generated/test output (e.g. /packages/metronome//). File: .gitignore (lines 48–50).

🤖 Prompt for AI Agents
In .gitignore around lines 48–50, the entry "packages/metronome/" wrongly
ignores an entire package that contains tracked source files; remove that line
or replace it with a root-anchored pattern that only ignores generated/test
output (for example "/packages/metronome/<generated-folder>/"). After updating
.gitignore, if any files in packages/metronome are already tracked and should
remain tracked, ensure you do not remove them from the index; if you intended to
stop tracking specific generated files, remove those from the repo with git rm
--cached <path> and commit the change so the narrowed ignore takes effect.

Comment on lines +227 to +235
/** Directly executes the SQL query without any parameters */
async function handleUnsafeSqlProxy(_: any, sql: string) {
try {
return await handleUnsafeSqlProxyWithDb(connect(), sql);
} catch (error: any) {
console.error("Error from unsafe SQL proxy:", error);
throw error;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Close DB connection in unsafe SQL handler to avoid leaks

connect() is called but never closed, leaking a database handle.

 async function handleUnsafeSqlProxy(_: any, sql: string) {
-  try {
-    return await handleUnsafeSqlProxyWithDb(connect(), sql);
-  } catch (error: any) {
+  const db = connect();
+  try {
+    return await handleUnsafeSqlProxyWithDb(db, sql);
+  } catch (error: any) {
     console.error("Error from unsafe SQL proxy:", error);
     throw error;
-  }
+  } finally {
+    db.close();
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/** Directly executes the SQL query without any parameters */
async function handleUnsafeSqlProxy(_: any, sql: string) {
try {
return await handleUnsafeSqlProxyWithDb(connect(), sql);
} catch (error: any) {
console.error("Error from unsafe SQL proxy:", error);
throw error;
}
}
/** Directly executes the SQL query without any parameters */
async function handleUnsafeSqlProxy(_: any, sql: string) {
const db = connect();
try {
return await handleUnsafeSqlProxyWithDb(db, sql);
} catch (error: any) {
console.error("Error from unsafe SQL proxy:", error);
throw error;
} finally {
db.close();
}
}
🤖 Prompt for AI Agents
In apps/desktop/electron/database/database.services.ts around lines 227 to 235,
the code calls connect() but never closes the returned DB handle, leaking
resources; change the function to await connect() into a local db variable, pass
that db to handleUnsafeSqlProxyWithDb, and ensure the db is closed in a finally
block (e.g., try { return await handleUnsafeSqlProxyWithDb(db, sql); } finally {
await db.close(); } or use the appropriate release method for your DB client) so
the connection is always released even on error.

Comment on lines +347 to 353
{selectedShapePageIds.map((shapePageId) => {
return (
<div
key={marcherShape.shapePage.id}
className="flex flex-col gap-12"
>
{singleShapeEditor(marcherShape)}
<div key={shapePageId} className="flex flex-col gap-12">
{singleShapeEditor(
marcherShapesByShapePageId[shapePageId],
)}
</div>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard against missing MarcherShape to prevent runtime crashes.

When a selected shape page has no corresponding MarcherShape on the canvas (e.g., after delete/copy or before canvas is ready), singleShapeEditor receives undefined and will crash. Guard the lookup.

Apply this diff:

-                {selectedShapePageIds.map((shapePageId) => {
-                    return (
-                        <div key={shapePageId} className="flex flex-col gap-12">
-                            {singleShapeEditor(
-                                marcherShapesByShapePageId[shapePageId],
-                            )}
-                        </div>
-                    );
-                })}
+                {selectedShapePageIds.map((shapePageId) => {
+                    const ms = marcherShapesByShapePageId[shapePageId];
+                    if (!ms) return null;
+                    return (
+                        <div key={shapePageId} className="flex flex-col gap-12">
+                            {singleShapeEditor(ms)}
+                        </div>
+                    );
+                })}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{selectedShapePageIds.map((shapePageId) => {
return (
<div
key={marcherShape.shapePage.id}
className="flex flex-col gap-12"
>
{singleShapeEditor(marcherShape)}
<div key={shapePageId} className="flex flex-col gap-12">
{singleShapeEditor(
marcherShapesByShapePageId[shapePageId],
)}
</div>
{selectedShapePageIds.map((shapePageId) => {
const ms = marcherShapesByShapePageId[shapePageId];
if (!ms) return null;
return (
<div key={shapePageId} className="flex flex-col gap-12">
{singleShapeEditor(ms)}
</div>
);
})}
🤖 Prompt for AI Agents
In apps/desktop/src/components/inspector/ShapeEditor.tsx around lines 347 to
353, the code maps selectedShapePageIds and passes
marcherShapesByShapePageId[shapePageId] directly to singleShapeEditor, which can
be undefined and crash; update the mapping to guard the lookup by checking that
marcherShapesByShapePageId[shapePageId] exists before rendering (either filter
selectedShapePageIds to those with a corresponding marcher shape or
conditionally render singleShapeEditor only when the lookup is truthy), ensuring
you keep the outer div key consistent and skip or render a safe fallback when
the shape is missing.

Comment on lines 39 to 44
const deleteMarchers = useMutation(
deleteMarchersMutationOptions(queryClient),
).mutate;
const updateMarchers = useMutation(
updateMarchersMutationOptions(queryClient),
).mutate;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use mutateAsync + await; prevent race conditions and clear refs only on success.

useMutation().mutate is fire-and-forget. This introduces races between delete/update and clears refs even on failure. Switch to mutateAsync, await both operations, and only clear refs on success. Also no-op early when nothing changed.

-    const deleteMarchers = useMutation(
-        deleteMarchersMutationOptions(queryClient),
-    ).mutate;
-    const updateMarchers = useMutation(
-        updateMarchersMutationOptions(queryClient),
-    ).mutate;
+    const { mutateAsync: deleteMarchersAsync } = useMutation(
+        deleteMarchersMutationOptions(queryClient),
+    );
+    const { mutateAsync: updateMarchersAsync } = useMutation(
+        updateMarchersMutationOptions(queryClient),
+    );
@@
-    async function handleSubmit() {
-        setIsEditing(false);
-
-        const modifiedMarchers: ModifiedMarcherArgs[] = [];
-
-        for (const [pageId, changes] of Object.entries(changesRef.current))
-            modifiedMarchers.push({ id: Number(pageId), ...changes });
-
-        if (deletionsRef.current.length > 0) {
-            const marcherIdsSet = new Set(deletionsRef.current.map((id) => id));
-            await deleteMarchers(marcherIdsSet);
-        }
-
-        const result = updateMarchers(modifiedMarchers);
-        changesRef.current = {};
-        deletionsRef.current = [];
-        return result;
-    }
+    async function handleSubmit() {
+        setIsEditing(false);
+
+        const modifiedMarchers: ModifiedMarcherArgs[] = [];
+        for (const [pageId, changes] of Object.entries(changesRef.current)) {
+            modifiedMarchers.push({ id: Number(pageId), ...changes });
+        }
+
+        if (deletionsRef.current.length === 0 && modifiedMarchers.length === 0) {
+            return;
+        }
+
+        try {
+            if (deletionsRef.current.length > 0) {
+                const marcherIdsSet = new Set(deletionsRef.current.map((id) => id));
+                await deleteMarchersAsync(marcherIdsSet);
+            }
+            if (modifiedMarchers.length > 0) {
+                await updateMarchersAsync(modifiedMarchers);
+            }
+            // clear only after successful mutations
+            changesRef.current = {};
+            deletionsRef.current = [];
+        } catch {
+            // onError toasts fire in mutation options; keep refs for retry
+        }
+    }

Also applies to: 67-84

🤖 Prompt for AI Agents
In apps/desktop/src/components/marcher/MarcherList.tsx around lines 39-44 (and
similarly for the block at 67-84), the code uses useMutation(...).mutate which
is fire-and-forget; change to useMutation(...).mutateAsync and await the calls,
add an early return/no-op when there are no marchers to update/delete, and only
clear refs (or perform UI state resets) after the awaited mutation resolves
successfully (handle errors via try/catch and do not clear refs on failure);
ensure both delete and update use await mutateAsync and run sequentially or in
Promise.all as intended so race conditions are prevented.

ModifiedSectionAppearanceArgs,
NewSectionAppearanceArgs,
} from "@/global/classes/SectionAppearance";
import { SectionAppearance } from "@/global/classes/SectionAppearance";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix type mismatch: use DatabaseSectionAppearance (DB shape) instead of SectionAppearance (class).

The query returns DatabaseSectionAppearance (snake_case fields). Typing local state as SectionAppearance causes TS errors and field mismatches (likely the build failure). Import the DB type and use it for local state.

Apply this diff:

-import { SectionAppearance } from "@/global/classes/SectionAppearance";
+import type { DatabaseSectionAppearance } from "@/db-functions";
@@
-    const [localAppearances, setLocalAppearances] = useState<
-        SectionAppearance[]
-    >([]);
+    const [localAppearances, setLocalAppearances] = useState<
+        DatabaseSectionAppearance[]
+    >([]);

Also applies to: 62-64

🤖 Prompt for AI Agents
In apps/desktop/src/components/marcher/section/SectionAppearanceList.tsx around
line 22 (and also apply changes at lines 62-64), replace the runtime class type
SectionAppearance with the database shape type DatabaseSectionAppearance: import
the DatabaseSectionAppearance type from the module that declares your DB types
and change the local state and any variables/props typed as SectionAppearance to
DatabaseSectionAppearance so they match the snake_case query result shape;
update the type annotations for the state initialization and any map/filter
callbacks to use DatabaseSectionAppearance.

Comment on lines +49 to +61
const queryClient = useQueryClient();
const { data: sectionAppearances } = useQuery(
allSectionAppearancesQueryOptions(),
);
const { mutate: createSectionAppearances } = useMutation(
createSectionAppearancesMutationOptions(queryClient),
);
const { mutate: updateSectionAppearances } = useMutation(
updateSectionAppearancesMutationOptions(queryClient),
);
const { mutate: deleteSectionAppearances } = useMutation(
deleteSectionAppearancesMutationOptions(queryClient),
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use mutateAsync and await; current try/catch and success toast run before the mutation finishes.

mutate is not promise-based; await mutate(...) resolves immediately and errors won't be caught. Switch to mutateAsync and await to sequence update/delete and correctly handle success/error.

Apply this diff:

   const queryClient = useQueryClient();
   const { data: sectionAppearances } = useQuery(
       allSectionAppearancesQueryOptions(),
   );
-  const { mutate: createSectionAppearances } = useMutation(
+  const { mutateAsync: createSectionAppearancesAsync } = useMutation(
       createSectionAppearancesMutationOptions(queryClient),
   );
-  const { mutate: updateSectionAppearances } = useMutation(
+  const { mutateAsync: updateSectionAppearancesAsync } = useMutation(
       updateSectionAppearancesMutationOptions(queryClient),
   );
-  const { mutate: deleteSectionAppearances } = useMutation(
+  const { mutateAsync: deleteSectionAppearancesAsync } = useMutation(
       deleteSectionAppearancesMutationOptions(queryClient),
   );
@@
   if (modifiedAppearances.length > 0) {
-      updateSectionAppearances(modifiedAppearances);
+      await updateSectionAppearancesAsync(modifiedAppearances);
   }
 
   if (deletionsRef.current.length > 0) {
-      deleteSectionAppearances(new Set(deletionsRef.current));
+      await deleteSectionAppearancesAsync(new Set(deletionsRef.current));
   }
@@
   try {
-      await createSectionAppearances([newAppearance]);
+      await createSectionAppearancesAsync([newAppearance]);
       toast.success(`Added style for ${sectionName}`);
   } catch (error) {

Also applies to: 96-103, 168-177

🤖 Prompt for AI Agents
In apps/desktop/src/components/marcher/section/SectionAppearanceList.tsx around
lines 49-61 (and likewise update usages at 96-103 and 168-177), the component
uses mutate (callback style) which is not promise-based so try/catch and success
toasts run before the mutation completes; replace mutate with mutateAsync, await
the call inside async handlers, and move try/catch around the awaited
mutateAsync so success and error toasts run after the mutation finishes; also
adjust handler signatures to be async and ensure you pass the same arguments to
mutateAsync as to mutate.

Comment on lines +98 to +110
let beatPosition = 0;
const measureStartBeatPositions: number[] = [];
const newBeats: NewBeatArgs[] = parsedMeasures.flatMap(
(measure) => {
measureStartBeatPositions.push(beatPosition);
return measure.beats.map((beat) => ({
position: beatPosition++,
duration: beat.duration,
include_in_measure: true,
notes: beat.notes,
}));
},
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix type error: NewBeatArgs does not include position (likely build break).

You're passing position in NewBeatArgs objects. NewBeatArgs only allows duration/include_in_measure/notes. This is very likely the cause of the build failure.

Apply this diff to remove the invalid field and compute measure starts via an offset:

-            let beatPosition = 0;
+            let beatIndex = 0;
             const measureStartBeatPositions: number[] = [];
-            const newBeats: NewBeatArgs[] = parsedMeasures.flatMap(
-                (measure) => {
-                    measureStartBeatPositions.push(beatPosition);
-                    return measure.beats.map((beat) => ({
-                        position: beatPosition++,
-                        duration: beat.duration,
-                        include_in_measure: true,
-                        notes: beat.notes,
-                    }));
-                },
-            );
+            const newBeats: NewBeatArgs[] = parsedMeasures.flatMap(
+                (measure) => {
+                    const start = beatIndex;
+                    measureStartBeatPositions.push(start);
+                    beatIndex += measure.beats.length;
+                    return measure.beats.map((beat) => ({
+                        duration: beat.duration,
+                        include_in_measure: true,
+                        notes: beat.notes,
+                    }));
+                },
+            );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let beatPosition = 0;
const measureStartBeatPositions: number[] = [];
const newBeats: NewBeatArgs[] = parsedMeasures.flatMap(
(measure) => {
measureStartBeatPositions.push(beatPosition);
return measure.beats.map((beat) => ({
position: beatPosition++,
duration: beat.duration,
include_in_measure: true,
notes: beat.notes,
}));
},
);
let beatIndex = 0;
const measureStartBeatPositions: number[] = [];
const newBeats: NewBeatArgs[] = parsedMeasures.flatMap(
(measure) => {
const start = beatIndex;
measureStartBeatPositions.push(start);
beatIndex += measure.beats.length;
return measure.beats.map((beat) => ({
duration: beat.duration,
include_in_measure: true,
notes: beat.notes,
}));
},
);
🤖 Prompt for AI Agents
In apps/desktop/src/components/music/MusicXmlImport.ts around lines 98 to 110,
the NewBeatArgs objects currently include a non-existent `position` property
which causes a type/build error; remove `position` from the constructed objects
and instead compute measure start positions by tracking a running beat offset:
push the current offset into measureStartBeatPositions at the start of each
measure, create each beat object with only duration/include_in_measure/notes,
and increment the running beat offset by the beat's duration as you iterate (do
not place position on the NewBeatArgs).

Copy link
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/desktop/electron/database/migrations/schema.ts (2)

186-193: Allow NULL path positions for marcher pages

path_start_position/path_end_position are declared nullable and the docstring says NULL should be interpreted as 0/1, but this CHECK rejects NULL because SQLite treats NULL as constraint failure. Inserts will blow up whenever we leave these optional fields unset. Please relax the CHECK to tolerate NULL while still enforcing the 0‑1 bound when a value is provided, and regenerate the snapshot/migration after the change.

         check(
             "marcher_pages_path_data_position_check",
-            sql`path_start_position >= 0 AND path_start_position <= 1 AND path_end_position >= 0 AND path_end_position <= 1`,
+            sql`(path_start_position IS NULL OR (path_start_position >= 0 AND path_start_position <= 1)) AND (path_end_position IS NULL OR (path_end_position >= 0 AND path_end_position <= 1))`,
         ),

217-224: Allow NULL path positions for midsets

Same issue here: the CHECK currently bars NULL even though both columns are optional. This will cause failures when midsets don’t specify a path. Please apply the same NULL-tolerant guard as above.

         check(
             "midsets_path_data_position_check",
-            sql`path_start_position >= 0 AND path_start_position <= 1 AND path_end_position >= 0 AND path_end_position <= 1`,
+            sql`(path_start_position IS NULL OR (path_start_position >= 0 AND path_start_position <= 1)) AND (path_end_position IS NULL OR (path_end_position >= 0 AND path_end_position <= 1))`,
         ),
🧹 Nitpick comments (1)
apps/desktop/src/__mocks__/TestSqlProxy.ts (1)

23-23: Keep initializeDatabase type-safe

Switching to as any masks the contract mismatch between the raw better-sqlite3 handle used in tests and the proxy/Drizzle database shape initializeDatabase now expects. If the migrator starts calling proxy-only APIs (e.g., batch or history helpers), this will compile but fail at runtime. Please feed the method the correctly typed database (wrap the in-memory handle the same way the renderer does or widen the accepted type in DrizzleMigrationService) instead of suppressing the type-checker.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d338eb and 56008e9.

📒 Files selected for processing (13)
  • CLAUDE.md (0 hunks)
  • apps/desktop/electron/database/__test__/DatabaseMocks.ts (1 hunks)
  • apps/desktop/electron/database/database.services.ts (5 hunks)
  • apps/desktop/electron/database/migrations/0008_round_mathemanic.sql (1 hunks)
  • apps/desktop/electron/database/migrations/meta/0008_snapshot.json (1 hunks)
  • apps/desktop/electron/database/migrations/meta/_journal.json (1 hunks)
  • apps/desktop/electron/database/migrations/schema.ts (11 hunks)
  • apps/desktop/electron/database/tables/__test__/testUtils.ts (2 hunks)
  • apps/desktop/electron/preload/index.ts (2 hunks)
  • apps/desktop/src/__mocks__/TestSqlProxy.ts (1 hunks)
  • apps/desktop/src/__mocks__/components/ChangeMarchersSelection.mock.tsx (0 hunks)
  • apps/desktop/src/__mocks__/globalMocks.ts (4 hunks)
  • apps/desktop/src/components/canvas/__test__/MocksForCanvas.ts (3 hunks)
💤 Files with no reviewable changes (2)
  • CLAUDE.md
  • apps/desktop/src/mocks/components/ChangeMarchersSelection.mock.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/desktop/electron/database/test/DatabaseMocks.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/electron/database/migrations/meta/_journal.json
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/repo.mdc)

Use zustand for state management in the application

Files:

  • apps/desktop/electron/database/tables/__test__/testUtils.ts
  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/preload/index.ts
  • apps/desktop/src/__mocks__/TestSqlProxy.ts
  • apps/desktop/electron/database/migrations/schema.ts
  • apps/desktop/src/__mocks__/globalMocks.ts
  • apps/desktop/src/components/canvas/__test__/MocksForCanvas.ts
apps/desktop/electron/**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Use absolute paths for file operations; avoid relative file paths

Files:

  • apps/desktop/electron/database/tables/__test__/testUtils.ts
  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/preload/index.ts
  • apps/desktop/electron/database/migrations/schema.ts
apps/desktop/electron/database/tables/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Do not add or modify legacy IPC-based table files under electron/database/tables; they are being removed

Files:

  • apps/desktop/electron/database/tables/__test__/testUtils.ts
apps/desktop/electron/database/database.services.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Remove/avoid imports of legacy table modules from electron/database/database.services.ts

Files:

  • apps/desktop/electron/database/database.services.ts
apps/desktop/electron/preload/index.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Remove/avoid IPC handlers and calls for database access in electron/preload/index.ts

Files:

  • apps/desktop/electron/preload/index.ts
**/__mocks__/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place test mocks under mocks directories

Files:

  • apps/desktop/src/__mocks__/TestSqlProxy.ts
  • apps/desktop/src/__mocks__/globalMocks.ts
apps/desktop/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use the @/ alias for imports referencing the src/ directory

Files:

  • apps/desktop/src/__mocks__/TestSqlProxy.ts
  • apps/desktop/src/__mocks__/globalMocks.ts
  • apps/desktop/src/components/canvas/__test__/MocksForCanvas.ts
🧠 Learnings (17)
📓 Common learnings
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/database.services.ts : Remove/avoid imports of legacy table modules from electron/database/database.services.ts
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/**/*.ts : Do not add or modify legacy IPC-based table files under electron/database/tables; they are being removed
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/__test__/*.test.ts : Remove legacy tests under electron/database/tables/__test__; merge into classes tests
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/__test__/*.test.ts : Remove legacy tests under electron/database/tables/__test__; merge into classes tests

Applied to files:

  • apps/desktop/electron/database/tables/__test__/testUtils.ts
  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/preload/index.ts
  • apps/desktop/src/__mocks__/TestSqlProxy.ts
  • apps/desktop/electron/database/migrations/schema.ts
  • apps/desktop/src/__mocks__/globalMocks.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : In entity tests, call internal *Db functions for raw results and public wrappers for DatabaseResponse

Applied to files:

  • apps/desktop/electron/database/tables/__test__/testUtils.ts
  • apps/desktop/src/__mocks__/TestSqlProxy.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : Use setupTestSqlProxy() in beforeEach for DB-related tests

Applied to files:

  • apps/desktop/electron/database/tables/__test__/testUtils.ts
  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/src/__mocks__/TestSqlProxy.ts
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Ensure full TypeScript type safety, including exported Database[TableName], New[TableName]Args, and Modified[TableName]Args types

Applied to files:

  • apps/desktop/electron/database/tables/__test__/testUtils.ts
  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/src/__mocks__/TestSqlProxy.ts
  • apps/desktop/src/__mocks__/globalMocks.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Use Drizzle-inferred types for DB entities (e.g., typeof table.$inferSelect) and avoid manually duplicating schema interfaces

Applied to files:

  • apps/desktop/electron/database/tables/__test__/testUtils.ts
  • apps/desktop/src/__mocks__/TestSqlProxy.ts
  • apps/desktop/electron/database/migrations/schema.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : Consolidate tests for migrated entities under src/global/classes/__test__/[Entity].test.ts

Applied to files:

  • apps/desktop/electron/database/tables/__test__/testUtils.ts
  • apps/desktop/src/__mocks__/globalMocks.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/database.services.ts : Remove/avoid imports of legacy table modules from electron/database/database.services.ts

Applied to files:

  • apps/desktop/electron/database/tables/__test__/testUtils.ts
  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/preload/index.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : Ensure tests create required foreign key dependencies first

Applied to files:

  • apps/desktop/electron/database/tables/__test__/testUtils.ts
  • apps/desktop/src/__mocks__/TestSqlProxy.ts
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/index.ts : Update apps/desktop/src/hooks/queries/index.ts to export the new hook (export * from "./useTableName")

Applied to files:

  • apps/desktop/electron/database/tables/__test__/testUtils.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/preload/index.ts : Remove/avoid IPC handlers and calls for database access in electron/preload/index.ts

Applied to files:

  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/preload/index.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/**/*.ts : Do not add or modify legacy IPC-based table files under electron/database/tables; they are being removed

Applied to files:

  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/preload/index.ts
  • apps/desktop/electron/database/migrations/schema.ts
  • apps/desktop/electron/database/migrations/meta/0008_snapshot.json
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Public wrapper functions should keep legacy names and wrap results in DatabaseResponse via promiseToDatabaseResponse

Applied to files:

  • apps/desktop/electron/database/database.services.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Internal database functions must use a *Db suffix and return raw data types (not DatabaseResponse)

Applied to files:

  • apps/desktop/electron/database/database.services.ts
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Integrate undo/redo by calling incrementUndoGroup within each transaction

Applied to files:

  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/preload/index.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Prefer single transactions that group multi-operation DB work; use incrementUndoGroup within transactions

Applied to files:

  • apps/desktop/electron/database/database.services.ts
📚 Learning: 2025-08-11T21:28:23.647Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-08-11T21:28:23.647Z
Learning: Applies to **/test/**/*.{ts,tsx,js,jsx} : Do not use mocks for packages or database operations in tests; the only exception is mocking React context providers when rendering components that rely on hooks

Applied to files:

  • apps/desktop/src/__mocks__/TestSqlProxy.ts
  • apps/desktop/src/__mocks__/globalMocks.ts
🧬 Code graph analysis (4)
apps/desktop/electron/database/tables/__test__/testUtils.ts (2)
apps/desktop/electron/database/db.ts (2)
  • schema (11-11)
  • getOrm (7-9)
apps/desktop/electron/database/services/DrizzleMigrationService.ts (1)
  • DrizzleMigrationService (19-151)
apps/desktop/electron/database/database.services.ts (1)
apps/desktop/src/global/database/db.ts (1)
  • db (6-17)
apps/desktop/src/__mocks__/globalMocks.ts (3)
apps/desktop/src/components/canvas/__test__/MocksForCanvas.ts (1)
  • mockMarchers (31-80)
apps/desktop/src/db-functions/shapes.ts (1)
  • DatabaseShape (10-16)
apps/desktop/src/db-functions/shapePages.ts (1)
  • DatabaseShapePage (23-31)
apps/desktop/src/components/canvas/__test__/MocksForCanvas.ts (4)
apps/desktop/src/__mocks__/globalMocks.ts (2)
  • mockMarcherPages (115-233)
  • mockMarchers (13-62)
apps/desktop/src/global/classes/MarcherPageIndex.ts (1)
  • marcherPageMapFromArray (34-54)
apps/desktop/src/global/classes/MarcherVisualGroup.ts (1)
  • marcherVisualsFromMarchers (122-143)
packages/core/src/field/FieldTheme.ts (1)
  • DEFAULT_FIELD_THEME (54-54)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: desktop-e2e (prerelease/0.0.13, pull-request)
  • GitHub Check: build
  • GitHub Check: desktop-e2e (main, main)
  • GitHub Check: build-and-release (ubuntu-latest)
  • GitHub Check: build-and-release (macos-13)
🔇 Additional comments (5)
apps/desktop/src/__mocks__/globalMocks.ts (2)

260-268: Give the last mockMarcherLine a unique ID.

mockMarcherLines now has two entries with id: 1, which trips consumers relying on unique identifiers. Bump the final ID to a fresh value.

     new MarcherLine({
-        id: 1,
+        id: 3,
         startPageId: 2,
         endPageId: 4,

195-232: Ensure mockMarcherPages IDs are unique.

The final three mockMarcherPages entries reuse IDs 1–3, overwriting the earlier pages when we build the marcherPageMap and breaking PK uniqueness assumptions. Please give those records their own IDs.

     {
-        id: 1,
+        id: 7,
         marcher_id: 1,
         page_id: 3,
@@
     },
     {
-        id: 2,
+        id: 8,
         marcher_id: 2,
         page_id: 3,
@@
     },
     {
-        id: 3,
+        id: 9,
         marcher_id: 3,
         page_id: 3,
apps/desktop/src/components/canvas/__test__/MocksForCanvas.ts (1)

7-7: Avoid pulling DEFAULT_FIELD_THEME through the package entry point

Importing DEFAULT_FIELD_THEME from @openmarch/core still resolves to packages/core/dist/index.js, but the dist bundle isn’t produced as part of the test/build workflow, so this blows up in CI (same failure we already saw) and the canvas mocks can’t even load. Please keep the test fixture self-contained—e.g. provide a local mock theme or source it from a test helper that doesn’t hit the package entry point—or make sure the core package is built before tests run.

apps/desktop/electron/database/database.services.ts (1)

193-201: Close DB handles in unsafe SQL proxy

Reiterating earlier feedback: connect() opens a fresh handle every call. Without a finally that closes it, repeated uses leak native connections.

 async function handleUnsafeSqlProxy(_: any, sql: string) {
-    try {
-        return await handleUnsafeSqlProxyWithDb(connect(), sql);
+    const db = connect();
+    try {
+        return await handleUnsafeSqlProxyWithDb(db, sql);
     } catch (error: any) {
         console.error("Error from unsafe SQL proxy:", error);
         throw error;
+    } finally {
+        db.close();
     }
 }
apps/desktop/electron/preload/index.ts (1)

278-281: Guard renderer access to unsafeSqlProxy

This was previously flagged: keep the method but hard-block it unless ENABLE_UNSAFE_SQL_PROXY is explicitly enabled, matching the main-process gate. Otherwise any renderer code (including plugins) can run arbitrary SQL.

-    /** Only needed for the triggers */
-    unsafeSqlProxy: (sql: string) =>
-        ipcRenderer.invoke("unsafeSql:proxy", sql) as Promise<{
-            rows: any[] | any;
-        }>,
+    /** Only needed for the triggers */
+    unsafeSqlProxy: (sql: string) => {
+        if (process.env.ENABLE_UNSAFE_SQL_PROXY !== "1") {
+            throw new Error("unsafeSqlProxy is disabled");
+        }
+        return ipcRenderer.invoke("unsafeSql:proxy", sql) as Promise<{
+            rows: any[] | any;
+        }>;
+    },

Comment on lines +156 to +158
async function handleUnsafeSqlProxyWithDb(db: Database.Database, sql: string) {
return await db.exec(sql);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix unsafe SQL proxy return payload

better-sqlite3#exec returns the Database instance, which can’t be cloned across the IPC boundary. Returning that object breaks the renderer call (and violates the { rows: … } contract we advertise downstream). Execute the SQL and respond with a serializable payload instead.

-async function handleUnsafeSqlProxyWithDb(db: Database.Database, sql: string) {
-    return await db.exec(sql);
-}
+async function handleUnsafeSqlProxyWithDb(
+    db: Database.Database,
+    sql: string,
+) {
+    db.exec(sql);
+    return { rows: [] as any[] };
+}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/desktop/electron/database/database.services.ts around lines 156 to 158,
the function currently returns the better-sqlite3 Database instance (from
db.exec), which is non-serializable; change the handler to execute the SQL and
return a plain serializable payload: trim and inspect the SQL (e.g., startsWith
SELECT) and for queries use db.prepare(sql).all() and return { rows: [...] },
for non-query statements use db.prepare(sql).run() and return { changes:
<number>, lastInsertRowid: <number> } (or similar serializable fields) instead
of returning the Database object.

Comment on lines +213 to 214
ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy);

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Gate unsafe SQL IPC registration

We shouldn’t register the unsafeSql:proxy channel in production builds. If ENABLE_UNSAFE_SQL_PROXY isn’t set, skip the handler so nothing (malicious or accidental) can invoke it.

 export function initHandlers() {
     // Generic SQL proxy handler for Drizzle ORM
     ipcMain.handle("sql:proxy", handleSqlProxy);
-    ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy);
+    if (process.env.ENABLE_UNSAFE_SQL_PROXY === "1") {
+        ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy);
+    }
🤖 Prompt for AI Agents
In apps/desktop/electron/database/database.services.ts around lines 213-214, the
IPC handler for "unsafeSql:proxy" is always registered; modify the code to only
call ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy) when the
environment variable ENABLE_UNSAFE_SQL_PROXY is set/truthy (e.g.,
process.env.ENABLE_UNSAFE_SQL_PROXY === '1' or 'true') and the build is not
production if desired. Add a simple guard that checks the env var before
registering the handler and ensure no handler is registered when the flag is
absent, so the channel remains inaccessible in production builds.

Comment on lines +53 to +59
// create 16 beats
const beatValues = Array.from({ length: 16 }, (_, i) => ({
duration: 0.5,
position: i + 1,
}));
void orm.insert(schema.beats).values(beatValues).run();
return orm;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Await the seed insert to keep async drivers in sync.

DbConnection is typed as "async" and will back the sql.js proxy path you just introduced. With that driver, .run() returns a promise; dropping the result with void means initTestDatabaseOrm resolves before the beats are actually inserted, so downstream tests can race on missing seed data. Please await the insert so both async and sync drivers behave consistently.

-    void orm.insert(schema.beats).values(beatValues).run();
+    await orm.insert(schema.beats).values(beatValues).run();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// create 16 beats
const beatValues = Array.from({ length: 16 }, (_, i) => ({
duration: 0.5,
position: i + 1,
}));
void orm.insert(schema.beats).values(beatValues).run();
return orm;
// create 16 beats
const beatValues = Array.from({ length: 16 }, (_, i) => ({
duration: 0.5,
position: i + 1,
}));
await orm.insert(schema.beats).values(beatValues).run();
return orm;
🤖 Prompt for AI Agents
In apps/desktop/electron/database/tables/__test__/testUtils.ts around lines 53
to 59, the seed insert uses void orm.insert(...).run() which drops the returned
promise and lets initTestDatabaseOrm resolve before the beats are actually
inserted; change this to await the insert call (await
orm.insert(schema.beats).values(beatValues).run()) so the function waits for the
async insert to complete and tests using async drivers (sql.js proxy) won't race
on missing seed data.

Copy link
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: 4

🧹 Nitpick comments (1)
apps/desktop/e2e/fixtures.mts (1)

17-34: Consider renaming to avoid shadowing.

The new top-level dbPath constant now shares a name with the existing const dbPath = … inside loadSqlIntoDatabase, which can be a little confusing when scanning the file. Renaming the top-level one to something like blankDatabaseRelativePath would keep intent obvious without changing behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 56008e9 and 34a50ae.

📒 Files selected for processing (15)
  • .cursor/rules/basic-database-interaction.mdc (1 hunks)
  • .cursor/rules/component-database-interaction.mdc (1 hunks)
  • .eslintrc.cjs (3 hunks)
  • .github/workflows/test.yaml (1 hunks)
  • .vscode/launch.json (1 hunks)
  • .zed/.rules (3 hunks)
  • apps/desktop/e2e/README.md (1 hunks)
  • apps/desktop/e2e/_codegen.mjs (1 hunks)
  • apps/desktop/e2e/fixtures.mts (2 hunks)
  • apps/desktop/electron/database/DatabaseActions.ts (0 hunks)
  • apps/desktop/electron/database/__test__/DatabaseActions.test.ts (0 hunks)
  • apps/desktop/electron/database/migrations/0009_left_bullseye.sql (1 hunks)
  • apps/desktop/electron/database/migrations/meta/0009_snapshot.json (1 hunks)
  • apps/desktop/electron/database/migrations/meta/_journal.json (1 hunks)
  • apps/desktop/electron/database/migrations/schema.ts (11 hunks)
💤 Files with no reviewable changes (2)
  • apps/desktop/electron/database/DatabaseActions.ts
  • apps/desktop/electron/database/test/DatabaseActions.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .vscode/launch.json
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/repo.mdc)

Use zustand for state management in the application

Files:

  • apps/desktop/electron/database/migrations/schema.ts
apps/desktop/electron/**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Use absolute paths for file operations; avoid relative file paths

Files:

  • apps/desktop/electron/database/migrations/schema.ts
🧠 Learnings (22)
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/__test__/*.test.ts : Remove legacy tests under electron/database/tables/__test__; merge into classes tests

Applied to files:

  • apps/desktop/e2e/_codegen.mjs
  • apps/desktop/e2e/fixtures.mts
  • apps/desktop/electron/database/migrations/schema.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/database.services.ts : Remove/avoid imports of legacy table modules from electron/database/database.services.ts

Applied to files:

  • apps/desktop/e2e/_codegen.mjs
📚 Learning: 2025-08-11T21:28:23.647Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-08-11T21:28:23.647Z
Learning: Applies to **/test/**/*.{ts,tsx,js,jsx} : Do not use mocks for packages or database operations in tests; the only exception is mocking React context providers when rendering components that rely on hooks

Applied to files:

  • .eslintrc.cjs
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to **/*.test.ts : Test files must use the .test.ts suffix

Applied to files:

  • .eslintrc.cjs
📚 Learning: 2025-08-11T21:28:23.647Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-08-11T21:28:23.647Z
Learning: Applies to **/test/**/*.{ts,tsx,js,jsx} : Place tests in a test folder within the same directory as the file being tested

Applied to files:

  • .eslintrc.cjs
📚 Learning: 2025-08-11T21:28:23.647Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-08-11T21:28:23.647Z
Learning: Applies to **/test/**/*.{ts,tsx,js,jsx} : Use Vitest to write all unit tests

Applied to files:

  • .eslintrc.cjs
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to **/*.Table.ts : Do not create traditional *.Table.ts files

Applied to files:

  • .eslintrc.cjs
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Implement smart cache invalidation: invalidate affected detail queries and list queries after mutations

Applied to files:

  • .zed/.rules
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Ensure full TypeScript type safety, including exported Database[TableName], New[TableName]Args, and Modified[TableName]Args types

Applied to files:

  • .zed/.rules
  • .cursor/rules/basic-database-interaction.mdc
  • .cursor/rules/component-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : Use setupTestSqlProxy() in beforeEach for DB-related tests

Applied to files:

  • apps/desktop/e2e/README.md
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Always use React Query hooks for all new database table interactions instead of traditional table files

Applied to files:

  • .cursor/rules/basic-database-interaction.mdc
  • .cursor/rules/component-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Follow naming conventions in hook files: file use[TableName].ts; hooks use[TableName], use[TableName]ById, useCreate[TableName], etc.; query keys [tableName] (camelCase); types Database[TableName], New[TableName]Args, Modified[TableName]Args

Applied to files:

  • .cursor/rules/basic-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Wrap all mutations in database transactions

Applied to files:

  • .cursor/rules/basic-database-interaction.mdc
  • .cursor/rules/component-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Create new query hook files at apps/desktop/src/hooks/queries/use[TableName].ts for each table

Applied to files:

  • .cursor/rules/basic-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Always export hooks: use[TableName], use[TableName]ById, useCreate[TableName], useUpdate[TableName], useDelete[TableName]

Applied to files:

  • .cursor/rules/basic-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/index.ts : Update apps/desktop/src/hooks/queries/index.ts to export the new hook (export * from "./useTableName")

Applied to files:

  • .cursor/rules/basic-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Leverage React Query error and loading states; do not implement manual useState for loading/error management

Applied to files:

  • .cursor/rules/basic-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/**/*.ts : Do not add or modify legacy IPC-based table files under electron/database/tables; they are being removed

Applied to files:

  • apps/desktop/e2e/fixtures.mts
  • apps/desktop/electron/database/migrations/schema.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Prefer single transactions that group multi-operation DB work; use incrementUndoGroup within transactions

Applied to files:

  • .cursor/rules/component-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Integrate undo/redo by calling incrementUndoGroup within each transaction

Applied to files:

  • .cursor/rules/component-database-interaction.mdc
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Consolidate each entity’s database logic into a single file src/global/classes/[Entity].ts (shared SQL bridge)

Applied to files:

  • apps/desktop/electron/database/migrations/schema.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Use Drizzle-inferred types for DB entities (e.g., typeof table.$inferSelect) and avoid manually duplicating schema interfaces

Applied to files:

  • apps/desktop/electron/database/migrations/schema.ts
🧬 Code graph analysis (1)
apps/desktop/e2e/fixtures.mts (1)
apps/desktop/e2e/_codegen.mjs (2)
  • dbPath (79-79)
  • __dirname (10-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build-and-release (macos-13)
  • GitHub Check: build-and-release (ubuntu-latest)
  • GitHub Check: build-and-release (macos-latest)
  • GitHub Check: desktop-e2e (prerelease/0.0.13, pull-request)
  • GitHub Check: build
🔇 Additional comments (6)
apps/desktop/e2e/README.md (1)

26-26: Fixture reference update looks good.
Renaming the base fixture to _blank.dots keeps the README aligned with the current setup script.

.zed/.rules (1)

170-174: Void prefix silences promise lint cleanly.

Nice touch adding void before each invalidateQueries call; it keeps the callbacks fire-and-forget while satisfying the no-floating-promises rule without altering behavior.

Also applies to: 206-214, 239-247

apps/desktop/e2e/_codegen.mjs (1)

79-84: Scoped path constant looks good.

Centralizing the relative _blank.dots path into dbPath makes the error message clearer without changing behavior. 👍

.eslintrc.cjs (1)

3-8: Install the TanStack Query ESLint plugin wherever linting runs.

Extending plugin:@tanstack/query/recommended requires @tanstack/eslint-plugin-query to be present; ESLint throws “Failed to load config” if the package is missing. Please add it (e.g., as a devDependency in the root/workspaces that lint) or confirm it’s already installed. Docs explicitly call for installing the plugin before using this preset.

#!/bin/bash
# Verify every package.json that runs ESLint declares the plugin dependency.
set -euo pipefail

echo "Packages missing @tanstack/eslint-plugin-query:"
fd -H package.json | while read -r pkg; do
  found=$(jq -r 'paths | select(length==2 and (.[0]=="devDependencies" or .[0]=="dependencies") and .[-1]=="@tanstack/eslint-plugin-query") | empty' "$pkg")
  if [[ -z "$found" ]]; then
    echo " - $pkg"
  fi
done
.cursor/rules/basic-database-interaction.mdc (2)

197-205: Remove .get() after .returning() in Drizzle SQLite.

Drizzle’s .returning() already yields an array of rows; .get() does not exist here and following this template will throw at runtime. Destructure the array (e.g., const [updatedItem] = …returning();) before pushing into updatedItems.


348-356: Don’t pass an array of keys to queryKey in invalidateQueries.

invalidateQueries expects a single query key. This template hands it an array of query keys (Array.from(itemIds).map(...)), which React Query v5 rejects. Loop the IDs and invalidate each key individually (or use a predicate).

Comment on lines +268 to +343
import {
DbConnection,
Database{tableName},
create{tableName}s,
get{tableName}s,
get{tableName}ById,
realDatabase{tableName}ToDatabase{tableName},
delete{tableName}s,
update{tableName}sInTransaction,
} from "@/db-functions";

const { table_name } = schema;

const KEY_BASE = "{table_name}";

// Query key factory
export const {tableName}Keys = {
all: () => [KEY_BASE] as const,
byId: (id: number) => [KEY_BASE, "id", id] as const,
// Add other specific keys as needed
};

const {tableName}Queries = {
getAll: async (db: DbConnection): Promise<Database{tableName}[]> => {
return await get{tableName}s({ db });
},
getById: async (
db: DbConnection,
id: number,
): Promise<Database{tableName} | undefined> => {
return await get{tableName}ById({ db, id });
},
};

/**
* Query options for getting all items
*/
export const all{tableName}sQueryOptions = () => {
return queryOptions<Database{tableName}[]>({
queryKey: {tableName}Keys.all(),
queryFn: async () => {
return await {tableName}Queries.getAll(db);
},
});
};

export const {tableName}QueryByIdOptions = (id: number) => {
return queryOptions<Database{tableName} | undefined>({
queryKey: {tableName}Keys.byId(id),
queryFn: async () => {
return await {tableName}Queries.getById(db, id);
},
});
};

export const create{tableName}sMutationOptions = (qc: QueryClient) => {
return mutationOptions({
mutationFn: (newItems: New{tableName}Args[]) =>
create{tableName}s({ db, newItems }),
onSuccess: (_, variables) => {
// Invalidate all queries
void qc.invalidateQueries({
queryKey: [KEY_BASE],
});
},
onError: (e, variables) => {
conToastError(`Error creating items`, e, variables);
},
});
};

export const update{tableName}sMutationOptions = (qc: QueryClient) => {
return mutationOptions({
mutationFn: (modifiedItems: Modified{tableName}Args[]) =>
update{tableName}s({ db, modifiedItems }),
onSuccess: (_, variables) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Import the function you call in the mutation template.

The hooks template calls update{tableName}s({ db, modifiedItems }), but the import list omits update{tableName}s. Anyone following this pattern will hit a TS compile error. Add update{tableName}s to the imports.

🤖 Prompt for AI Agents
In .cursor/rules/basic-database-interaction.mdc around lines 268 to 343 the
template calls update{tableName}s(...) in the mutation but that function is
missing from the import list at the top; add update{tableName}s to the imported
identifiers from "@/db-functions" so the mutation call compiles, keeping the
same import ordering/style as the surrounding imports.

Comment on lines +96 to +113
mutationFn,
onSuccess: () => {
// Invalidate all of the relevant queries
// Always invalidate beats first and wait for them to finish. This is important because measures rely on beats
await queryClient.invalidateQueries({
queryKey: beatKeys.all(),
});
void queryClient.invalidateQueries({
queryKey: measureKeys.all(),
});
// ... more queries if needed

if (successKey) toast.success(tolgee.t(successKey));
},
onError: (error) => {
conToastError(tolgee.t(errorKey), error);
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make the onSuccess handler async before using await.

The example calls await queryClient.invalidateQueries(...) inside onSuccess, but the handler isn’t marked async, so the snippet is invalid as written. Update the template to onSuccess: async () => { … }.

🤖 Prompt for AI Agents
In .cursor/rules/component-database-interaction.mdc around lines 96 to 113 the
onSuccess handler uses await but isn’t declared async; change the handler
signature to onSuccess: async () => { … } so await is valid, leaving the rest of
the logic (invalidateQueries calls and toast) unchanged.

Comment on lines +10 to +17
strategy:
matrix:
include:
- branch: main
artifact: main
- branch: ${{ github.head_ref }}
artifact: pull-request

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid redundant matrix entries
The matrix defines branch/artifact, but none of the steps consume those values. Each matrix run just checks out the default PR merge commit, so the workflow executes twice with identical inputs, doubling runtime without adding coverage. Either drop the matrix or wire matrix.branch into the checkout (and any other intended branch-specific logic).

🤖 Prompt for AI Agents
.github/workflows/test.yaml lines 10-17: The matrix currently includes
branch/artifact pairs that are never used, causing duplicate identical runs;
either remove the entire strategy.matrix block to run once, or wire the matrix
values into downstream steps — update actions/checkout to use ref: ${{
matrix.branch }} (or otherwise pass matrix.branch into any branch-specific
steps) and reference matrix.artifact where artifacts are created/uploaded so
each matrix entry produces distinct behavior.

Comment on lines +14 to +22
const timestamps = {
created_at: text()
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
updated_at: text()
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`)
.$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Turn the shared timestamps helper into a factory before reuse

timestamps currently holds live column-builder instances. Drizzle’s builders are single-use; spreading the same builder across multiple sqliteTable calls trips Drizzle’s guard (ColumnBuilder was already used) and prevents the module from loading. Convert the helper into a function that returns fresh builders for each table, and call it where needed.

-const timestamps = {
-    created_at: text()
-        .notNull()
-        .default(sql`(CURRENT_TIMESTAMP)`),
-    updated_at: text()
-        .notNull()
-        .default(sql`(CURRENT_TIMESTAMP)`)
-        .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
-};
+const timestamps = () => ({
+    created_at: text()
+        .notNull()
+        .default(sql`(CURRENT_TIMESTAMP)`),
+    updated_at: text()
+        .notNull()
+        .default(sql`(CURRENT_TIMESTAMP)`)
+        .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
+});

Then spread ...timestamps() in each table definition.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const timestamps = {
created_at: text()
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
updated_at: text()
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`)
.$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
};
const timestamps = () => ({
created_at: text()
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
updated_at: text()
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`)
.$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
});
🤖 Prompt for AI Agents
In apps/desktop/electron/database/migrations/schema.ts around lines 14 to 22,
the shared timestamps object currently holds live Drizzle column-builder
instances which are single-use and cause "ColumnBuilder was already used" when
reused; convert that const into a factory function (e.g., function timestamps()
{ return { created_at: text().notNull().default(sql`(CURRENT_TIMESTAMP)`),
updated_at: text().notNull().default(sql`(CURRENT_TIMESTAMP)`).$onUpdate(() =>
sql`(CURRENT_TIMESTAMP)`) }; }) so each call returns fresh builders, and update
all table definitions to spread the result with ...timestamps() instead of
spreading the original object.

Copy link
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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/electron/database/migrations/schema.ts (1)

58-77: Add unique constraint for beats.position to match intent.

Comment says “Integer and unique,” but constraint is missing. Add a unique index.

 export const beats = sqliteTable(
   "beats",
   {
     id: integer().primaryKey(),
     /** Duration from this beat to the next in second. */
     duration: real().notNull(),
     /** The position of this beat in the show. Integer and unique */
     position: integer().notNull(),
     /** Whether this beat is included in a measure. 0 = false, 1 = true. */
     include_in_measure: integer().default(1).notNull(),
     /** Human readable notes. */
     notes: text(),
-    ...timestamps,
+    ...timestamps(),
   },
-  (_table) => [
+  (table) => [
     check("beats_duration_check", sql`duration >= 0`),
     check("beats_position_check", sql`position >= 0`),
     check("beats_include_in_measure", sql`include_in_measure IN (0, 1)`),
+    unique().on(table.position),
   ],
 );
🧹 Nitpick comments (7)
apps/desktop/electron/database/services/sqljs-driver.ts (2)

14-16: Prefer the public SQLJsDatabase type export to avoid mismatches.

Self-typing BaseSQLiteDatabase<"sync", void, TSchema> can diverge from Drizzle’s actual RunResult type. Import/re-export SQLJsDatabase from drizzle-orm/sql-js instead.


32-43: Avoid duplicating relational schema extraction.

Manually calling extractTablesRelationalConfig mirrors Drizzle internals and can drift. The public drizzle already derives this from config.schema. Drop this block if you switch to the public adapter.

apps/desktop/electron/database/services/DrizzleMigrationService.ts (2)

107-134: Use absolute paths for file IO (packaging-safe).

hasPendingMigrations defaults to a relative path. Resolve from __dirname to comply with the guideline and avoid CWD issues.

As per coding guidelines.

-  const folder = migrationsFolder || "./electron/database/migrations";
+  const folder =
+    migrationsFolder ||
+    path.resolve(__dirname, "../migrations");

148-149: Make trigger creation idempotent or drop-before-create.

createAllTriggers will fail on existing triggers unless the SQL uses IF NOT EXISTS. Either prepend a dropAllTriggers call here or update trigger DDL to be idempotent.

apps/desktop/electron/database/migrations/schema.ts (2)

24-33: Make browserSafeBinaryBlob null- and Buffer-safe.

Handle nulls and Buffer/ArrayBuffer inputs to avoid runtime errors when the column is nullable.

-const browserSafeBinaryBlob = customType<{
-    data: Uint8Array;
-    driverData: Uint8Array;
-}>({
+const browserSafeBinaryBlob = customType<{
+  data: Uint8Array | null;
+  driverData: Uint8Array | null;
+}>({
   dataType: () => "blob",
-  fromDriver: (value) => new Uint8Array(value),
-  toDriver: (value) => value,
+  fromDriver: (value) =>
+    value == null
+      ? null
+      : value instanceof Uint8Array
+      ? value
+      : new Uint8Array(value as ArrayBuffer | Buffer),
+  toDriver: (value) => value,
 });

240-247: Use browserSafeBinaryBlob for audio_files.data for web portability.

blob() infers Buffer in Node; switch to the Uint8Array-backed custom type (as with field_properties.image) to avoid type divergence in the renderer/SQL.js.

 export const audio_files = sqliteTable("audio_files", {
   id: integer().primaryKey(),
   path: text().notNull(),
   nickname: text(),
-  data: blob(),
+  data: browserSafeBinaryBlob(),
   selected: integer().default(0).notNull(),
-  ...timestamps,
+  ...timestamps(),
 });
apps/desktop/electron/database/migrations/0007_misty_zemo.sql (1)

16-31: Add unique index for beats.position here (optional if added in a later migration).

To enforce the intended uniqueness:

-- After renaming __new_beats to beats:
CREATE UNIQUE INDEX IF NOT EXISTS beats_position_unique ON beats (position);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b772f8b and 7a42b0e.

⛔ Files ignored due to path filters (2)
  • packages/path-utility/example/public/vite.svg is excluded by !**/*.svg
  • packages/path-utility/example/src/assets/react.svg is excluded by !**/*.svg
📒 Files selected for processing (46)
  • .cursor/rules/basic-database-interaction.mdc (1 hunks)
  • .cursor/rules/command-syntax.mdc (1 hunks)
  • .cursor/rules/component-database-interaction.mdc (1 hunks)
  • .cursor/rules/database-interaction.mdc (0 hunks)
  • .cursor/rules/multi-query-hooks.mdc (1 hunks)
  • .cursor/rules/unit-testing.mdc (1 hunks)
  • .eslintrc.cjs (3 hunks)
  • .github/workflows/test.yaml (1 hunks)
  • .gitignore (1 hunks)
  • .vscode/launch.json (1 hunks)
  • .zed/.rules (3 hunks)
  • CLAUDE.md (0 hunks)
  • apps/desktop/e2e/README.md (1 hunks)
  • apps/desktop/e2e/_codegen.mjs (1 hunks)
  • apps/desktop/e2e/fixtures.mts (2 hunks)
  • apps/desktop/electron/database/DatabaseActions.ts (0 hunks)
  • apps/desktop/electron/database/__test__/DatabaseActions.test.ts (0 hunks)
  • apps/desktop/electron/database/__test__/DatabaseMocks.ts (1 hunks)
  • apps/desktop/electron/database/__test__/database.history.test.ts (0 hunks)
  • apps/desktop/electron/database/constants.ts (0 hunks)
  • apps/desktop/electron/database/database.services.ts (5 hunks)
  • apps/desktop/electron/database/migrations/0000_productive_madripoor.sql (1 hunks)
  • apps/desktop/electron/database/migrations/0006_bitter_proemial_gods.sql (1 hunks)
  • apps/desktop/electron/database/migrations/0007_misty_zemo.sql (1 hunks)
  • apps/desktop/electron/database/migrations/0008_round_mathemanic.sql (1 hunks)
  • apps/desktop/electron/database/migrations/0009_left_bullseye.sql (1 hunks)
  • apps/desktop/electron/database/migrations/meta/0006_snapshot.json (1 hunks)
  • apps/desktop/electron/database/migrations/meta/0007_snapshot.json (1 hunks)
  • apps/desktop/electron/database/migrations/meta/0008_snapshot.json (1 hunks)
  • apps/desktop/electron/database/migrations/meta/0009_snapshot.json (1 hunks)
  • apps/desktop/electron/database/migrations/meta/_journal.json (1 hunks)
  • apps/desktop/electron/database/migrations/schema.ts (11 hunks)
  • apps/desktop/electron/database/migrations/triggers.ts (3 hunks)
  • apps/desktop/electron/database/services/DrizzleMigrationService.ts (3 hunks)
  • apps/desktop/electron/database/services/sqljs-driver.ts (1 hunks)
  • apps/desktop/electron/database/tables/BeatTable.ts (0 hunks)
  • apps/desktop/electron/database/tables/MarcherPageTable.ts (0 hunks)
  • apps/desktop/electron/database/tables/MarcherTable.ts (0 hunks)
  • apps/desktop/electron/database/tables/PageTable.ts (0 hunks)
  • apps/desktop/electron/database/tables/ShapePageMarcherTable.ts (0 hunks)
  • apps/desktop/electron/database/tables/ShapePageTable.ts (0 hunks)
  • apps/desktop/electron/database/tables/ShapeTable.ts (0 hunks)
  • apps/desktop/electron/database/tables/UtilityTable.ts (0 hunks)
  • apps/desktop/electron/database/tables/__test__/BeatTable.test.ts (0 hunks)
  • apps/desktop/electron/database/tables/__test__/MarcherPageTable.test.ts (0 hunks)
  • apps/desktop/electron/database/tables/__test__/MarcherTable.test.ts (0 hunks)
💤 Files with no reviewable changes (17)
  • apps/desktop/electron/database/tables/PageTable.ts
  • apps/desktop/electron/database/tables/test/MarcherTable.test.ts
  • apps/desktop/electron/database/DatabaseActions.ts
  • apps/desktop/electron/database/test/database.history.test.ts
  • apps/desktop/electron/database/constants.ts
  • CLAUDE.md
  • apps/desktop/electron/database/tables/ShapeTable.ts
  • apps/desktop/electron/database/tables/MarcherPageTable.ts
  • apps/desktop/electron/database/test/DatabaseActions.test.ts
  • apps/desktop/electron/database/tables/test/MarcherPageTable.test.ts
  • .cursor/rules/database-interaction.mdc
  • apps/desktop/electron/database/tables/UtilityTable.ts
  • apps/desktop/electron/database/tables/BeatTable.ts
  • apps/desktop/electron/database/tables/MarcherTable.ts
  • apps/desktop/electron/database/tables/ShapePageTable.ts
  • apps/desktop/electron/database/tables/ShapePageMarcherTable.ts
  • apps/desktop/electron/database/tables/test/BeatTable.test.ts
✅ Files skipped from review due to trivial changes (1)
  • .cursor/rules/component-database-interaction.mdc
🚧 Files skipped from review as they are similar to previous changes (13)
  • apps/desktop/e2e/fixtures.mts
  • .cursor/rules/unit-testing.mdc
  • apps/desktop/electron/database/migrations/meta/0006_snapshot.json
  • .vscode/launch.json
  • .zed/.rules
  • .github/workflows/test.yaml
  • apps/desktop/electron/database/migrations/meta/0008_snapshot.json
  • apps/desktop/electron/database/migrations/0000_productive_madripoor.sql
  • .cursor/rules/multi-query-hooks.mdc
  • apps/desktop/electron/database/migrations/meta/_journal.json
  • apps/desktop/electron/database/test/DatabaseMocks.ts
  • .cursor/rules/basic-database-interaction.mdc
  • apps/desktop/e2e/_codegen.mjs
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/repo.mdc)

Use zustand for state management in the application

Files:

  • apps/desktop/electron/database/services/sqljs-driver.ts
  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/database/migrations/triggers.ts
  • apps/desktop/electron/database/migrations/schema.ts
  • apps/desktop/electron/database/services/DrizzleMigrationService.ts
apps/desktop/electron/**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Use absolute paths for file operations; avoid relative file paths

Files:

  • apps/desktop/electron/database/services/sqljs-driver.ts
  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/database/migrations/triggers.ts
  • apps/desktop/electron/database/migrations/schema.ts
  • apps/desktop/electron/database/services/DrizzleMigrationService.ts
apps/desktop/electron/database/database.services.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Remove/avoid imports of legacy table modules from electron/database/database.services.ts

Files:

  • apps/desktop/electron/database/database.services.ts
🧠 Learnings (22)
📓 Common learnings
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/__test__/*.test.ts : Remove legacy tests under electron/database/tables/__test__; merge into classes tests
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Always use React Query hooks for all new database table interactions instead of traditional table files
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : Use setupTestSqlProxy() in beforeEach for DB-related tests

Applied to files:

  • apps/desktop/e2e/README.md
  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/database/migrations/triggers.ts
  • apps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Use Drizzle-inferred types for DB entities (e.g., typeof table.$inferSelect) and avoid manually duplicating schema interfaces

Applied to files:

  • apps/desktop/electron/database/services/sqljs-driver.ts
  • apps/desktop/electron/database/migrations/schema.ts
  • apps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Consolidate each entity’s database logic into a single file src/global/classes/[Entity].ts (shared SQL bridge)

Applied to files:

  • apps/desktop/electron/database/services/sqljs-driver.ts
  • apps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/__test__/*.test.ts : Remove legacy tests under electron/database/tables/__test__; merge into classes tests

Applied to files:

  • apps/desktop/electron/database/services/sqljs-driver.ts
  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/database/migrations/triggers.ts
  • apps/desktop/electron/database/migrations/schema.ts
  • apps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Run pnpm tsc --noEmit for type checking and pnpm lint for linting in the desktop app

Applied to files:

  • apps/desktop/electron/database/services/sqljs-driver.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/database.services.ts : Remove/avoid imports of legacy table modules from electron/database/database.services.ts

Applied to files:

  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/preload/index.ts : Remove/avoid IPC handlers and calls for database access in electron/preload/index.ts

Applied to files:

  • apps/desktop/electron/database/database.services.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/electron/database/tables/**/*.ts : Do not add or modify legacy IPC-based table files under electron/database/tables; they are being removed

Applied to files:

  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/database/migrations/schema.ts
  • apps/desktop/electron/database/services/DrizzleMigrationService.ts
  • apps/desktop/electron/database/migrations/meta/0007_snapshot.json
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Ensure full TypeScript type safety, including exported Database[TableName], New[TableName]Args, and Modified[TableName]Args types

Applied to files:

  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/database/migrations/triggers.ts
  • apps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Public wrapper functions should keep legacy names and wrap results in DatabaseResponse via promiseToDatabaseResponse

Applied to files:

  • apps/desktop/electron/database/database.services.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Internal database functions must use a *Db suffix and return raw data types (not DatabaseResponse)

Applied to files:

  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Integrate undo/redo by calling incrementUndoGroup within each transaction

Applied to files:

  • apps/desktop/electron/database/database.services.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Prefer single transactions that group multi-operation DB work; use incrementUndoGroup within transactions

Applied to files:

  • apps/desktop/electron/database/database.services.ts
  • apps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-11T21:28:23.647Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-08-11T21:28:23.647Z
Learning: Applies to **/test/**/*.{ts,tsx,js,jsx} : Do not use mocks for packages or database operations in tests; the only exception is mocking React context providers when rendering components that rely on hooks

Applied to files:

  • .eslintrc.cjs
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to **/*.test.ts : Test files must use the .test.ts suffix

Applied to files:

  • .eslintrc.cjs
📚 Learning: 2025-08-11T21:28:23.647Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-08-11T21:28:23.647Z
Learning: Applies to **/test/**/*.{ts,tsx,js,jsx} : Place tests in a test folder within the same directory as the file being tested

Applied to files:

  • .eslintrc.cjs
📚 Learning: 2025-08-11T21:28:23.647Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/unit-testing.mdc:0-0
Timestamp: 2025-08-11T21:28:23.647Z
Learning: Applies to **/test/**/*.{ts,tsx,js,jsx} : Use Vitest to write all unit tests

Applied to files:

  • .eslintrc.cjs
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to **/*.Table.ts : Do not create traditional *.Table.ts files

Applied to files:

  • .eslintrc.cjs
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : In entity tests, call internal *Db functions for raw results and public wrappers for DatabaseResponse

Applied to files:

  • apps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:06.043Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/__test__/*.test.ts : Consolidate tests for migrated entities under src/global/classes/__test__/[Entity].test.ts

Applied to files:

  • apps/desktop/electron/database/services/DrizzleMigrationService.ts
📚 Learning: 2025-08-17T02:33:33.561Z
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Wrap all mutations in database transactions

Applied to files:

  • apps/desktop/electron/database/services/DrizzleMigrationService.ts
🧬 Code graph analysis (3)
apps/desktop/electron/database/database.services.ts (1)
apps/desktop/src/global/database/db.ts (1)
  • db (6-17)
apps/desktop/electron/database/migrations/triggers.ts (2)
apps/desktop/electron/database/tables/__test__/testUtils.ts (1)
  • DbConnection (8-12)
apps/desktop/electron/database/services/DrizzleMigrationService.ts (1)
  • DB (13-13)
apps/desktop/electron/database/services/DrizzleMigrationService.ts (3)
apps/desktop/electron/database/db.ts (2)
  • DB (5-5)
  • schema (11-11)
apps/desktop/electron/database/tables/__test__/testUtils.ts (1)
  • DbConnection (8-12)
apps/desktop/electron/database/migrations/triggers.ts (2)
  • dropAllTriggers (60-69)
  • createAllTriggers (75-84)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: test (prerelease/0.0.13, pull-request)
  • GitHub Check: test (main, main)
  • GitHub Check: build-and-release (macos-13)
  • GitHub Check: build-and-release (ubuntu-latest)
  • GitHub Check: build
  • GitHub Check: desktop-e2e (prerelease/0.0.13, pull-request)
  • GitHub Check: desktop-e2e (main, main)
🔇 Additional comments (14)
apps/desktop/e2e/README.md (1)

26-26: Fixture name correction looks good

Updating the docs to point at _blank.dots keeps the instructions aligned with the current fixture naming—thanks for catching it.

.gitignore (1)

48-50: Do not ignore the entire metronome package.

We still have tracked sources under packages/metronome, so this glob will hide real code and block future changes from being committed. Please drop it or replace it with a root-anchored ignore that targets only generated artifacts.

.eslintrc.cjs (1)

3-8: Install @tanstack/eslint-plugin-query where ESLint runs.

Extending plugin:@tanstack/query/recommended will error unless @tanstack/eslint-plugin-query is listed in the workspaces that run ESLint (root or per-package). The last repo scan still showed it missing in several package.json files, so lint will break. Please add the dependency and re-run installs, or confirm it’s already in place.

Run this script to re-check the tree:

#!/bin/bash
set -euo pipefail

echo "Packages missing @tanstack/eslint-plugin-query:"
fd -a package.json | while read -r f; do
  has=$(jq -r '(.devDependencies["@tanstack/eslint-plugin-query"] // .dependencies["@tanstack/eslint-plugin-query"]) // empty' "$f")
  if [[ -z "$has" ]]; then
    echo " - $f"
  fi
done
apps/desktop/electron/database/services/sqljs-driver.ts (1)

25-30: Type the logger and narrow correctly (fixes strict TS build).

let logger; is implicit any under noImplicitAny, and config.logger !== false assigns undefined. Type it and guard on object.

-import { DefaultLogger } from "drizzle-orm/logger";
+import { DefaultLogger, type Logger } from "drizzle-orm/logger";
@@
-    let logger;
+    let logger: Logger | undefined;
     if (config.logger === true) {
         logger = new DefaultLogger();
-    } else if (config.logger !== false) {
-        logger = config.logger;
+    } else if (typeof config.logger === "object") {
+        logger = config.logger;
     }
apps/desktop/electron/database/database.services.ts (3)

156-158: Return a serializable payload from the unsafe proxy.

better-sqlite3#exec returns the Database object, which cannot cross the IPC boundary and breaks the renderer contract. Execute the SQL and return data/plain objects instead of the Database instance.

Apply this diff:

 async function handleUnsafeSqlProxyWithDb(
     db: Database.Database,
     sql: string,
 ) {
-    return await db.exec(sql);
+    const trimmed = sql.trim();
+    if (/^select/i.test(trimmed)) {
+        const rows = db.prepare(sql).all();
+        return { rows };
+    }
+    const result = db.prepare(sql).run();
+    return {
+        rows: [],
+        changes: result.changes,
+        lastInsertRowid:
+            typeof result.lastInsertRowid === "bigint"
+                ? Number(result.lastInsertRowid)
+                : result.lastInsertRowid,
+    };
 }

194-201: Close the temporary DB connection in handleUnsafeSqlProxy.

Each invocation opens a new better-sqlite3 connection and never closes it, leaking handles and eventually exhausting resources. Reuse the connection or at least close it in a finally.

Apply this diff:

 async function handleUnsafeSqlProxy(_: any, sql: string) {
-    try {
-        return await handleUnsafeSqlProxyWithDb(connect(), sql);
+    const db = connect();
+    try {
+        return await handleUnsafeSqlProxyWithDb(db, sql);
     } catch (error: any) {
         console.error("Error from unsafe SQL proxy:", error);
         throw error;
+    } finally {
+        db.close();
     }
 }

211-214: Guard unsafe SQL IPC registration behind an env flag.

The unsafeSql:proxy channel should only exist when explicitly enabled (e.g., for local debugging). Registering it unconditionally exposes a production attack surface.

Apply this diff:

 export function initHandlers() {
     // Generic SQL proxy handler for Drizzle ORM
     ipcMain.handle("sql:proxy", handleSqlProxy);
-    ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy);
+    if (process.env.ENABLE_UNSAFE_SQL_PROXY === "1") {
+        ipcMain.handle("unsafeSql:proxy", handleUnsafeSqlProxy);
+    }
apps/desktop/electron/database/services/DrizzleMigrationService.ts (3)

10-13: Move DbConnection type out of test utilities; avoid redefining DB alias here.

Production code should not import types from tests. Create apps/desktop/electron/database/types/db.ts and re-export shared types there; also avoid shadowing DB exported from ../db by renaming this local union (or import DB from ../db and introduce a new alias like DrizzleDb for any union use).


137-150: Wrap initialization in a single transaction.

PRAGMA + seed insert + trigger creation should be atomic.

-async initializeDatabase(db: DbConnection | DB) {
-  await db.run(sql`PRAGMA user_version = 7`);
-  await db.insert(schema.field_properties).values({
-    id: 1,
-    json_data: JSON.stringify(FieldPropertiesTemplates.HIGH_SCHOOL_FOOTBALL_FIELD_NO_END_ZONES),
-  });
-  await createAllTriggers(db);
-}
+async initializeDatabase(db: DbConnection | DB) {
+  await (db as any).transaction?.(async (tx: any) => {
+    await tx.run(sql`PRAGMA user_version = 7`);
+    await tx.insert(schema.field_properties).values({
+      id: 1,
+      json_data: JSON.stringify(
+        FieldPropertiesTemplates.HIGH_SCHOOL_FOOTBALL_FIELD_NO_END_ZONES,
+      ),
+    });
+    await createAllTriggers(tx);
+  });
+}

62-71: Fix invalid runtime check: instanceof BetterSQLite3Database is a type, not a constructor.

This will not work at runtime and can throw. Either:

  • Restrict this service to BetterSQLite3 (preferred) and remove the union so you can call migrate(...) directly, or
  • Add an explicit capability flag/constructor param and branch on that, not a TS type.

Example (restrict to BetterSQLite3):

- export type DB = BetterSQLite3Database<typeof schema> | DbConnection;
+ // Keep this service BetterSQLite3-only; run migrations in main process
+ type DrizzleDb = BetterSQLite3Database<typeof schema>;
...
- if (this.db instanceof BetterSQLite3Database) {
-   await migrate(this.db, { migrationsFolder: folder, migrationsTable: "__drizzle_migrations" });
- } else {
-   throw new Error("Drizzle migrations are not supported for this database");
- }
+ await migrate(this.db as DrizzleDb, {
+   migrationsFolder: folder,
+   migrationsTable: "__drizzle_migrations",
+ });
⛔ Skipped due to learnings
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-17T02:33:06.043Z
Learning: Applies to apps/desktop/src/global/classes/*.ts : Use Drizzle-inferred types for DB entities (e.g., typeof table.$inferSelect) and avoid manually duplicating schema interfaces
Learnt from: CR
PR: OpenMarch/OpenMarch#0
File: .cursor/rules/database-interaction.mdc:0-0
Timestamp: 2025-08-17T02:33:33.561Z
Learning: Applies to apps/desktop/src/hooks/queries/use*.ts : Ensure full TypeScript type safety, including exported Database[TableName], New[TableName]Args, and Modified[TableName]Args types
apps/desktop/electron/database/migrations/schema.ts (2)

184-185: Confirm migration for rotation_degrees exists and is NOT NULL DEFAULT 0.

Schema adds rotation_degrees real().notNull().default(0) to marcher_pages. Ensure a migration (e.g., 0008) adds this column with the same constraints and backfills existing rows to 0.


14-22: Convert timestamps helper to a factory and update spreads.

Reusing Drizzle builders across tables triggers “ColumnBuilder was already used.” Return fresh builders per call and spread ...timestamps().

-const timestamps = {
-  created_at: text().notNull().default(sql`(CURRENT_TIMESTAMP)`),
-  updated_at: text().notNull().default(sql`(CURRENT_TIMESTAMP)`).$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
-};
+const timestamps = () => ({
+  created_at: text().notNull().default(sql`(CURRENT_TIMESTAMP)`),
+  updated_at: text()
+    .notNull()
+    .default(sql`(CURRENT_TIMESTAMP)`)
+    .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
+});
@@
-    ...timestamps,
+    ...timestamps(),
@@
-        ...timestamps,
+        ...timestamps(),
@@
-        ...timestamps,
+        ...timestamps(),
@@
-    ...timestamps,
+    ...timestamps(),
@@
-        ...timestamps,
+        ...timestamps(),
@@
-    ...timestamps,
+    ...timestamps(),
@@
-    ...timestamps,
+    ...timestamps(),
@@
-        ...timestamps,
+        ...timestamps(),
@@
-    ...timestamps,
+    ...timestamps(),

Also applies to: 70-71, 103-104, 131-132, 140-141, 165-166, 209-210, 246-247, 252-253, 267-268, 284-285, 301-302

apps/desktop/electron/database/migrations/0006_bitter_proemial_gods.sql (1)

34-35: Fix DEFAULT timestamp expressions (literal string vs function).

Use SQLite DEFAULT (CURRENT_TIMESTAMP), not a quoted literal.

-  `created_at` text DEFAULT 'sql`(CURRENT_TIMESTAMP)`' NOT NULL,
-  `updated_at` text DEFAULT 'sql`(CURRENT_TIMESTAMP)`' NOT NULL,
+  `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
+  `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
apps/desktop/electron/database/migrations/0007_misty_zemo.sql (1)

135-153: Make position_order NOT NULL and backfill before creating the unique index.

Nullable allows multiple NULLs per shape_page_id, defeating uniqueness. Align with schema and backfill NULLs deterministically.

 CREATE TABLE `__new_shape_page_marchers` (
   `id` integer PRIMARY KEY NOT NULL,
   `shape_page_id` integer NOT NULL,
   `marcher_id` integer NOT NULL,
-  `position_order` integer,
+  `position_order` integer NOT NULL,
   `created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
   `updated_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
   `notes` text,
   FOREIGN KEY (`shape_page_id`) REFERENCES `shape_pages`(`id`) ON UPDATE no action ON DELETE cascade,
   FOREIGN KEY (`marcher_id`) REFERENCES `marchers`(`id`) ON UPDATE no action ON DELETE cascade
 );
 -- Backfill NULLs on insert to preserve uniqueness per shape_page_id
-INSERT INTO `__new_shape_page_marchers`("id","shape_page_id","marcher_id","position_order","created_at","updated_at","notes")
-SELECT "id","shape_page_id","marcher_id","position_order","created_at","updated_at","notes" FROM `shape_page_marchers`;
+INSERT INTO `__new_shape_page_marchers`("id","shape_page_id","marcher_id","position_order","created_at","updated_at","notes")
+SELECT sp.id,
+       sp.shape_page_id,
+       sp.marcher_id,
+       COALESCE(
+         sp.position_order,
+         (SELECT IFNULL(MAX(sp2.position_order), 0) + 1
+            FROM shape_page_marchers sp2
+           WHERE sp2.shape_page_id = sp.shape_page_id)
+       ) AS position_order,
+       sp.created_at,
+       sp.updated_at,
+       sp.notes
+  FROM shape_page_marchers sp;

Comment on lines +7 to +9
When running commands, start with the following "source ~/.zshrc"

When running vitest, use "npx vitest run {test_here}"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Clarify shell-agnostic setup instructions.

Requiring every contributor to run source ~/.zshrc before commands is brittle—bash (our default in several CI scripts) will warn or fail if that file doesn’t exist, and users on other shells (fish, PowerShell, bash on Windows) can’t follow it. Please make the setup instruction conditional (e.g., “if you’re in zsh and rely on aliases, run …”) or point to a more general environment bootstrap script instead.

🤖 Prompt for AI Agents
In .cursor/rules/command-syntax.mdc around lines 7–9, the instruction "source
~/.zshrc" is shell-specific and brittle; replace it with a brief, shell-agnostic
note that tells users to source their shell's startup file only if needed (e.g.,
"if you use zsh and rely on aliases, run source ~/.zshrc") or point to a common
project bootstrap script (e.g., "source ./scripts/bootstrap" or "source
~/.profile" for POSIX shells), and keep the vitest invocation as-is but ensure
the wording reflects it is universal (e.g., "Run tests: npx vitest run
{test_here}").

Comment on lines +69 to +143
"beats": {
"name": "beats",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"duration": {
"name": "duration",
"type": "real",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"position": {
"name": "position",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"include_in_measure": {
"name": "include_in_measure",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 1
},
"notes": {
"name": "notes",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
},
"updated_at": {
"name": "updated_at",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(CURRENT_TIMESTAMP)"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {
"beats_duration_check": {
"name": "beats_duration_check",
"value": "duration >= 0"
},
"beats_position_check": {
"name": "beats_position_check",
"value": "position >= 0"
},
"beats_include_in_measure": {
"name": "beats_include_in_measure",
"value": "include_in_measure IN (0, 1)"
}
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Enforce uniqueness for beats.position (schema/data integrity).

Code comments claim position is “Integer and unique,” but neither this snapshot nor schema adds a unique constraint. Add a unique index on beats(position) to prevent duplicate positions and keep page relations deterministic.

Would you like a follow-up migration (e.g., 0010) that adds:

CREATE UNIQUE INDEX IF NOT EXISTS beats_position_unique ON beats (position);

🏁 Script executed:

#!/bin/bash
# List all migrations
ls apps/desktop/electron/database/migrations

# Find where the 'beats' table is created
rg -nP "createTable\(['\"]beats['\"]" -C3 apps/desktop/electron/database/migrations

# Search for any unique constraint or index on 'position'
rg -n "position" -C3 apps/desktop/electron/database/migrations | rg -C3 "unique"

Length of output: 25931


Enforce uniqueness on beats.position
The JSDoc in apps/desktop/electron/database/migrations/schema.ts declares position as unique, but neither the snapshots nor migrations define this constraint.

  • Add migration (e.g., 0010_add_beats_position_unique.sql) with:
    CREATE UNIQUE INDEX IF NOT EXISTS beats_position_unique ON beats (position);
  • Update the position column definition in schema.ts to include .unique(), aligning implementation with the comment.
🤖 Prompt for AI Agents
In apps/desktop/electron/database/migrations/meta/0007_snapshot.json around
lines 69–143 the snapshot lacks the unique constraint for beats.position even
though schema.ts JSDoc declares it; add a new migration file (e.g.,
migrations/0010_add_beats_position_unique.sql) that runs a CREATE UNIQUE INDEX
IF NOT EXISTS beats_position_unique ON beats (position); and update
apps/desktop/electron/database/migrations/schema.ts to mark the position column
as unique (e.g., add .unique() to its column definition) so the schema and
migrations/snapshots stay consistent.

Comment on lines +1 to +52
import type { Database } from "sql.js";
import { DefaultLogger } from "drizzle-orm/logger";
import {
createTableRelationsHelpers,
extractTablesRelationalConfig,
type RelationalSchemaConfig,
type TablesRelationalConfig,
} from "drizzle-orm/relations";
import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core/db";
import { SQLiteSyncDialect } from "drizzle-orm/sqlite-core/dialect";
import type { DrizzleConfig } from "drizzle-orm/utils";
import { SQLJsSession } from "drizzle-orm/sql-js/session";

export type SQLJsDatabase<
TSchema extends Record<string, unknown> = Record<string, never>,
> = BaseSQLiteDatabase<"sync", void, TSchema>;

export function drizzle<
TSchema extends Record<string, unknown> = Record<string, never>,
>(
client: Database,
config: DrizzleConfig<TSchema> = {},
): SQLJsDatabase<TSchema> {
const dialect = new SQLiteSyncDialect({ casing: config.casing });
let logger;
if (config.logger === true) {
logger = new DefaultLogger();
} else if (config.logger !== false) {
logger = config.logger;
}

let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
if (config.schema) {
const tablesConfig = extractTablesRelationalConfig(
config.schema,
createTableRelationsHelpers,
);
schema = {
fullSchema: config.schema,
schema: tablesConfig.tables,
tableNamesMap: tablesConfig.tableNamesMap,
};
}

const session = new SQLJsSession(client, dialect, schema, { logger });
return new BaseSQLiteDatabase(
"sync",
dialect,
session,
schema,
) as SQLJsDatabase<TSchema>;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Replace custom driver with Drizzle’s public sql-js adapter (removes brittle deep imports).

This file reimplements Drizzle’s sql-js adapter using internal modules (relations/sqlite-core/session/utils). That’s fragile across minor releases and unnecessary—public drizzle-orm/sql-js already supports schema, logger, and casing. Refactor to a thin wrapper to keep types stable and fix future upgrade risk.

Apply this diff to replace the file:

- import type { Database } from "sql.js";
- import { DefaultLogger } from "drizzle-orm/logger";
- import {
-     createTableRelationsHelpers,
-     extractTablesRelationalConfig,
-     type RelationalSchemaConfig,
-     type TablesRelationalConfig,
- } from "drizzle-orm/relations";
- import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core/db";
- import { SQLiteSyncDialect } from "drizzle-orm/sqlite-core/dialect";
- import type { DrizzleConfig } from "drizzle-orm/utils";
- import { SQLJsSession } from "drizzle-orm/sql-js/session";
-
- export type SQLJsDatabase<
-     TSchema extends Record<string, unknown> = Record<string, never>,
- > = BaseSQLiteDatabase<"sync", void, TSchema>;
-
- export function drizzle<
-     TSchema extends Record<string, unknown> = Record<string, never>,
- >(
-     client: Database,
-     config: DrizzleConfig<TSchema> = {},
- ): SQLJsDatabase<TSchema> {
-     const dialect = new SQLiteSyncDialect({ casing: config.casing });
-     let logger;
-     if (config.logger === true) {
-         logger = new DefaultLogger();
-     } else if (config.logger !== false) {
-         logger = config.logger;
-     }
-
-     let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
-     if (config.schema) {
-         const tablesConfig = extractTablesRelationalConfig(
-             config.schema,
-             createTableRelationsHelpers,
-         );
-         schema = {
-             fullSchema: config.schema,
-             schema: tablesConfig.tables,
-             tableNamesMap: tablesConfig.tableNamesMap,
-         };
-     }
-
-     const session = new SQLJsSession(client, dialect, schema, { logger });
-     return new BaseSQLiteDatabase(
-         "sync",
-         dialect,
-         session,
-         schema,
-     ) as SQLJsDatabase<TSchema>;
- }
+import type { Database } from "sql.js";
+import { drizzle as drizzleSqlJs, type SQLJsDatabase } from "drizzle-orm/sql-js";
+import type { DrizzleConfig } from "drizzle-orm";
+
+export type { SQLJsDatabase };
+
+export function drizzle<
+  TSchema extends Record<string, unknown> = Record<string, never>,
+>(
+  client: Database,
+  config: DrizzleConfig<TSchema> = {},
+): SQLJsDatabase<TSchema> {
+  return drizzleSqlJs<TSchema>(client, config);
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { Database } from "sql.js";
import { DefaultLogger } from "drizzle-orm/logger";
import {
createTableRelationsHelpers,
extractTablesRelationalConfig,
type RelationalSchemaConfig,
type TablesRelationalConfig,
} from "drizzle-orm/relations";
import { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core/db";
import { SQLiteSyncDialect } from "drizzle-orm/sqlite-core/dialect";
import type { DrizzleConfig } from "drizzle-orm/utils";
import { SQLJsSession } from "drizzle-orm/sql-js/session";
export type SQLJsDatabase<
TSchema extends Record<string, unknown> = Record<string, never>,
> = BaseSQLiteDatabase<"sync", void, TSchema>;
export function drizzle<
TSchema extends Record<string, unknown> = Record<string, never>,
>(
client: Database,
config: DrizzleConfig<TSchema> = {},
): SQLJsDatabase<TSchema> {
const dialect = new SQLiteSyncDialect({ casing: config.casing });
let logger;
if (config.logger === true) {
logger = new DefaultLogger();
} else if (config.logger !== false) {
logger = config.logger;
}
let schema: RelationalSchemaConfig<TablesRelationalConfig> | undefined;
if (config.schema) {
const tablesConfig = extractTablesRelationalConfig(
config.schema,
createTableRelationsHelpers,
);
schema = {
fullSchema: config.schema,
schema: tablesConfig.tables,
tableNamesMap: tablesConfig.tableNamesMap,
};
}
const session = new SQLJsSession(client, dialect, schema, { logger });
return new BaseSQLiteDatabase(
"sync",
dialect,
session,
schema,
) as SQLJsDatabase<TSchema>;
}
import type { Database } from "sql.js";
import { drizzle as drizzleSqlJs, type SQLJsDatabase } from "drizzle-orm/sql-js";
import type { DrizzleConfig } from "drizzle-orm";
export type { SQLJsDatabase };
export function drizzle<
TSchema extends Record<string, unknown> = Record<string, never>,
>(
client: Database,
config: DrizzleConfig<TSchema> = {},
): SQLJsDatabase<TSchema> {
return drizzleSqlJs<TSchema>(client, config);
}

Comment on lines +45 to +51
const session = new SQLJsSession(client, dialect, schema, { logger });
return new BaseSQLiteDatabase(
"sync",
dialect,
session,
schema,
) as SQLJsDatabase<TSchema>;
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify sql.js WASM asset handling in desktop packaging.

Ensure initSqlJs({ locateFile }) resolves sql-wasm.wasm at runtime in Electron. Missing asset wiring will fail DB init in renderer.


🏁 Script executed:

#!/bin/bash
# Check for sql.js initialization and wasm asset packaging
set -euo pipefail

echo "References to sql.js init and wasm:"
rg -n -g 'apps/desktop/**' -C2 -e 'initSqlJs\s*\(' -e 'locateFile' -e 'sql-wasm\.wasm' || true

echo
echo "Electron packaging config that might include wasm assets:"
fd -a package.json -HI apps/desktop | while read -r p; do
  echo "--- $p"
  jq -r '.build // empty' "$p" || true
done

Length of output: 1637



🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for initSqlJs usage in sqljs-driver.ts"
rg -n -C3 'initSqlJs' apps/desktop/electron/database/services/sqljs-driver.ts || true

echo
echo "Searching for class SQLJsSession definition"
rg -n -C3 'class SQLJsSession' apps/desktop/electron/database/services/sqljs-driver.ts || true

echo
echo "Searching for locateFile in sqljs-driver.ts"
rg -n -C3 'locateFile' apps/desktop/electron/database/services/sqljs-driver.ts || true

Length of output: 138


Include sql.js WASM in the Electron build and configure locateFile
In apps/desktop/electron/database/services/sqljs-driver.ts, initialize sql.js with a locateFile callback that points to the packaged WASM (e.g. from process.resourcesPath or your asar). Then in apps/desktop/package.json under build.files, add "node_modules/sql.js/dist/sql-wasm.wasm" (or the entire node_modules/sql.js/dist folder) so the WASM is bundled. Without this, the renderer’s DB init will fail.

🤖 Prompt for AI Agents
In apps/desktop/electron/database/services/sqljs-driver.ts around lines 45 to
51, the sql.js WASM is not being located during runtime causing DB init
failures; initialize sql.js before creating the SQLJsSession by calling its init
(or configure the initSqlJs) with a locateFile callback that resolves to the
packaged wasm (use process.resourcesPath or the asar path to point to
node_modules/sql.js/dist/sql-wasm.wasm), and update apps/desktop/package.json
build.files to include "node_modules/sql.js/dist/sql-wasm.wasm" (or the whole
"node_modules/sql.js/dist" folder) so the wasm is included in the Electron
build.

@AlexDumo AlexDumo merged commit 1729dc3 into main Sep 29, 2025
16 checks passed
This was referenced Oct 1, 2025
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.

Cannot redo FieldProperties Undo When undoing or redoing, refresh the field properties

1 participant