Load @db/sqlite on first use instead of at import - #23
Merged
Merged
Conversation
@db/sqlite dlopens the SQLite library as it evaluates, so a static import made every consumer pay that dlopen — and inherit its failure modes — even when no SQLite client was ever created. @probitas/probitas re-exports this package from its root module, so importing `@probitas/probitas` at all was enough to trigger it, and a broken library took down scenarios that never touch SQLite. Importing @db/sqlite inside createSqliteClient defers the load to the point where SQLite is actually wanted. The function already returned a Promise, so making it async keeps the signature compatible. A load failure now surfaces as a catchable error from createSqliteClient rather than killing the module graph. Deferred imports are not an option here: @db/sqlite uses top-level await, and a subgraph containing it cannot be deferred. The integration test now loads @db/sqlite up front. The dlopen used to happen during module evaluation, outside the resource sanitizer's scope; deferring it moves it inside a test, where the library @db/sqlite keeps open for the life of the process is reported as a leak. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E6iErRVYiy7LzNGLT5Vzh1
5 tasks done
There was a problem hiding this comment.
🟢 Approval recommended
The lazy-loading change preserves the public Promise<SqliteClient> contract while preventing import-time native side effects, and the test adjustment aligns with the sanitizer’s expected resource lifetime semantics.
Pull request overview
This PR defers loading @db/sqlite until a SQLite client is actually created, avoiding process-wide native library dlopen side effects during unrelated imports (notably when @probitas/probitas is imported but SQLite is never used).
Changes:
- Converted the
@db/sqlitemodule-scope import to a type-only import and moved the runtime import intocreateSqliteClient()viaawait import("@db/sqlite"). - Updated the integration test to eagerly
import "@db/sqlite"at file scope so the one-time, process-lifetime native handle is outside Deno’s per-test resource sanitizer scope.
File summaries
| File | Description |
|---|---|
| packages/probitas-client-sql-sqlite/client.ts | Lazily imports @db/sqlite inside createSqliteClient to prevent native loading at module evaluation time. |
| packages/probitas-client-sql-sqlite/integration_test.ts | Loads @db/sqlite once at test-file evaluation to avoid sanitizer false positives for a process-lifetime resource. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
LocalStack merged its community and pro images, and every image published after that merge quits with exit code 55 unless LOCALSTACK_AUTH_TOKEN is set. The test job died at container init before a single test ran, and compose.yaml hits the same wall locally. 4.14.0 is the last community release, so it starts without a token. This is a stopgap that freezes the SQS test environment at February 2026; the same pin and the same follow-up apply to probitas-test/probitas. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E6iErRVYiy7LzNGLT5Vzh1
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
3 of 4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@db/sqliteinsidecreateSqliteClientinstead of at module scope, so the SQLite library is loaded only when a SQLite client is actually created.@db/sqliteup front in the integration test, keeping its process-lifetime library out of the resource sanitizer's scope.Why
@db/sqlitedlopens the SQLite library while it evaluates, so a static import made every consumer of this package pay that dlopen — and inherit its failure modes — even when no SQLite client was ever created.@probitas/probitasre-exports this package from its root module (mod.ts→client.ts→sql.ts→sql/sqlite.ts), which means importing@probitas/probitasat all was enough to trigger it. When the library failed to load, scenarios that never touch SQLite died with it.That is not hypothetical. In probitas-test/probitas#108, updating the pinned nixpkgs moved CI onto an environment where the prebuilt
libsqlite3.sothat@db/sqlitedownloads segfaults on dlopen, and every scenario subprocess died before running a step — none of them used SQLite.Auditing every client module in
@probitas/probitas(hooking bothDeno.dlopenandprocess.dlopen, one module per process) showed SQLite is the only one that loads anything native at import time:sql.tssql/sqlite.tssql/duckdb.ts@probitas/client-sql-duckdbhas the same static-import shape, but@duckdb/node-apidefers its own native binding untilDuckDBInstance.create(), so it costs nothing today. It is left alone here.createSqliteClientalready returnedPromise<SqliteClient>, so making itasynckeeps the signature compatible with every caller. A load failure now surfaces as a catchable error fromcreateSqliteClientrather than killing the module graph at import time.Why not
import defer. Deno 2.9.5 does implement deferred imports, and they work on ordinary modules. They cannot help here:@db/sqliteuses top-level await (await dlopen(...)inffi.ts), and a subgraph containing an async module cannot be deferred — Deno evaluates it eagerly anyway. Verified directly before ruling it out.The test change. Deferring the load is exactly what moves the dlopen from module evaluation (outside the resource sanitizer's scope) into a test.
@db/sqlitekeeps its library open for the life of the process, so the sanitizer reported it as a per-test leak. Loading it at the top of the test file puts that one-time load back outside the sanitizer's scope, which is accurate, and keeps leak detection on for everything else.Test Plan
deno task verifyon Deno 2.9.5 — 1411 passed, 0 failed, 44 ignoredDENO_SQLITE_PATH: importing the package no longer dlopenscreateSqliteClientand surfaces as a catchableSqliteError🤖 Generated with Claude Code
https://claude.ai/code/session_01E6iErRVYiy7LzNGLT5Vzh1