upgrading/extension/upgrades/0.10-to-0.11/instructions.md
upgrading/extension/upgrades/0.10-to-0.11/instructions.mdBrowse 76 files
14,915 bytes
Token encoding: o200k_base
Snapshot fac8604
from: "0.10" to: "0.11" changes:
- id: namespace-kind-required-on-handcrafted-contract-literals
summary: Add the
kinddiscriminator ('sql-namespace'for SQL namespaces;'mongo-namespace'for Mongo namespaces) to every handcraftedContract<{ namespaces: { ... } }>type literal in extension test-d files and inline-typed fixtures. The frameworkNamespaceinterface tightenedkindfrom optional to required; emittedcontract.d.tsliterals carry it automatically, but anything you write by hand will fail typecheck without it. detection: glob: "**/*.test-d.ts" contains: - "Contract<" - "namespaces" anyMatch: true - id: facade-add-close-and-async-dispose
summary: |
The official Prisma 8 facades (
@internal/postgres,@internal/sqlite,@internal/mongo) now exposeclose()and[Symbol.asyncDispose]so short-lived scripts can release facade-owned resources cleanly and exit instead of hanging on a live connection. Extensions that expose a facade in the same shape should add the same surface for parity, honouring the ownership rule (only close resources the facade itself constructed) and managing a terminal closed state (subsequent operations reject with a clear error). No script — manual code authoring per extension. detection: glob: "**/src/runtime/*.ts" contains: - "createRuntimeCore" anyMatch: true - id: mongo-close-ownership-rule
summary: |
The Mongo facade's
db.close()is silently breaking. Before 0.11 it unconditionally calledawait runtime.close(), which closed the underlyingMongoClientregardless of how the facade was constructed. From 0.11 it honours the ownership rule and closes only facade-constructed clients (from a{ url }binding); when the facade was constructed with{ mongoClient }the caller-suppliedMongoClientis left untouched. Extensions that wrap or extend@internal/mongoand relied ondb.close()disposing of a sharedMongoClientneed to either switch to a{ url }binding (so the facade owns the client) or close theMongoClientthemselves. detection: glob: "**/*.{ts,tsx}" contains: - "mongoClient" - "@internal/mongo" anyMatch: true - id: insert-single-row-wrap-in-array
summary: Wrap single-row
.insert({...})call sites in an array —.insert([{...}]). The single-object overload is removed;.insert()now exclusively accepts an array of row objects. detection: glob: "**/*.{ts,tsx}" contains: - ".insert(" anyMatch: true - id: insert-ast-with-values-to-with-rows
summary: Replace
InsertAst.withValues(assignments)withInsertAst.withRows([assignments]). ThewithValuesmethod is removed;withRowsaccepts an array of assignment maps. detection: glob: "**/*.{ts,tsx}" contains: - ".withValues("
0.10 → 0.11 — Extension-author upgrade instructions
namespace-kind-required-on-handcrafted-contract-literals
Starting at the 0.11 release, the framework Namespace interface tightens kind from optional (inherited from IRNode) to required. Any handcrafted Contract<{ namespaces: { ... } }> type literal — typically used in extension test-d files and inline-typed fixtures — must add kind: 'sql-namespace' (or 'mongo-namespace') to each namespace literal or TypeScript will reject the literal as not structurally assignable to Namespace.
This does not affect:
- Real
contract.d.tsfiles emitted byprisma-next contract emit. The emitter printskindautomatically. - Real
contract.jsonenvelopes. The runtimekindfield is non-enumerable on namespace class instances, so it doesn't surface in the JSON on-disk envelope and the byte hash is unchanged. - Plain-literal
Contractvalues constructed at runtime (the framework only tightens the type declaration ofkind; the runtime walks tolerate missing values).
It does affect:
.test-d.tsfiles in extension packages that handcraftContract<{ namespaces: { __unbound__: { id, tables, ... } } }>literals to assert generic-inference shapes. Each namespace literal needskind: 'sql-namespace'(SQL namespaces) orkind: 'mongo-namespace'(Mongo namespaces) added alongsideid.- Any inline-typed fixture file (
test/fixtures/*.d.ts, etc.) that mirrors what the emitter would produce for testing.
Before 0.11
type ContractUnderTest = Contract<
SqlStorage<string> & {
readonly namespaces: {
readonly __unbound__: {
readonly id: '__unbound__';
readonly tables: {
readonly user: {
// ... columns, primaryKey, etc.
};
};
};
};
readonly storageHash: StorageHash;
},
// ...
>;
Starting at 0.11
type ContractUnderTest = Contract<
SqlStorage<string> & {
readonly namespaces: {
readonly __unbound__: {
readonly id: '__unbound__';
readonly kind: 'sql-namespace'; // ← new: required
readonly tables: {
readonly user: {
// ... columns, primaryKey, etc.
};
};
};
};
readonly storageHash: StorageHash;
},
// ...
>;
Mapping table
| Namespace family | Discriminator literal | Where it surfaces in handcrafted types |
|---|---|---|
SQL (SqlNamespace, SqlUnboundNamespace) | 'sql-namespace' | Anywhere you write Contract<{ namespaces: { … } }> with a SQL-style namespace |
Mongo (MongoNamespace, MongoUnboundNamespace) | 'mongo-namespace' | Anywhere you write Contract<{ namespaces: { … } }> with a Mongo-style namespace |
Postgres (PostgresSchema, when handcrafted) | 'postgres-schema' or 'postgres-unbound-schema' | Rare in extension code; only needed if you handcraft a literal with a Postgres-specific namespace class |
Detection
Run the matcher's grep over your extension's source:
rg --files-with-matches -t ts -e 'Contract<' -e 'namespaces' -g '**/*.test-d.ts'
For each match, open the file and add the kind literal to every namespace entry under namespaces:. If you have many such literals, a regex-driven mechanical pass works (e.g. find every readonly id: '<ns-id>'; inside a namespaces: block and inject readonly kind: 'sql-namespace'; immediately after).
Why the change
The framework needs every namespace IR node to carry its family discriminator at the type level so that cross-family namespace walks (the new elementCoordinates(storage) surface in @internal/framework-components/ir) can dispatch on a known-present kind, not an optional one.
The runtime invariant has always held — every concrete namespace class sets kind non-enumerably via Object.defineProperty(this, 'kind', { value, enumerable: false }) in its constructor. The type tightening makes the invariant honest at the consumer surface.
What you do not need to change
- No
.d.tsregeneration is needed. Runprisma-next contract emitonly as part of your normal authoring flow; the emitter handles thekindfield for you. - No
contract.jsonsnapshot changes. The hash inputs are unchanged becausekindis non-enumerable on namespace class instances. - No runtime API changes. Extension factories that construct namespaces via
new SqlStorage(...),new PostgresSchema(...), etc. are unchanged — the constructors continue to materialisekindnon-enumerably.
facade-add-close-and-async-dispose
Starting at the 0.11 release the three official facades (postgres(), sqlite(), mongo()) expose two new methods:
interface ClientFacade {
// ...existing surface...
close(): Promise<void>;
[Symbol.asyncDispose](): Promise<void>;
}
This is the surface that lets a short-lived script (tsx my-script.ts) release facade-owned connection resources and exit cleanly. Without it, a pg.Pool (or analogous keep-alive in SQLite / Mongo) keeps Node's event loop alive and the script hangs after its last query prints.
If your extension exposes a facade in the same shape (e.g. you publish your own postgresServerless() or someDriver() factory that returns the same client object), add the equivalent surface. Three properties the surface must have:
-
Ownership rule.
close()releases only the resources the facade itself constructed. A{ url }(or similar opaque-string) binding means the facade opened the connection — facade owns it,close()disposes it. A{ pool }/{ client }/{ mongoClient }(caller-supplied opaque-handle) binding means the caller owns it —close()leaves it untouched. The facade must capture this ownership decision at construction time and remember it. -
Idempotence.
close()can be called multiple times in a row without throwing. The second and later calls are no-ops. -
Terminal closed state. After
close()resolves, the facade is permanently locked. Any subsequentdb.orm.X.<op>(...),db.runtime(),db.connect()call rejects withError('<target> client is closed'). This catches use-after-close bugs cleanly instead of silently re-opening resources.
Reference implementations in this repo:
packages/3-extensions/postgres/src/runtime/postgres.ts(full pattern;{ url }vs{ pool }ownership)packages/3-extensions/sqlite/src/runtime/sqlite.ts(only-{ path }shape; the facade always owns)packages/3-extensions/mongo/src/runtime/mongo.ts({ url }vs{ mongoClient })
The [Symbol.asyncDispose] alias is one line — delegate to close() — and enables the TS 5.2+ await using db = yourFacade(...) syntax. There is no good reason not to add both methods together.
mongo-close-ownership-rule
Before 0.11, @internal/mongo's db.close() looked like this:
async close() {
try {
await runtimePromise;
await runtime.close(); // unconditional — closed any MongoClient, owned or not
} catch { /* swallow */ }
closed = true;
}
From 0.11 it captures the ownership decision at construction time and only closes the MongoClient if the facade owns it:
let ownedDispose: (() => Promise<void>) | undefined;
if (resolvedBinding.kind === 'url') {
ownedDispose = () => driver.close();
}
// ...
async close() {
try {
await runtimePromise;
await ownedDispose?.(); // no-op when the caller supplied the MongoClient
} catch { /* swallow */ }
closed = true;
}
If your extension uses mongo({ mongoClient: someClient }) and previously called db.close() expecting someClient to be closed, your test suite will now show the MongoClient outliving the facade. Two ways to migrate:
- Switch to a
{ url }binding. If your extension constructs theMongoClientpurely to hand it tomongo(), drop the manual construction — pass the connection string in{ url }and let the facade own the client. - Close the
MongoClientexplicitly. If your extension genuinely needs to share aMongoClientacross multiple consumers (e.g. one client backs severalmongo()facades, or the client is held in a higher-level connection-management seam), keep the{ mongoClient }binding and addawait someClient.close()after your last facade is disposed.
No script — the right migration depends on why your extension was sharing the client in the first place. Walk the call sites by hand.
insert-single-row-wrap-in-array
Starting at the 0.11 release, the .insert() method on the SQL builder accepts only an array of row objects. The single-object overload that previously allowed .insert({ field: value }) is removed.
Before 0.11:
const ast = InsertAst.into(TableSource.named(tableName))
.insert({ field: value });
// or via the query builder:
db.sql.table.insert({ field: value }).build();
Starting at 0.11:
const ast = InsertAst.into(TableSource.named(tableName))
.insert([{ field: value }]);
// or via the query builder:
db.sql.table.insert([{ field: value }]).build();
Walk every .ts / .tsx file matched by the detection.glob above. For each call site that passes a plain object directly to .insert(...), wrap the argument in an array:
.insert(row)→.insert([row]).insert({ field: value })→.insert([{ field: value }])
Variable references to a row object are safe to wrap directly:
// Before
await runtime.execute(db.sql.table.insert(row).build());
// After
await runtime.execute(db.sql.table.insert([row]).build());
If a call site already passes an array (.insert([row1, row2])), it is already correct — leave it unchanged.
TypeScript will report bare-object call sites as type errors after the bump, which is a reliable compile-time signal for every affected site.
insert-ast-with-values-to-with-rows
Starting at the 0.11 release, the InsertAst.withValues(assignments) method is removed. Extensions that build InsertAst directly via the AST layer must switch to InsertAst.withRows([assignments]).
Before 0.11:
const ast = InsertAst.into(TableSource.named(tableName))
.withValues(createAssignments.assignments)
.withOnConflict(onConflict);
Starting at 0.11:
const ast = InsertAst.into(TableSource.named(tableName))
.withRows([createAssignments.assignments])
.withOnConflict(onConflict);
The change is:
- Replace
.withValues(expr)with.withRows([expr])— the single-row overload is removed;withRowsaccepts an array of assignment maps.
Walk every .ts / .tsx file matched by the detection.glob above. For each call site matching .withValues(, apply the replacement. The argument remains a single expression; it just needs to be wrapped in an array.
Validation
After applying the rules above, run pnpm typecheck && pnpm test (or your extension's equivalent). prisma-8-check-pins should also pass — the pin set is unchanged for this transition; the breaking change is in the InsertAst builder method surface, not the dependency contract.
Validation by execution
These entries are prose-only (no scripts). The substrate diff on packages/3-extensions/ is additive (new methods on the three official facades) plus the Mongo behaviour change documented above; the namespace-kind-required-on-handcrafted-contract-literals entry covers a type-only tightening with no runtime substrate transform. There is no codemod to apply against a reverted substrate — the framework changes are the new surfaces, and these instructions describe the consumer-side translation, not a substrate transform.
The release-pipeline gate (pnpm check:upgrade-coverage) is satisfied by this directory existing with at least one entry. The substantive verification of the consumer-facing translation lives in the published skill's per-step bump-install-instructions-validate-commit loop, which runs in extension authors' own CI.