Skip to content

Wrap package creation's stub database inserts in a single transaction - #195

Merged
laffer1 merged 2 commits into
mainfrom
create-stub-db-transaction
Sep 8, 2026
Merged

laffer1 merged 2 commits into
mainfrom
create-stub-db-transaction

Conversation

@laffer1

@laffer1 laffer1 commented Sep 8, 2026 •

Copy link
Copy Markdown
Member

Fixes #193.

Summary

  • mport_create_primative now opens a transaction after the stub schema is created, runs the asset, package, depends, conflicts, categories and annotation inserts, and commits before closing the handle.
  • Any failure jumps to a DBFAIL label that rolls back via sqlite3_exec (so the original error is not clobbered), closes the handle, and removes the temp directory. This matches the rollback pattern in the delete and install paths.
  • The failure path now also closes the stub handle, which the old code leaked.

Verification

  • libmport and mport.create build clean under -Werror.
  • kyua test mport_create_test: 5/5 passed.
  • cppcheck/clang-format precommit script clean. Splint stops on a pre-existing parse error at an unrelated line.
  • A bundle built from a 4000-file staged tree contains all 4000 asset rows plus the package and category rows.

Timing

mport.create on a 4000-file staged tree, three runs each, ZFS /tmp:

Build Wall time sys time
Before (autocommit per insert) 27.1 to 27.9 s 4.1 to 4.6 s
After (single transaction) 0.75 to 0.87 s 0.23 to 0.29 s

🤖 Generated with Claude Code

https://claude.ai/code/session_01VqVYKWFCX58Cb5NiGHHjzo

Summary by Sourcery

Make package creation transactional so generated stub databases are committed atomically and cleaned up safely on failure.

Bug Fixes:

  • Ensure package creation rolls back all stub database changes when any asset or metadata insertion fails.
  • Close the stub database handle on schema-generation and insertion failures, preventing resource leaks.

Enhancements:

  • Batch package asset and metadata inserts in a single database transaction, substantially improving package creation performance.

mport_create_primative wrote the bundle's stub database one autocommit
INSERT at a time, so SQLite synced the journal to disk for every asset
row. A port with thousands of files paid one fsync per file, which
dominated the wall-clock time of mport.create on slower storage.

Open a transaction after the stub schema is created, commit it after the
last insert, and roll back and close the handle on any failure. The stub
database is rebuilt from scratch on every run and discarded on error, so
there is no durability requirement between rows. The rollback goes through
sqlite3_exec directly so it cannot clobber the error being returned. The
failure path now also closes the stub handle, which was previously leaked.

Creating a 4000-file package on ZFS drops from about 27 seconds to under
one second.

Fixes #193

AI-Assisted-By: Claude Fable 5.1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqVYKWFCX58Cb5NiGHHjzo
@sourcery-ai

sourcery-ai Bot commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Package creation now performs all stub database inserts in a single transaction, substantially reducing bulk-build overhead while ensuring failures roll back, close the database handle, and remove temporary state without clobbering the original error.

Sequence diagram for transactional package creation

sequenceDiagram
    participant Creator as mport_create_primative
    participant DB as StubDatabase
    participant FS as TemporaryState

    Creator->>DB: create_stub_db()
    Creator->>DB: mport_db_do(BEGIN TRANSACTION)
    Creator->>DB: insert_assetlist()
    Creator->>DB: insert_meta()
    Creator->>DB: mport_db_do(COMMIT TRANSACTION)
    Creator->>DB: sqlite3_close()
    Creator->>FS: clean_up(tmpdir)

    alt insert or transaction failure
        Creator->>DB: sqlite3_exec(ROLLBACK)
        Creator->>DB: sqlite3_close()
        Creator->>FS: clean_up(tmpdir)
    end
Loading

File-Level Changes

Change Details Files
Batch all stub database writes in one transaction during package creation.
  • Begin a transaction after creating the stub schema.
  • Run asset, package metadata, dependency, conflict, category, and annotation inserts within the transaction.
  • Commit only after all inserts succeed, preserving the existing insert ordering.
libmport/create_primative.c
Centralize database-failure cleanup with rollback and resource removal.
  • Route transaction setup, insert, and commit failures through a DBFAIL path.
  • Rollback directly with sqlite3_exec without replacing the original error code.
  • Close the database handle and remove the temporary directory on failure, fixing the prior handle leak.
libmport/create_primative.c

Assessment against linked issues

Issue Objective Addressed Explanation
#193 Wrap all stub database inserts performed by mport_create_primative in a single transaction, beginning after stub database creation and committing after the final insert. ✅
#193 Roll back the stub database transaction when any insert or commit-related operation fails, while preserving the original error and cleaning up the temporary database. ✅
#193 Close the stub database handle on failure to avoid leaking the database connection. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Claude finished @laffer1's task in 2m 17s —— View job


Claude finished the code review

Todo list

  • Gather context (diff, changed file, related patterns in delete/install paths)
  • Understand request (code review of transaction wrapping change)
  • Review correctness, memory safety, error propagation, SQL, path traversal, consistency
  • Post final review feedback

Review

The core change — wrapping the stub-DB inserts in BEGIN TRANSACTION / COMMIT TRANSACTION, with a DBFAIL label that rolls back via sqlite3_exec before sqlite3_close — is correct and matches the existing rollback idiom used in delete_primative.c:475 and bundle_read_install_pkg.c:1300 (discard the rollback's own error so it doesn't clobber the original error_code). No SQL injection risk (all statements are fixed string literals, no interpolated user data). Timing numbers in the description are plausible for switching 4000+ autocommit inserts to one transaction.

Two things worth a look, both edge cases rather than blockers:

1. create_stub_db schema-creation failure still leaks the DB handle (libmport/create_primative.c:95-96, 139-157)
If sqlite3_open() in create_stub_db succeeds but the subsequent mport_generate_stub_schema(mport, *db) call fails (line 156), *db is left open and non-NULL, and the caller's create_stub_db(...) != MPORT_OK check sends control to CLEANUP (line 96), not DBFAIL — so the handle is never closed, only the tmpdir is removed. This is pre-existing behavior (not introduced by this diff), but it's adjacent to exactly what this PR is fixing, and the PR description states the failure path "now also closes the stub handle, which the old code leaked" — that's only true for failures after BEGIN TRANSACTION is reached. A schema-creation failure is still an unclosed-handle leak. Worth a follow-up (route this failure through a close, or route create_stub_db's internal failure to also close *db before returning).

2. DBFAIL's sqlite3_close(db) return value is discarded, and can silently fail to actually close the handle (libmport/create_primative.c:131-132)
sqlite3_close() (not _v2) returns SQLITE_BUSY and leaves the connection open if there's an unfinalized prepared statement. Several bind-failure branches in insert_assetlist (e.g. lines 194-213) and insert_meta (e.g. lines 314-370) return/RETURN_ERROR without calling sqlite3_finalize(stmnt) first — a pre-existing leak, out of scope for this PR. But it means that if one of those rare bind failures triggers the new DBFAIL path, sqlite3_close(db) here can fail and leave the handle open (the temp file still gets unlinked by clean_up(tmpdir), but the in-process sqlite3* and its fd leak). Since sqlite3_close() un-checked is already the pattern used elsewhere in this codebase (e.g. merge_primative.c:178), this isn't a new inconsistency — just flagging that the "closes the stub handle" guarantee isn't airtight given the unfinalized-statement paths elsewhere in this file.

Neither of these is introduced by this diff, and both stem from pre-existing gaps in insert_assetlist/insert_meta/create_stub_db error handling rather than the transaction logic itself, which is sound.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="libmport/create_primative.c" line_range="128-133" />
<code_context>

+	return error_code;
+
+DBFAIL:
+	/* sqlite3_exec directly so the rollback cannot clobber the error; a no-op if
+	 * the transaction never began or was already committed */
+	(void)sqlite3_exec(db, "ROLLBACK", NULL, NULL, NULL);
+	(void)sqlite3_close(db);
+	clean_up(tmpdir);
+
 	return error_code;
</code_context>
<issue_to_address>
**issue (bug_risk):** When an insert fails after preparing a SQLite statement, several insert helpers return without finalizing that statement; the DBFAIL cleanup then ignores SQLITE_BUSY from sqlite3_close, leaving the stub handle open while the temporary directory is removed.

**Triggers:** When an asset bind/step fails or malformed dependency data causes an early return from an insert helper.

**Suggested fix:** Ensure every insert helper finalizes its statement on all error paths before DBFAIL calls sqlite3_close, and check or otherwise handle the close result.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread libmport/create_primative.c
…schema failure

Review of #195 pointed out two gaps around the new DBFAIL rollback path.
Several insert helpers returned from a bind or step failure without
finalizing their prepared statement, so sqlite3_close in DBFAIL could
return SQLITE_BUSY and leave the stub handle open. And when
mport_generate_stub_schema failed, create_stub_db returned with the
connection still open and the caller never closed it.

Route every failure in insert_assetlist, insert_meta and insert_depends
through a single label that finalizes the statement, finalize before the
regcomp failure return in insert_conflicts, and close the handle in
create_stub_db when schema creation fails. insert_meta also no longer
leaks os_release on bind failures. Error codes and messages are
unchanged.

Verified by forcing a malformed depend through mport.create: it exits 1
with the original message, produces no bundle, and leaves no temp dir.

AI-Assisted-By: Claude Fable 5.1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqVYKWFCX58Cb5NiGHHjzo
@github-actions

github-actions Bot commented Sep 8, 2026 •

Copy link
Copy Markdown

Claude finished @laffer1's task in 1m 20s —— View job


Re-review of 2b1ff2c — both prior findings are fixed

Commit 2b1ff2c directly addresses the two issues raised in the previous review pass (mine and Sourcery's inline comment). Verified against the diff (d6ff609..2b1ff2c) and the full file.

1. Sourcery's finding — unfinalized statements before DBFAIL's sqlite3_close ✅ fixed
Every insert helper (insert_assetlist, insert_meta, insert_depends, insert_conflicts, insert_annotations) now routes all error returns through a single done:/end-of-function sqlite3_finalize(stmnt) instead of bare return/RETURN_ERROR mid-function:

  • insert_assetlist (libmport/create_primative.c:184-307): every bind/step/stat/hash failure now does error_code = SET_ERROR(...); goto done;, and done: unconditionally finalizes.
  • insert_meta (create_primative.c:333-399): same pattern; as a bonus this also fixes a pre-existing leak where os_release was only freed on the bind-7 failure path — now free(os_release) runs for every exit.
  • insert_depends (create_primative.c:557-620): same goto done conversion.
  • insert_conflicts (create_primative.c:486-490): the regcomp failure path now finalizes stmnt before returning (previously leaked the prepared statement).
  • insert_annotations/insert_categories: already finalized unconditionally after their loops; unchanged.

With no path left that returns from a helper with a live prepared statement, sqlite3_close(db) at DBFAIL (create_primative.c:128-137) can no longer see SQLITE_BUSY from a dangling stmnt in these callees. The added comment on create_primative.c:132-133 accurately documents this invariant. Confirmed each error_code = mport_err_code(); goto done; substitution (replacing RETURN_CURRENT_ERROR) is behaviorally identical — RETURN_CURRENT_ERROR is #defined as return mport_err_code() (mport_private.h:208), so deferring through done doesn't change what's returned.

2. My finding — create_stub_db schema-creation failure leaked the handle ✅ fixed
create_primative.c:158-162 now closes *db and sets it to NULL when mport_generate_stub_schema fails, before returning to the caller. The caller's goto CLEANUP path (not DBFAIL) never touches db again, so there's no double-close risk either.

No new issues found in the delta. Rollback/commit control flow, SQL statements (all fixed string literals, no injected data), and cleanup ordering are unchanged and remain correct.

Nice fix — this closes out the two gaps from the initial review cleanly and without introducing new ones.

@laffer1
laffer1 merged commit ff6ee53 into main Sep 8, 2026
5 of 6 checks passed
laffer1 added a commit that referenced this pull request Sep 8, 2026
…schema failure

Review of #195 pointed out two gaps around the new DBFAIL rollback path.
Several insert helpers returned from a bind or step failure without
finalizing their prepared statement, so sqlite3_close in DBFAIL could
return SQLITE_BUSY and leave the stub handle open. And when
mport_generate_stub_schema failed, create_stub_db returned with the
connection still open and the caller never closed it.

Route every failure in insert_assetlist, insert_meta and insert_depends
through a single label that finalizes the statement, finalize before the
regcomp failure return in insert_conflicts, and close the handle in
create_stub_db when schema creation fails. insert_meta also no longer
leaks os_release on bind failures. Error codes and messages are
unchanged.

Verified by forcing a malformed depend through mport.create: it exits 1
with the original message, produces no bundle, and leaves no temp dir.

AI-Assisted-By: Claude Fable 5.1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VqVYKWFCX58Cb5NiGHHjzo
Signed-off-by: Lucas Holt <luke@foolishgames.com>
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.

Wrap package creation's stub database inserts in a single transaction

1 participant