Skip to content

feat(runtime): decouple driver instantiation from binding#151

Merged
wmadden merged 32 commits intomainfrom
spec/tml-1837-runtime-dx-decouple-driver-instantiation
Feb 19, 2026
Merged

feat(runtime): decouple driver instantiation from binding#151
wmadden merged 32 commits intomainfrom
spec/tml-1837-runtime-dx-decouple-driver-instantiation

Conversation

@wmadden
Copy link
Contributor

@wmadden wmadden commented Feb 15, 2026

closes TML-1837

Goal / purpose

Make runtime stack instantiation deterministic and env-free by separating driver instantiation from connection binding. Drivers are created unbound as part of instantiateExecutionStack(), and only become usable after connect(binding) at the runtime boundary.

Before / After

// BEFORE (conceptual)
// driver was created with connection-ish options at the boundary
const stackInstance = instantiateExecutionStack(stack) // no driver instance
const binding = resolvePostgresBinding(options)
const driver = postgresDriverDescriptor.create({ connect: binding, cursor: options.cursor })
const runtime = createRuntime({ stackInstance, context, driver })
// AFTER
// stack instantiation includes an unbound driver; binding happens at connect()
const stackInstance = instantiateExecutionStack(stack) // includes driver (unbound)
const binding = resolvePostgresBinding(options)
await stackInstance.driver.connect(binding)
const runtime = createRuntime({ stackInstance, context, driver: stackInstance.driver })

What changed and why

  • RuntimeDriverDescriptor.create(options?) is now explicitly non-connection and supports driver-specific create options.
  • ExecutionStackInstance now includes driver, so instantiateExecutionStack() produces a complete stack instance deterministically.
  • SqlDriver.connect(binding) is now generic (TBinding), keeping the shared interface free of Postgres-specific types.
  • Postgres driver now supports an unbound → connect(binding) lifecycle, with use-before-connect failing fast and clearly.
  • Legacy Postgres runtime helper constructors were removed so descriptor + connect is the supported path.
  • postgres().runtime() is now async because it awaits driver.connect(binding).
  • Docs/ADRs updated (ADR 159 + subsystem/README updates) to codify terminology and lifecycle.

Notes / known failures

  • pnpm test:packages currently fails in @prisma-next/integration-kysely with Command \"prisma-next\" not found (appears pre-existing).
  • pnpm test:e2e currently fails with TypeError: runtime.close is not a function (see agent-os/specs/2026-02-15-runtime-dx-decouple-runtime-driver-instantiation-from-connection-binding/verifications/final-verification.html).

Summary by CodeRabbit

Release Notes

  • New Features

    • Driver descriptor pattern enables unbound driver creation followed by explicit binding via connect(binding) at runtime.
  • Bug Fixes

    • Improved driver lifecycle management for deterministic, environment-free initialization.
  • Refactor

    • Runtime initialization is now asynchronous; all runtime() calls must be awaited.
    • Driver binding occurs at connect time rather than instantiation.
  • Documentation

    • Added ADR 159 documenting driver terminology and two-state lifecycle.
    • Updated architecture guides with new binding flow and execution stack patterns.
  • Tests

    • Improved test readability with BDD-style grouping and explicit lifecycle assertions.

@linear
Copy link

linear bot commented Feb 15, 2026

TML-1837 Runtime DX: decouple runtime driver instantiation from connection binding

Summary

Today, runtime drivers (e.g. Postgres) require connection configuration at driver instantiation time (e.g. { connect: { pool | client }, cursor }). This makes RuntimeDriverDescriptor.create(options) inherently environment-bound and prevents instantiateExecutionStack() from meaningfully instantiating a “complete” stack when a driver descriptor is present.

This ticket proposes separating driver instantiation from connection binding so that connection information is provided only at connect time. This enables:

  • stack instantiation to be fully deterministic and env-free (descriptors → instances)
  • connection/pool/client wiring to happen at the boundary where env/runtime configuration is available
  • clearer DX: “instantiate stack” vs “connect driver” responsibilities

Motivation / Background

  • Runtime DX foundation (TML-1834) introduced ExecutionStack + ExecutionStackInstance + ExecutionContext. The driver is currently the odd component out because it cannot be instantiated without connection options.
  • We want a consistent model across stack components: instantiateExecutionStack() should be able to instantiate all components in the stack without requiring env access.
  • This also aligns with upcoming runtime DX work (TML-1831) where we want context construction and other wiring steps to be deterministic and testable.

Proposed direction (non-binding)

Introduce a runtime driver lifecycle where:

  • Driver descriptor create() produces an unbound driver instance (no connection info required)
  • Connection binding occurs only at connect time, e.g. driver.connect(connection) where connection can be { pool | client } (or a future abstract transport binding)

This likely requires one of:

  • API change: update SqlDriver.connect() to accept a connection binding (and store it internally)
  • or separate interface: keep SqlDriver minimal, introduce SqlDriverConnector/SqlConnectionProvider that runtime holds and passes into execution (preferred only if it keeps lane/runtime boundaries clean)

Scope

  • Define the new runtime driver connection binding model (types + lifecycle)
  • Update @prisma-next/sql-relational-core driver interface(s) accordingly
  • Update Postgres runtime driver implementation to match (no connection required in constructor)
  • Update runtime creation wiring so connection binding happens via connect-time config
  • Update examples and tests to reflect the new flow

Acceptance criteria

  • Runtime drivers can be instantiated without connection info.
  • Connection information is provided only at connect time.
  • Stack instantiation remains deterministic/env-free even when a driver descriptor is present.
  • Example demos still work with the new wiring.
  • Unit/integration tests cover both:
    • missing connection binding errors
    • successful connect + execution

Non-goals

  • Changing adapter/target descriptor patterns
  • Introducing new targets/drivers beyond Postgres (unless required for shared typing)

Notes

  • This is expected to be a breaking internal refactor; keep migration focused and update call sites directly (no long-lived shims).

@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

Important

Review skipped

This PR was authored by the user configured for CodeRabbit reviews. By default, CodeRabbit skips reviewing PRs authored by this user. It's recommended to use a dedicated user account to post CodeRabbit review feedback.

To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This pull request decouples runtime driver instantiation from connection binding by introducing an unbound driver creation phase and explicit connect-time binding. Drivers are created via descriptor factories without connection configuration, then bound using a new connect(binding) method before runtime instantiation, enabling environment-free driver setup and flexible binding strategies.

Changes

Cohort / File(s) Summary
Specification and Architecture Documentation
agent-os/specs/2026-02-15-runtime-dx-decouple-runtime-driver-instantiation-from-connection-binding/*, docs/architecture/adrs/ADR 159 - Driver Terminology and Lifecycle.md, docs/architecture/adrs/ADR 152 - ...md
Adds comprehensive spec and ADR documentation for the new driver lifecycle model: unbound creation via create(), binding via connect(binding), and ExecutionStack integration. Includes requirements, lifecycle diagrams, and migration planning.
Core Runtime Framework Types
packages/1-framework/1-core/runtime/execution-plane/src/types.ts, packages/1-framework/1-core/runtime/execution-plane/src/stack.ts
Updates RuntimeDriverDescriptor with optional TCreateOptions parameter; adds driver field to ExecutionStackInstance; instantiates unbound drivers and returns them in the execution stack structure.
SQL Driver Interface
packages/2-sql/4-lanes/relational-core/src/ast/driver-types.ts, packages/2-sql/4-lanes/relational-core/test/ast/driver-types.*
Introduces generic TBinding parameter to SqlDriver<TBinding> interface; updates connect(binding: TBinding) signature to accept binding configuration; adds test coverage for binding behavior.
SQL Runtime Type Updates
packages/2-sql/5-runtime/src/sql-context.ts, packages/2-sql/5-runtime/src/sql-runtime.ts
Erases binding type to unknown at shared runtime layer; updates driver descriptor type parameters and plugin/option signatures to use SqlDriver<unknown>.
PostgreSQL Driver Implementation
packages/3-targets/7-drivers/postgres/src/postgres-driver.ts, packages/3-targets/7-drivers/postgres/src/exports/runtime.ts, packages/3-targets/7-drivers/postgres/README.md
Introduces PostgresBinding union type and PostgresDriverCreateOptions; replaces factory functions with descriptor-based create() and connect(binding) pattern; implements PostgresUnboundDriverImpl and createBoundDriverFromBinding for lifecycle management.
PostgreSQL Client and Runtime
packages/3-targets/8-clients/postgres/src/runtime/postgres.ts, packages/3-targets/8-clients/postgres/src/runtime/binding.ts, packages/3-targets/8-clients/postgres/src/exports/runtime.ts
Updates PostgresClient.runtime() to return Promise<Runtime>; implements async driver connect flow; moves PostgresBinding export to driver package; adds cursor option propagation via PostgresDriverCreateOptions.
Runtime Initialization Functions
examples/prisma-next-demo/src/prisma-no-emit/runtime.ts, examples/prisma-orm-demo/src/prisma-next/runtime.ts, test/integration/test/utils.ts
Converts getRuntime/getPrismaNextRuntime/createTestRuntime functions to async; implements driver creation and explicit connect(binding) before runtime instantiation; adds IntegrationDriverOptions and binding resolution logic.
Example and Integration Tests
examples/*/src/main*.ts, examples/*/scripts/seed.ts, examples/*/test/*.ts, test/integration/test/*.ts, test/e2e/framework/test/utils.ts
Updates all runtime creation call sites to await async functions; changes runtime property types to Promise<Runtime> where applicable; implements driver connect patterns in test utilities.
PostgreSQL Driver Tests
packages/3-targets/7-drivers/postgres/test/driver.*.ts
Replaces createPostgresDriverFromOptions usage with postgresRuntimeDriverDescriptor.create(); updates connect calls to accept binding descriptors; adds comprehensive unbound/bound lifecycle tests covering pgPool, pgClient, and URL bindings.
Test Configuration and Tooling
.cursor/rules/test-intent-readability.mdc, packages/1-framework/3-tooling/cli/test/config-loader.test.ts, packages/3-extensions/integration-kysely/package.json, packages/3-targets/7-drivers/postgres/vitest.config.ts, .gitignore
Adds test readability guidelines; updates config loader tests with compilation timeouts; modifies emit script to build CLI before execution; adjusts coverage thresholds; adds spec artifact ignores.
Framework Documentation
packages/1-framework/1-core/runtime/execution-plane/README.md, docs/architecture/subsystems/4. Runtime & Plugin Framework.md, docs/reference/typescript-patterns.md
Updates runtime documentation to reflect unbound driver creation, boundary-time binding, and two-step lifecycle; revises code examples and ADR references.

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

@wmadden wmadden changed the base branch from main to tml-1890-runtime-dx-postgres-one-liner-lazy-client-prisma February 15, 2026 11:43
@wmadden wmadden force-pushed the spec/tml-1837-runtime-dx-decouple-driver-instantiation branch from af6f58c to 66b91ce Compare February 15, 2026 13:52
Copy link

@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: 8

Caution

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

⚠️ Outside diff range comments (4)
packages/3-targets/7-drivers/postgres/test/driver.basic.test.ts (1)

1-731: 🛠️ Refactor suggestion | 🟠 Major

Split this test file to keep it under 500 lines.

This file is well over the 500‑line limit; please split by logical describe blocks (e.g., driver.basic.streaming.test.ts, driver.basic.connections.test.ts, driver.basic.transactions.test.ts) so each file is self‑contained.

As per coding guidelines: "/*.test.ts: Keep test files under 500 lines to maintain readability and navigability. If a test file exceeds this limit, it should be split into multiple files."

packages/3-targets/7-drivers/postgres/src/postgres-driver.ts (1)

220-234: 🧹 Nitpick | 🔵 Trivial

Type mismatch: connect() signature accepts binding but ignores it.

The connect(_binding: PostgresBinding) method accepts a binding parameter but doesn't use it because the connection is already established in the constructor. This implements SqlDriver<PostgresBinding> but the actual binding happens during construction via createBoundDriverFromBinding.

This design is intentional per the PR's lifecycle, but consider documenting this in a comment or updating the interface to clarify that "bound" drivers have no-op connect methods.

📝 Optional: Add clarifying comment
-  async connect(_binding: PostgresBinding): Promise<void> {}
+  /** No-op: connection is established at construction time via createBoundDriverFromBinding */
+  async connect(_binding: PostgresBinding): Promise<void> {}
examples/prisma-orm-demo/src/prisma-next/runtime.ts (2)

20-72: ⚠️ Potential issue | 🟠 Major

Guard async singleton initialization and clean up on failure.

With async init, concurrent calls can create multiple clients/drivers, and any error before runtime is assigned leaves a Client allocated. Cache the in-flight promise and only publish globals after success; ensure cleanup in catch/finally.

🛠️ Proposed fix (promise guard + cleanup)
 let runtime: Runtime | undefined;
+let runtimePromise: Promise<Runtime> | undefined;
 let client: Client | undefined;
 
 export async function getPrismaNextRuntime(): Promise<Runtime> {
-  if (!runtime) {
-    const connectionString = process.env['DATABASE_URL'];
-    if (!connectionString) {
-      throw new Error('DATABASE_URL environment variable is required');
-    }
-
-    client = new Client({ connectionString });
-
-    const contract = validateContract<Contract>(contractJson);
-
-    const stack = createSqlExecutionStack({
-      target: postgresTarget,
-      adapter: postgresAdapter,
-      driver: postgresDriver,
-      extensionPacks: [],
-    });
-
-    const stackInstance = instantiateExecutionStack(stack);
-
-    const context = createExecutionContext({
-      contract,
-      stack,
-    });
-
-    const driverDescriptor = stack.driver;
-    if (!driverDescriptor) {
-      throw new Error('Driver descriptor missing from execution stack');
-    }
-
-    const driver = driverDescriptor.create({ cursor: { disabled: true } });
-    await driver.connect({ kind: 'pgClient', client });
-
-    runtime = createRuntime({
-      stackInstance,
-      context,
-      driver,
-      verify: {
-        mode: 'onFirstUse',
-        requireMarker: false,
-      },
-      plugins: [
-        budgets({
-          maxRows: 10_000,
-          defaultTableRows: 10_000,
-          tableRows: { user: 10_000 },
-          maxLatencyMs: 1_000,
-        }),
-      ],
-    });
-  }
-  return runtime;
+  if (runtime) {
+    return runtime;
+  }
+  if (!runtimePromise) {
+    runtimePromise = (async () => {
+      const connectionString = process.env['DATABASE_URL'];
+      if (!connectionString) {
+        throw new Error('DATABASE_URL environment variable is required');
+      }
+
+      const nextClient = new Client({ connectionString });
+
+      try {
+        const contract = validateContract<Contract>(contractJson);
+
+        const stack = createSqlExecutionStack({
+          target: postgresTarget,
+          adapter: postgresAdapter,
+          driver: postgresDriver,
+          extensionPacks: [],
+        });
+
+        const stackInstance = instantiateExecutionStack(stack);
+
+        const context = createExecutionContext({
+          contract,
+          stack,
+        });
+
+        const driverDescriptor = stack.driver;
+        if (!driverDescriptor) {
+          throw new Error('Driver descriptor missing from execution stack');
+        }
+
+        const driver = driverDescriptor.create({ cursor: { disabled: true } });
+        await driver.connect({ kind: 'pgClient', client: nextClient });
+
+        const nextRuntime = createRuntime({
+          stackInstance,
+          context,
+          driver,
+          verify: {
+            mode: 'onFirstUse',
+            requireMarker: false,
+          },
+          plugins: [
+            budgets({
+              maxRows: 10_000,
+              defaultTableRows: 10_000,
+              tableRows: { user: 10_000 },
+              maxLatencyMs: 1_000,
+            }),
+          ],
+        });
+
+        runtime = nextRuntime;
+        client = nextClient;
+        return nextRuntime;
+      } catch (error) {
+        await nextClient.end().catch(() => undefined);
+        throw error;
+      } finally {
+        runtimePromise = undefined;
+      }
+    })();
+  }
+  return runtimePromise;
 }

38-56: ⚠️ Potential issue | 🟠 Major

Use stackInstance.driver instead of creating a separate driver via stack.driver.

instantiateExecutionStack() now instantiates the driver as an unbound instance included in the returned stack instance. Remove the separate stack.driver.create() call and use stackInstance.driver directly. The driver options (like cursor) must be curried into the descriptor when creating the stack, not passed at instantiation time.

🔧 Proposed fix (use stackInstance.driver)
    const stackInstance = instantiateExecutionStack(stack);

    const context = createExecutionContext({
      contract,
      stack,
    });

-    const driverDescriptor = stack.driver;
-    if (!driverDescriptor) {
-      throw new Error('Driver descriptor missing from execution stack');
-    }
-
-    const driver = driverDescriptor.create({ cursor: { disabled: true } });
+    const driver = stackInstance.driver;
+    if (!driver) {
+      throw new Error('Relational runtime requires a driver descriptor on the execution stack');
+    }
+
     await driver.connect({ kind: 'pgClient', client });

Note: Driver-specific options like cursor: { disabled: true } must be configured by wrapping the driver descriptor's create() function when the stack is created, not at instantiation time.

Copy link

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

Caution

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

⚠️ Outside diff range comments (4)
examples/prisma-orm-demo/test/utils/control-client.ts (1)

116-118: 🧹 Nitpick | 🔵 Trivial

Consider type-safe pool state tracking.

The cast (pool as { ended?: boolean }).ended accesses an undocumented internal property of pg.Pool. While functional, this relies on implementation details that could change.

Alternative: track pool state with a wrapper or boolean flag if this pattern is used elsewhere.

examples/prisma-orm-demo/src/prisma-next/runtime.ts (2)

17-71: ⚠️ Potential issue | 🟠 Major

Prevent double initialization after making runtime async.

Because the function now awaits, two callers can enter before runtime is assigned, leading to duplicate clients/drivers and a lost runtime reference. Add a shared init promise (or similar lock) to guarantee single initialization.

🔧 Proposed fix (single-flight initialization)
 let runtime: Runtime | undefined;
+let runtimePromise: Promise<Runtime> | undefined;
 let client: Client | undefined;

 export async function getPrismaNextRuntime(): Promise<Runtime> {
-  if (!runtime) {
-    const connectionString = process.env['DATABASE_URL'];
-    if (!connectionString) {
-      throw new Error('DATABASE_URL environment variable is required');
-    }
-
-    client = new Client({ connectionString });
-
-    const contract = validateContract<Contract>(contractJson);
-
-    const stack = createSqlExecutionStack({
-      target: postgresTarget,
-      adapter: postgresAdapter,
-      driver: postgresDriver,
-      extensionPacks: [],
-    });
-
-    const stackInstance = instantiateExecutionStack(stack);
-
-    const context = createExecutionContext({
-      contract,
-      stack,
-    });
-
-    const driverDescriptor = stack.driver;
-    if (!driverDescriptor) {
-      throw new Error('Driver descriptor missing from execution stack');
-    }
-
-    const driver = driverDescriptor.create({ cursor: { disabled: true } });
-    await driver.connect({ kind: 'pgClient', client });
-
-    runtime = createRuntime({
-      stackInstance,
-      context,
-      driver,
-      verify: {
-        mode: 'onFirstUse',
-        requireMarker: false,
-      },
-      plugins: [
-        budgets({
-          maxRows: 10_000,
-          defaultTableRows: 10_000,
-          tableRows: { user: 10_000 },
-          maxLatencyMs: 1_000,
-        }),
-      ],
-    });
-  }
-  return runtime;
+  if (runtime) {
+    return runtime;
+  }
+  if (!runtimePromise) {
+    runtimePromise = (async () => {
+      const connectionString = process.env['DATABASE_URL'];
+      if (!connectionString) {
+        throw new Error('DATABASE_URL environment variable is required');
+      }
+
+      client = new Client({ connectionString });
+
+      const contract = validateContract<Contract>(contractJson);
+
+      const stack = createSqlExecutionStack({
+        target: postgresTarget,
+        adapter: postgresAdapter,
+        driver: postgresDriver,
+        extensionPacks: [],
+      });
+
+      const stackInstance = instantiateExecutionStack(stack);
+
+      const context = createExecutionContext({
+        contract,
+        stack,
+      });
+
+      const driverDescriptor = stack.driver;
+      if (!driverDescriptor) {
+        throw new Error('Driver descriptor missing from execution stack');
+      }
+
+      const driver = driverDescriptor.create({ cursor: { disabled: true } });
+      await driver.connect({ kind: 'pgClient', client });
+
+      runtime = createRuntime({
+        stackInstance,
+        context,
+        driver,
+        verify: {
+          mode: 'onFirstUse',
+          requireMarker: false,
+        },
+        plugins: [
+          budgets({
+            maxRows: 10_000,
+            defaultTableRows: 10_000,
+            tableRows: { user: 10_000 },
+            maxLatencyMs: 1_000,
+          }),
+        ],
+      });
+
+      return runtime;
+    })().finally(() => {
+      runtimePromise = undefined;
+    });
+  }
+  return runtimePromise;
 }

38-56: ⚠️ Potential issue | 🟠 Major

Use the driver instance from instantiation instead of creating a separate one.

instantiateExecutionStack() creates a driver instance that is stored in stackInstance.driver. Creating another via driverDescriptor.create() leaves the first driver unconnected and unused, causing a resource leak. The cursor options should be routed through the stack instantiation if needed, not applied to a second driver creation.

Fix: Use stackInstance.driver
 const stackInstance = instantiateExecutionStack(stack);

-const driverDescriptor = stack.driver;
-if (!driverDescriptor) {
-  throw new Error('Driver descriptor missing from execution stack');
-}
-
-const driver = driverDescriptor.create({ cursor: { disabled: true } });
+const driver = stackInstance.driver;
+if (!driver) {
+  throw new Error('Driver missing from execution stack instance');
+}
 await driver.connect({ kind: 'pgClient', client });
packages/3-targets/8-clients/postgres/src/runtime/postgres.ts (1)

153-177: ⚠️ Potential issue | 🟠 Major

Prevent concurrent runtime() calls from creating multiple drivers.
Because runtime() is now async, two callers can enter before runtimeInstance is set, leading to multiple instantiateExecutionStack() + driver.connect() calls and leaked pools. Cache the in-flight initialization promise to dedupe concurrent calls.

🔧 Suggested fix (cache initialization promise)
 let runtimeInstance: Runtime | undefined;
+let runtimePromise: Promise<Runtime> | undefined;

 return {
   sql,
   schema,
   orm,
   context,
   stack,
   async runtime() {
     if (runtimeInstance) {
       return runtimeInstance;
     }
+    if (runtimePromise) {
+      return runtimePromise;
+    }

-    const stackInstance = instantiateExecutionStack(stack);
-    const driver = stackInstance.driver;
-    if (driver === undefined) {
-      throw new Error('Relational runtime requires a driver descriptor on the execution stack');
-    }
-
-    await driver.connect(binding);
-
-    runtimeInstance = createRuntime({
-      stackInstance,
-      context,
-      driver,
-      verify: options.verify ?? { mode: 'onFirstUse', requireMarker: false },
-      ...(options.plugins ? { plugins: options.plugins } : {}),
-    });
-
-    return runtimeInstance;
+    runtimePromise = (async () => {
+      const stackInstance = instantiateExecutionStack(stack);
+      const driver = stackInstance.driver;
+      if (driver === undefined) {
+        throw new Error('Relational runtime requires a driver descriptor on the execution stack');
+      }
+
+      await driver.connect(binding);
+
+      runtimeInstance = createRuntime({
+        stackInstance,
+        context,
+        driver,
+        verify: options.verify ?? { mode: 'onFirstUse', requireMarker: false },
+        ...(options.plugins ? { plugins: options.plugins } : {}),
+      });
+
+      return runtimeInstance;
+    })();
+
+    try {
+      return await runtimePromise;
+    } catch (error) {
+      runtimePromise = undefined;
+      throw error;
+    }
   },
 };

Copy link

@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: 6

Caution

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

⚠️ Outside diff range comments (4)
test/integration/test/codecs.test.ts (1)

1-528: 🧹 Nitpick | 🔵 Trivial

Consider splitting this test file to comply with the 500-line limit.

This file has 528 lines, exceeding the 500-line guideline. Consider splitting by logical groupings such as:

  • codecs.timestamp.test.ts (timestamp encoding/decoding tests)
  • codecs.primitives.test.ts (numbers, strings)
  • codecs.nulls.test.ts (null handling)
  • codecs.annotations.test.ts (codec overrides and assignments)

As per coding guidelines: "Keep test files under 500 lines to maintain readability and navigability."

packages/3-targets/7-drivers/postgres/test/driver.basic.test.ts (1)

119-121: 🧹 Nitpick | 🔵 Trivial

Consider using object matcher for related assertions.

These assertions check related properties of the same array. Per coding guidelines, prefer toMatchObject or toEqual for checking multiple related values.

♻️ Optional: Use single assertion
-      expect(rows).toHaveLength(4);
-      expect(rows[0]).toEqual({ id: 1, name: 'a' });
-      expect(rows[3]).toEqual({ id: 4, name: 'd' });
+      expect(rows).toEqual([
+        { id: 1, name: 'a' },
+        { id: 2, name: 'b' },
+        { id: 3, name: 'c' },
+        { id: 4, name: 'd' },
+      ]);
examples/prisma-orm-demo/src/prisma-next/runtime.ts (2)

20-72: ⚠️ Potential issue | 🟠 Major

Guard async initialization to avoid multiple runtimes.

With the new await path, concurrent callers can all pass the !runtime check and initialize multiple Client/driver instances, leaking connections and returning different runtimes. Add an in‑flight promise/mutex so only one initialization runs and all callers await it.


27-56: ⚠️ Potential issue | 🟠 Major

Ensure client cleanup if connect/runtime creation fails.

If driver.connect(...) or createRuntime(...) throws, the module-level client remains allocated and the half-initialized state is retained. Use a local client and a try/catch to close on failure and only assign to the module-level client after success.

🧹 Proposed fix (local client + cleanup)
-    client = new Client({ connectionString });
+    const nextClient = new Client({ connectionString });
@@
-    const driver = driverDescriptor.create({ cursor: { disabled: true } });
-    await driver.connect({ kind: 'pgClient', client });
-
-    runtime = createRuntime({
-      stackInstance,
-      context,
-      driver,
-      verify: {
-        mode: 'onFirstUse',
-        requireMarker: false,
-      },
-      plugins: [
-        budgets({
-          maxRows: 10_000,
-          defaultTableRows: 10_000,
-          tableRows: { user: 10_000 },
-          maxLatencyMs: 1_000,
-        }),
-      ],
-    });
+    const driver = driverDescriptor.create({ cursor: { disabled: true } });
+    try {
+      await driver.connect({ kind: 'pgClient', client: nextClient });
+      runtime = createRuntime({
+        stackInstance,
+        context,
+        driver,
+        verify: {
+          mode: 'onFirstUse',
+          requireMarker: false,
+        },
+        plugins: [
+          budgets({
+            maxRows: 10_000,
+            defaultTableRows: 10_000,
+            tableRows: { user: 10_000 },
+            maxLatencyMs: 1_000,
+          }),
+        ],
+      });
+      client = nextClient;
+    } catch (err) {
+      await nextClient.end().catch(() => undefined);
+      throw err;
+    }

Comment on lines +28 to +44
```
Group 1 (Core interfaces) ─────────────────────────────────┐
Group 2 (SqlDriver interface) ◄──────────────────────────────┤
Group 3 (Execution stack) ◄─────────────────────────────────┤
Group 4 (Postgres driver) ◄──────────────────────────────────┘
Group 5 (Runtime wiring) ◄──────────────────────────────────┘
Group 6 (Legacy cleanup) ◄──────────────────────────────────┘
Group 7 (Tests — integration) ◄──────────────────────────────┘
Group 8 (Docs/ADR) ◄──────────────────────────────────────────┘
```
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add language specifier to fenced code block.

The dependency diagram code block at line 28 is missing a language specifier. While this is a planning document, adding text or plaintext improves rendering consistency.

📝 Suggested fix
-```
+```text
 Group 1 (Core interfaces) ─────────────────────────────────┐
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 28-28: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

Base automatically changed from tml-1890-runtime-dx-postgres-one-liner-lazy-client-prisma to main February 16, 2026 08:56
@wmadden wmadden force-pushed the spec/tml-1837-runtime-dx-decouple-driver-instantiation branch from 3c9a601 to 6fbbcb8 Compare February 16, 2026 14:13
Copy link

@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 (2)
examples/prisma-orm-demo/src/prisma-next/runtime.ts (1)

45-51: ⚠️ Potential issue | 🟡 Minor

Redundant driver creation—use stackInstance.driver instead of manually calling driverDescriptor.create().

instantiateExecutionStack() already calls stack.driver.create() internally and returns the driver instance in stackInstance.driver. Creating another driver via driverDescriptor.create() results in two separate driver instances.

Proposed fix
   const stackInstance = instantiateExecutionStack(stack);
-
-  const context = createExecutionContext({
-    contract,
-    stack,
-  });
-
-  const driverDescriptor = stack.driver;
-  if (!driverDescriptor) {
+  const driver = stackInstance.driver;
+  if (!driver) {
     throw new Error('Driver descriptor missing from execution stack');
   }

-  const driver = driverDescriptor.create({ cursor: { disabled: true } });
   await driver.connect({ kind: 'pgClient', client });

+  const context = createExecutionContext({
+    contract,
+    stack,
+  });

If cursor options are required, configure them in the stack definition rather than creating a separate driver instance.

packages/3-targets/7-drivers/postgres/test/driver.basic.test.ts (1)

119-121: 🧹 Nitpick | 🔵 Trivial

Consider using object matcher for related assertions.

Per coding guidelines, prefer toMatchObject over multiple individual expect().toBe() calls when checking 2 or more related values.

♻️ Optional: Use toMatchObject
-      expect(rows).toHaveLength(4);
-      expect(rows[0]).toEqual({ id: 1, name: 'a' });
-      expect(rows[3]).toEqual({ id: 4, name: 'd' });
+      expect(rows).toHaveLength(4);
+      expect(rows).toMatchObject([
+        { id: 1, name: 'a' },
+        expect.anything(),
+        expect.anything(),
+        { id: 4, name: 'd' },
+      ]);

Alternatively, just verify the full array:

-      expect(rows).toHaveLength(4);
-      expect(rows[0]).toEqual({ id: 1, name: 'a' });
-      expect(rows[3]).toEqual({ id: 4, name: 'd' });
+      expect(rows).toEqual([
+        { id: 1, name: 'a' },
+        { id: 2, name: 'b' },
+        { id: 3, name: 'c' },
+        { id: 4, name: 'd' },
+      ]);

@wmadden wmadden force-pushed the spec/tml-1837-runtime-dx-decouple-driver-instantiation branch from 6fbbcb8 to d29d78e Compare February 16, 2026 14:52
Copy link

@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: 5

Caution

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

⚠️ Outside diff range comments (4)
examples/prisma-orm-demo/src/prisma-next/runtime.ts (2)

20-28: ⚠️ Potential issue | 🟠 Major

Guard async singleton init to prevent double runtime/client creation.

Asynchronous initialization can race if multiple callers hit this concurrently, creating multiple clients and possibly leaking the first one. Add an in-flight promise gate.

✅ Proposed fix
-let runtime: Runtime | undefined;
-let client: Client | undefined;
+let runtime: Runtime | undefined;
+let runtimePromise: Promise<Runtime> | undefined;
+let client: Client | undefined;

 export async function getPrismaNextRuntime(): Promise<Runtime> {
-  if (!runtime) {
+  if (runtime) {
+    return runtime;
+  }
+  if (runtimePromise) {
+    return runtimePromise;
+  }
+  runtimePromise = (async () => {
     const connectionString = process.env['DATABASE_URL'];
     if (!connectionString) {
       throw new Error('DATABASE_URL environment variable is required');
     }
 
     client = new Client({ connectionString });
@@
     runtime = createRuntime({
       stackInstance,
       context,
       driver,
@@
     });
-  }
-  return runtime;
+    return runtime;
+  })();
+  try {
+    return await runtimePromise;
+  } catch (error) {
+    runtimePromise = undefined;
+    throw error;
+  }
 }

45-56: ⚠️ Potential issue | 🟠 Major

Reuse stackInstance.driver instead of creating a second driver instance.

instantiateExecutionStack already materializes the driver descriptor (line 132 of stack.ts), storing it as stackInstance.driver. Creating another driver from stack.driver wastes the first instance and introduces configuration divergence. Replace lines 45–50 to use stackInstance.driver directly, matching the pattern in examples/prisma-next-demo/src/prisma-no-emit/runtime.ts and packages/3-targets/8-clients/postgres/src/runtime/postgres.ts.

examples/prisma-next-demo/src/main-no-emit.ts (1)

52-91: ⚠️ Potential issue | 🟠 Major

Handle runtime initialization failures before command dispatch.

getRuntime now rejects asynchronously but is awaited before the try block, so init errors bypass your error handling and runtime.close() isn’t safely guarded. Move initialization into the try and only close when defined.

🔧 Suggested fix
 async function main() {
   const { databaseUrl } = loadAppConfig();
-  const runtime = await getRuntime(databaseUrl);
-  try {
+  let runtime: Awaited<ReturnType<typeof getRuntime>> | undefined;
+  try {
+    runtime = await getRuntime(databaseUrl);
     if (cmd === 'users') {
       const limit = args[0] ? Number.parseInt(args[0], 10) : 10;
       const users = await getUsers(runtime, limit);
       console.log(JSON.stringify(users, null, 2));
@@
   } catch (error) {
     console.error('Error:', error);
     process.exit(1);
   } finally {
-    await runtime.close();
+    if (runtime) {
+      await runtime.close();
+    }
   }
 }
packages/3-targets/7-drivers/postgres/src/postgres-driver.ts (1)

29-46: 🧹 Nitpick | 🔵 Trivial

Extract shared cursor options type to avoid drift.
Both driver option types define the same cursor shape; a shared type keeps them in sync.

♻️ Suggested refactor
+type PostgresCursorOptions = {
+  readonly batchSize?: number;
+  readonly disabled?: boolean;
+};
+
 export interface PostgresDriverOptions {
   readonly connect: { client: Client } | { pool: PoolType };
-  readonly cursor?:
-    | {
-        readonly batchSize?: number;
-        readonly disabled?: boolean;
-      }
-    | undefined;
+  readonly cursor?: PostgresCursorOptions | undefined;
 }
 
 export interface PostgresDriverCreateOptions {
-  readonly cursor?:
-    | {
-        readonly batchSize?: number;
-        readonly disabled?: boolean;
-      }
-    | undefined;
+  readonly cursor?: PostgresCursorOptions | undefined;
 }

Copy link
Contributor

@jkomyno jkomyno left a comment

Choose a reason for hiding this comment

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

Multi-agent code review — 9 non-blocking inline findings (P2/P3). The P1 race condition in runtime() memoization is tracked separately.

Copy link

@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: 2

Caution

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

⚠️ Outside diff range comments (2)
examples/prisma-next-demo/test/runtime.integration.test.ts (2)

491-509: 🧹 Nitpick | 🔵 Trivial

Consolidate assertions using object matchers.

Multiple individual toHaveProperty and typeof checks can be combined into toMatchObject assertions for clearer, more maintainable tests.

♻️ Proposed refactor using object matchers
 expect(rows).toHaveLength(2);
-expect(rows[0]).toHaveProperty('id');
-expect(rows[0]).toHaveProperty('email');
-expect(rows[0]).toHaveProperty('posts');
-expect(Array.isArray(rows[0]!.posts)).toBe(true);
+expect(rows[0]).toMatchObject({
+  id: expect.any(String),
+  email: expect.any(String),
+  posts: expect.any(Array),
+});

 const alice = rows.find((r) => r.email === 'alice@example.com');
 expect(alice).toBeDefined();
 expect(alice!.posts).toHaveLength(2);
-expect(alice!.posts[0]).toHaveProperty('id');
-expect(alice!.posts[0]).toHaveProperty('title');
-expect(alice!.posts[0]).toHaveProperty('createdAt');
-expect(typeof alice!.posts[0]!.id).toBe('string');
-expect(typeof alice!.posts[0]!.title).toBe('string');
+expect(alice!.posts[0]).toMatchObject({
+  id: expect.any(String),
+  title: expect.any(String),
+  createdAt: expect.any(Date),
+});

 const bob = rows.find((r) => r.email === 'bob@example.com');
 expect(bob).toBeDefined();
-expect(bob!.posts).toHaveLength(1);
-expect(bob!.posts[0]!.title).toBe('Third Post');
+expect(bob!.posts).toMatchObject([
+  { title: 'Third Post' },
+]);

As per coding guidelines: "Prefer object matchers (toMatchObject) over multiple individual expect().toBe() calls when checking 2 or more related values in tests"


124-517: 🧹 Nitpick | 🔵 Trivial

Consider splitting test file to stay under 500 lines.

The file is 517 lines, slightly exceeding the 500-line guideline. Consider splitting into logical groups:

  • runtime.basic.integration.test.ts - streaming, type inference, includeMany tests
  • runtime.budgets.integration.test.ts - budget enforcement tests

As per coding guidelines: "Keep test files under 500 lines to maintain readability and navigability."

Copy link

@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: 1

@wmadden wmadden force-pushed the spec/tml-1837-runtime-dx-decouple-driver-instantiation branch from 5fe4b6d to b692d16 Compare February 18, 2026 05:59
Copy link
Contributor

@jkomyno jkomyno left a comment

Choose a reason for hiding this comment

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

@wmadden — thorough review of the PR. The architecture is sound, plane/layer/domain boundaries are clean, and the two-phase driver lifecycle is well-designed. ADR 159 and the test coverage are excellent. A few things need addressing before merge.


Blocking

P1-1: Resource leak on failed runtime initialization

packages/3-targets/8-clients/postgres/src/runtime/postgres.ts:159-177

When driver.connect(binding) succeeds but createRuntime() fails (e.g., codec validation on verify: 'startup'), the promise resets via .catch() and the next runtime() call creates a fresh driver via instantiateExecutionStack. The previous driver — already connected with a live pool — is never closed.

Each retry on a URL binding allocates a new pg.Pool (default 10 connections). In flapping environments, this can exhaust max_connections.

Fix: wrap the post-connect code in try/catch:

runtimePromise = (async () => {
  const stackInstance = instantiateExecutionStack(stack);
  const driver = stackInstance.driver;
  if (driver === undefined) {
    throw new Error('Relational runtime requires a driver descriptor on the execution stack');
  }
  try {
    await driver.connect(binding);
    return createRuntime({ stackInstance, context, driver, ... });
  } catch (e) {
    await driver.close().catch(() => {});
    throw e;
  }
})();

P1-2: Driver lifecycle errors use plain Error — no structured codes

packages/3-targets/7-drivers/postgres/src/exports/runtime.ts:22-27, packages/3-targets/8-clients/postgres/src/runtime/binding.ts:25-49

All driver lifecycle errors ('Postgres driver not connected...', 'already connected...', binding validation) are plain Error objects with string messages. The rest of the codebase uses RuntimeErrorEnvelope with code/category/details and SqlQueryError with kind/sqlState. The driver layer — the first thing consumers hit during setup — is the odd one out.

This contradicts the project's "agent-accessible" design goal and forces string-matching for programmatic error handling.

Fix: use runtimeError() or a parallel driverError() with codes like DRIVER.NOT_CONNECTED, DRIVER.ALREADY_CONNECTED, DRIVER.BINDING_INVALID.

P2-1: TOCTOU race in PostgresUnboundDriverImpl state transitions

packages/3-targets/7-drivers/postgres/src/exports/runtime.ts:29-101

Methods check #delegate === null then use #delegate. If close() nullifies #delegate between check and use (concurrent callers), queries go to a closed pool producing confusing errors.

Fix: capture #delegate in a local variable at method entry. Extract a #requireDelegate() helper:

#requireDelegate(): SqlDriver<PostgresBinding> {
  const delegate = this.#delegate;
  if (delegate === null) throw new Error(USE_BEFORE_CONNECT_MESSAGE);
  return delegate;
}

P2-2: Reconnection after close() is implicitly supported but untested

runtime.ts:40-45, 54-58

close() resets #delegate = null, so a subsequent connect() succeeds. The error message says "Call close() before reconnecting" — implying reconnection is supported. But no test covers close-then-reconnect. Either add a test or prevent reconnection with a permanent #closed flag.

P2-3: Direct client acquireConnection() concurrency (existing TODO)

postgres-driver.ts:279

The // TODO: This might need to be protected with a mutex is a real concern. Multiple concurrent acquireConnection() calls on a Client-based driver share the same underlying client, leading to nested transaction errors. Promote to a tracked issue or add a mutex.

P2-4: Reliance on undocumented pg internals

postgres-driver.ts:246, 285-300

pool.ended, client._ending, client._connection are undocumented pg internals. These can break on any pg version bump. Maintain explicit internal state flags (e.g., #closed) instead.

P2-5: Driver state not introspectable

SqlDriver interface at driver-types.ts:16-20

ADR 159 defines a clear state machine (Unbound → Connected → Closed) but there's no way to query the current state. An isConnected getter or state property would be a low-effort improvement.


Nits (non-blocking)

  • Credential leakage in runtime error paths (postgres-driver.ts:326-335): The control-plane driver uses redactDatabaseUrl() but the runtime-plane does not. Connection failure errors from pg may include the connection string.

  • Inconsistent driver creation in integration test utils (test/integration/test/utils.ts:69-84): instantiateExecutionStack() creates a driver on L69, then a separate driver is created manually on L84. The first is discarded. Use the descriptor-wrapping pattern consistently.

  • EXPLAIN SQL concatenation (postgres-driver.ts:104): EXPLAIN (FORMAT JSON) ${request.sql} uses string interpolation. Currently safe (DSL-generated SQL), but the public API surface could be misused. A comment or validation would help.

  • PostgresDriverOptions appears to be dead code (postgres-driver.ts:29-37): Only used by internal constructors, never imported externally after the refactor. Remove export or inline.

  • Duplicate cursor type shape written out 3 times across postgres-driver.ts. Extract a shared CursorOptions type.

  • execute() on unbound driver (runtime.ts:63-77): The manual AsyncIterable with return() and throw() is 15 lines of ceremony. An async function* generator that throws would be 3 lines.

  • EXPLAIN_NOT_SUPPORTED_MESSAGE guard (runtime.ts:86-88): Unreachable — createBoundDriverFromBinding only returns drivers that inherit explain from PostgresQueryable.

  • Git history: Consider squashing the 64 commits before merge. Only 5 are feat, 21 are fix (touching the same files), and 7+ are spec-artifact cleanup with zero net diff. The fix-to-feat ratio of 4:1 suggests edge cases were discovered iteratively rather than designed upfront.


Overall the design is well-motivated. The separation of instantiation from binding is clean, the discriminated unions are precise, and the promise memoization with retry is correct. Looking forward to seeing the P1/P2 items addressed.

@wmadden
Copy link
Contributor Author

wmadden commented Feb 19, 2026

Thanks @jkomyno — I addressed the blocking P1/P2 feedback and pushed the changes.

Summary of what changed:

  • P1-1 (resource leak on failed runtime initialization)

    • Postgres client runtime initialization now closes the connected driver if any step in connect/runtime creation fails, so retries cannot leak pools.
    • Added a regression test covering createRuntime failure + clean retry.
  • P1-2 (plain Error for lifecycle/binding failures)

    • Binding validation and unbound-driver lifecycle failures now throw structured errors with stable codes (e.g. DRIVER.BINDING_INVALID, DRIVER.NOT_CONNECTED, DRIVER.ALREADY_CONNECTED) and category RUNTIME.
    • Updated tests to assert on structured error fields rather than message strings.
  • P2-1 (TOCTOU race on delegate access)

    • Unbound driver now captures the delegate at method entry (requireDelegate helper) to avoid checking then using a different value.
  • P2-2 (reconnect semantics after close)

    • Added explicit close-state tracking and a test that exercises connect -> close -> reconnect.
  • P2-3 (direct client acquireConnection concurrency)

    • Serialized PostgresDirectDriverImpl.acquireConnection using a mutex-style lease so concurrent callers cannot share a single client connection.
    • Added a targeted concurrency test.
  • P2-4 (reliance on pg internals)

    • Removed use of pool.ended / client._ending / client._connection and replaced with explicit closed/connected state and connect serialization.
    • Updated error/lifecycle tests to match the new semantics.
  • P2-5 (driver state introspection)

    • Added an optional SqlDriver.state (unbound/connected/closed) and implemented it for the postgres drivers.
  • Nit: integration helper consistency

    • Integration runtime helper now reuses stackInstance.driver (instead of creating a second driver instance) and curries cursor options into the driver descriptor when building the stack.

Commits:

  • 0d0547c fix(postgres-client): close driver when runtime init fails
  • 698d0b4 fix(driver-postgres): add structured lifecycle errors and safe delegate access
  • 82ed93f fix(driver-postgres): serialize direct client connections and track closed state
  • 8255a75 refactor(integration-tests): use instantiated stack driver in runtime helper

Verification:

  • pnpm --filter @prisma-next/postgres test && pnpm --filter @prisma-next/postgres typecheck
  • pnpm --filter @prisma-next/driver-postgres test && pnpm --filter @prisma-next/driver-postgres typecheck
  • pnpm --filter @prisma-next/integration-tests typecheck

@wmadden wmadden requested a review from jkomyno February 19, 2026 07:19
Capture the shaped requirements and full specification for decoupling driver instantiation from connection binding, including fail-fast semantics, typed driver-determined binding, and removal of legacy helper paths.
…tion binding

- Add TCreateOptions param to RuntimeDriverDescriptor (default void)
- Change create(options?: TCreateOptions) for optional, connection-free instantiation
- Add readonly driver to ExecutionStackInstance
- Update instantiateExecutionStack to call stack.driver.create() when present
- Add unit and type tests for driver-in / driver-out behavior
- SqlDriver<TBinding> interface: connect(binding: TBinding) instead of connect()
- Default TBinding = void for drivers without binding
- Update mock driver and Postgres driver implementers
…1837 Group 4)

- Add PostgresBinding type (url | pgPool | pgClient)
- Add PostgresUnboundDriverImpl with connect(binding) lifecycle
- postgresRuntimeDriverDescriptor.create() returns unbound driver, no connection args
- Use-before-connect fails with clear error
- Update SqlDriver<PostgresBinding> for pool/direct impls
- Add driver.unbound.test.ts for descriptor create + connect path
- Update existing tests to pass binding to connect()
Group 5 (runtime-wiring): Use stackInstance.driver from instantiateExecutionStack,
call connect(binding) before createRuntime, assert driver exists. Make runtime()
async since connect is async. Update examples to await db.runtime().
…ecycle

- prisma-no-emit: make getRuntime async, use create() + connect(binding)
- prisma-orm-demo: make getPrismaNextRuntime and createTestRuntime async, use connect(binding)
- integration test utils: make createTestRuntime/createTestRuntimeFromClient async, call driver.connect() before createRuntime
- update all integration test call sites to await createTestRuntime
Relax runtime driver typing from SqlDriver<void> to SqlDriver<unknown> so clients can use connect(binding) with driver-defined binding types.
…path

Drop PostgresDriverOptions from runtime exports and migrate integration test helpers to a local options shape so the descriptor + connect lifecycle remains the primary API signal.
…ckage

Use a single canonical PostgresBinding type across client and driver runtime surfaces to reduce drift risk, and remove a duplicate type import in the postgres runtime entrypoint.
Use the TypeScript compilation timeout budget for async config-loader error-path tests so full-suite runs stay reliable under variable I/O and module load timing.
Use the driver created by instantiateExecutionStack() instead of creating a second runtime driver instance in the no-emit runtime setup.
Ensure test runtime setup cleans up created pg pools when driver.connect throws so failures do not leak resources or hang test runs.
Implement return() and throw() on the pre-connect execute iterator so it behaves like a full AsyncIterator while preserving use-before-connect errors.
Add focused mock-based tests for cursor read failure normalization and bound-driver lifecycle edge paths so postgres-driver branch/function coverage meets threshold without loosening limits.
…unctions

Set branch and function coverage thresholds to 95% in driver-postgres so enforcement reflects current quality targets while remaining strict.
…failures

Use stackInstance.driver in ORM runtime setup and ensure pg pools are closed if driver.connect fails so demo/test helpers do not leak connections.
…setup

Ensure seed runtime teardown uses closeTestRuntime so both runtime and pool are closed, preventing leaked connections across integration runs.
…re handling

Use SqlDriver<unknown> consistently across sql-runtime interfaces, ensure integration test helper closes pool on connect failure, and update README example to await collected execute results.
…rrent calls

Use a promise singleton in postgres runtime initialization so concurrent runtime() callers share one connect/createRuntime flow, and reset the memoized promise on failure so later calls can retry cleanly.
- fix(demo): Driver descriptor missing → Driver missing error message
- test(cli): remove duplicate file-not-found test in config-loader
- refactor(driver-postgres): remove redundant explain() check; add connect() comments
- refactor(postgres): use ifDefined for conditional plugins spread
Switch the postgres client package to root export plus main/module/types metadata and align the test pg Pool mock with current constructor usage.
… options

Make PostgresDriverOptions internal and derive the exported PostgresDriverCreateOptions with Omit so cursor option shape cannot drift between constructor and create API.
…me helper

Remove the legacy connect-shape adapter in integration test runtime utilities and update call sites to pass PostgresBinding explicitly so helper inputs match the runtime API contract.
If driver.connect succeeds but createRuntime throws, close the connected driver before rejecting so retries cannot leak pools. Also emit structured binding validation errors so callers can handle setup failures without string matching.
…te access

Emit structured driver lifecycle errors with stable codes, and avoid TOCTOU on delegate access by capturing the bound driver at method entry. Also expose an optional driver state property and cover close-then-reconnect behavior.
…losed state

Prevent concurrent acquireConnection calls from sharing a direct client by serializing leases, and replace reliance on pg internal flags with explicit closed/connected state. Update tests to match the new semantics.
… helper

Avoid discarding the driver created during stack instantiation by reusing stackInstance.driver, and curry cursor options into the driver descriptor at stack creation time.
@wmadden wmadden force-pushed the spec/tml-1837-runtime-dx-decouple-driver-instantiation branch from a4bae38 to 864c9a9 Compare February 19, 2026 08:05
@wmadden
Copy link
Contributor Author

wmadden commented Feb 19, 2026

Performed the targeted squash pass after rebasing onto origin/main.

What was done:

  • Rebasing: branch rebased cleanly onto latest origin/main (with conflict resolution during replay).
  • History cleanup: post-main history rewritten from 70 commits to 31 commits.
  • Strategy: preserved semantic milestones (feature/refactor/test intent commits) and folded iterative/noisy follow-ups (lint-only, checkoff/spec-artifact cleanup, micro-fixups) into their nearest logical parent commits.

Validation after rewrite:

  • pnpm --filter @prisma-next/postgres test
  • pnpm --filter @prisma-next/postgres typecheck
  • pnpm --filter @prisma-next/driver-postgres test
  • pnpm --filter @prisma-next/driver-postgres typecheck
  • pnpm --filter @prisma-next/integration-tests typecheck

Force-pushed with lease: spec/tml-1837-runtime-dx-decouple-driver-instantiation now points to the rewritten, squashed history.

…branches

Prevent flaky coverage failures in adapter-postgres by using explicit database operation timeouts on slower tests, and add direct-client branch tests in driver-postgres so branch thresholds are met reliably.
@wmadden wmadden merged commit 4e733ba into main Feb 19, 2026
11 checks passed
@wmadden wmadden deleted the spec/tml-1837-runtime-dx-decouple-driver-instantiation branch February 19, 2026 10:58
saevarb added a commit that referenced this pull request Feb 19, 2026
Adapt sql-dml.arrays.test.ts to the driver refactoring from PR #151:
use createTestRuntimeFromClient (async) instead of createTestRuntime
with the old { connect: { client } } shape.

Co-authored-by: Cursor <cursoragent@cursor.com>
saevarb added a commit that referenced this pull request Feb 23, 2026
Adapt sql-dml.arrays.test.ts to the driver refactoring from PR #151:
use createTestRuntimeFromClient (async) instead of createTestRuntime
with the old { connect: { client } } shape.

Co-authored-by: Cursor <cursoragent@cursor.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.

2 participants