Skip to content

feat(postgres,sqlite): add typed temp table creation in transactions#836

Open
paulwer wants to merge 1 commit into
prisma:mainfrom
paulwer:feat-temp-tables
Open

feat(postgres,sqlite): add typed temp table creation in transactions#836
paulwer wants to merge 1 commit into
prisma:mainfrom
paulwer:feat-temp-tables

Conversation

@paulwer

@paulwer paulwer commented Jun 15, 2026

Copy link
Copy Markdown

Signed-off-by: paulwer paul@wer-ner.de

Linked issue

https://discord.com/channels/937751382725886062/1514991788455104672

Summary

Adds a first-class, type-safe tempTable() API to the transaction context of both the Postgres and SQLite runtime extensions.

The API surface on the transaction context:

tx.tempTable(): TempTableBuilder

The builder exposes two creation modes:

  • as(query) — derives the table schema and initial data from any typed query source (SQL builder query or ORM collection). The returned handle is parameterised over the inferred row shape.
  • from(columns) — creates an empty table with an explicitly specified column list. Data is loaded separately via append().

The returned TempTableHandle<Row> is a first-class join source (composable with innerJoin, leftJoin, and FROM). It also exposes:

  • append(input) — appends rows after creation; accepts either a typed query source or raw scalar rows ((string | number | boolean | null)[][]). Passing an empty array is a no-op.
  • drop() / await using — explicit or automatic cleanup.
  • name and fields — resolved table name and typed column metadata.

Cleanup strategy is target-specific: Postgres emits ON COMMIT DROP; SQLite registers a pre-commit hook that issues DROP TABLE IF EXISTS before the COMMIT.

Testing performed

  • pnpm --filter @prisma-next/postgres test — 86 passed (88 total; 2 pre-existing failures unrelated to this PR)
  • pnpm --filter @prisma-next/sqlite test — 42/42 passed
  • Coverage includes: SQL generation, type-level tests (test-d.ts), temp-table reuse in join composition, from() + append() round-trips, literal inlining (NULL, booleans, numbers, single-quote escaping), pre-commit hook cleanup, and await using disposal.

Skill update

n/a — no CLI commands, flags, config fields, or glossary terms were changed.

Checklist

  • All commits are signed off (git commit -s) per the DCO.
  • I read CONTRIBUTING.md and the change is scoped to one logical concern.
  • Tests are updated.
  • The PR title follows the TML-NNNN: convention.
  • Skill update section is filled in.

Notes for the reviewer

  • The API is intentionally transaction-only. Outside of a transaction, the connection pool does not guarantee the same session across sequential calls, so a temp table created on one pool member would not be visible to queries executed on another.
  • from() is columns-only by design — initial data is always loaded via append(). This keeps the creation step lightweight and makes multi-batch loading explicit.
  • append() with a typed query source is type-checked against Row; raw scalar rows are not (mapping SQL column types to TypeScript types at the API boundary is not feasible without a code-generation step).
  • The returned handle is a stable join source: buildAst() always returns TableSource.named(tableName), so the same handle can be passed to multiple joins in the same transaction without re-evaluating the source query.
  • No asRaw(string) API — raw SQL strings are not accepted as a source to preserve typed field-metadata for downstream join composition.

@paulwer
paulwer requested a review from a team as a code owner June 15, 2026 21:52
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a tempTable() method to both PostgresTransactionContext and SqliteTransactionContext that creates transaction-scoped temporary tables from subqueries. The method accepts an optional name or options object, validates or auto-generates a safe table name, lowers the subquery AST to SQL via the runtime adapter, executes CREATE TEMP TABLE ... AS ..., and returns a handle supporting field access and async cleanup. ORM collections are bridged to this API via an internal symbol contract. Runtime, type-level, and end-to-end tests validate both runtimes.

Changes

Transaction tempTable() API — Postgres and SQLite

Layer / File(s) Summary
Internal temp-table query source contract and ORM bridge
packages/3-extensions/sql-orm-client/src/internal-temp-table-source.ts, packages/3-extensions/sql-orm-client/src/collection.ts, packages/3-extensions/sql-orm-client/src/exports/index.ts, packages/3-extensions/sql-orm-client/test/collection.as-subquery.test.ts
Defines INTERNAL_TO_TEMP_TABLE_QUERY_SOURCE symbol and InternalTempTableQuerySource type contract. Implements the symbol method on Collection to expose AST and row-field metadata, re-exports the symbol, and validates the bridge with collection-specific tests.
Public types and TransactionContext interface extension
packages/3-extensions/postgres/src/runtime/postgres.ts, packages/3-extensions/sqlite/src/runtime/sqlite.ts
Exports TempTableCreateOptions, TempTableHandle, and TempTableBuilder interfaces for both runtimes, and extends PostgresTransactionContext and SqliteTransactionContext with the tempTable(options?) method signature.
Runtime import updates for SQL builder and planning
packages/3-extensions/postgres/src/runtime/postgres.ts, packages/3-extensions/sqlite/src/runtime/sqlite.ts
Adds imports for relational-core AST utilities, scope typing, planning functions, and adapter typing required for lowering subquery ASTs and executing temp-table DDL.
Core builder implementation and execution
packages/3-extensions/postgres/src/runtime/postgres.ts, packages/3-extensions/sqlite/src/runtime/sqlite.ts
Implements identifier quoting, name validation (regex and length checks) with auto-generated pn_temp_... fallback, createTempTableBuilder: plans and lowers a subquery, rejects non-literal bind parameters, executes CREATE TEMP TABLE ... AS ..., and returns a TempTableHandle with field metadata and drop()/async-dispose cleanup. Refactors execution-stack instantiation to enable builder access.
Transaction context binding
packages/3-extensions/postgres/src/runtime/postgres.ts, packages/3-extensions/sqlite/src/runtime/sqlite.ts
Attaches tempTable(...) to the transaction context object returned by postgres().transaction() and sqlite().transaction(), delegating to the builder with the active context, contract, and adapter.
Runtime integration tests
packages/3-extensions/postgres/test/postgres.test.ts, packages/3-extensions/sqlite/test/transaction.test.ts
Tests for auto-generated and explicit temp-table names, SQL statement assertion (CREATE TEMP TABLE ... AS, DROP TABLE IF EXISTS), field/codec metadata validation, and same-transaction temp-table reuse in subsequent planned statements.
Type-level contract tests
packages/3-extensions/postgres/test/transaction.types.test-d.ts, packages/3-extensions/sqlite/test/transaction.types.test-d.ts
Vitest type assertions verifying tempTable presence on both transaction context types, awaited handle shape (name: string, fields: Record<string, { codecId, nullable }>, drop(): Promise<void>, [Symbol.asyncDispose]()), and acceptance of internally-convertible inputs via INTERNAL_TO_TEMP_TABLE_QUERY_SOURCE.
End-to-end temp-table reuse flows
test/e2e/framework/test/sqlite/transaction.test.ts, test/e2e/framework/test/transaction-orm.test.ts
Integration tests validating temp-table materialization from tx.sql subqueries and ORM collections, projection planning from temp-table AST, join reuse within the same transaction, and explicit cleanup.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • prisma/prisma-next#737: Adds the db.transaction() / SqliteTransactionContext facade in packages/3-extensions/sqlite/src/runtime/sqlite.ts — the same transaction context that this PR extends with tempTable().

Suggested reviewers

  • aqrln
  • wmadden

Poem

🐇 Hop hop, a temp table appears,
Built inside a transaction with care,
A name is resolved — or conjured from thin air,
CREATE, then DROP when the query is done,
Both Postgres and SQLite share the same fun! 🍀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately and concisely describes the main change: adding typed temp table creation support in transactions for both Postgres and SQLite.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/3-extensions/postgres/src/runtime/postgres.ts`:
- Line 225: Replace the bare `as unknown as TempTableJoinSource<Row>` cast with
`blindCast` in two locations: In
packages/3-extensions/postgres/src/runtime/postgres.ts at line 225 in the
`asJoinSource` method, replace `return source as unknown as
TempTableJoinSource<Row>` with `return blindCast<TempTableJoinSource<Row>,
'source implements TempTableJoinSource duck-type'>(source)`. Apply the identical
fix in packages/3-extensions/sqlite/src/runtime/sqlite.ts at line 194 in the
corresponding `asJoinSource` method. This ensures compliance with coding
guidelines that prohibit bare `as` casts in production code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 6a088fd8-abb9-4c95-af81-be3fa224b03b

📥 Commits

Reviewing files that changed from the base of the PR and between 10d8b60 and 5eb7185.

📒 Files selected for processing (6)
  • packages/3-extensions/postgres/src/runtime/postgres.ts
  • packages/3-extensions/postgres/test/postgres.test.ts
  • packages/3-extensions/postgres/test/transaction.types.test-d.ts
  • packages/3-extensions/sqlite/src/runtime/sqlite.ts
  • packages/3-extensions/sqlite/test/transaction.test.ts
  • packages/3-extensions/sqlite/test/transaction.types.test-d.ts

Comment thread packages/3-extensions/postgres/src/runtime/postgres.ts Outdated
@paulwer
paulwer force-pushed the feat-temp-tables branch 2 times, most recently from 8cb7684 to 26257d6 Compare June 16, 2026 06:34

@aqrln aqrln left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a good start, but, before discussing the implementation further, let's start with a spec that describes the user-facing API, and then add end-to-end tests.

TempTableJoinSource<Row> & {
readonly name: string;
readonly fields: Row;
drop(): Promise<void>;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should we use Symbol.asyncDispose?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good call, implemented. The temp table handle now exposes Symbol.asyncDispose and maps it to drop(), so explicit cleanup and async disposal behave consistently.

I dont think, that would be widly used and we dont need it technical for cleanup the tempTable,because of the transactions itself, but supporting this pattern might help some folks.

const tableName = resolveTempTableName(options);
const quotedTableName = quoteIdentifier(tableName);
const queryPlan = planFromAst(query.buildAst(), contract, 'dsl');
const adapter = instantiateExecutionStack(stack).adapter;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this should not instantiate a new execution stack, we should already have access to everything here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed, fixed. I removed the extra stack instantiation path and now reuse the existing runtime/adapter context that is already available in the transaction flow. Please check

});

const createPlan = Object.freeze({
sql: `CREATE TEMP TABLE ${quoteIdentifier(tableName)} AS ${lowered.sql}`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we should use an AST node for this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. I switched to an AST-backed path for temp table creation planning instead of constructing ad-hoc SQL execution outside the AST/plan flow.

name: tableName,
fields: rowFields,
async drop(): Promise<void> {
await executeRawSql(txCtx, contract, `DROP TABLE IF EXISTS ${quotedTableName}`);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

likewise

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Applied the same AST-based approach.


return {
async as<Row extends Record<string, ScopeField>>(
query: Subquery<Row>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

where does the subquery come from? could you add end-to-end tests that show how you use this to materialize an ORM or SQL query builder query into a temp table?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Great point, added both variants in E2E. We now test materializing temp tables from SQL builder queries and from ORM queries using new internal method (please review as well or give me instructions, how else to solve, otherwise we could descide to omit ORM for now), and verify reuse in FROM and JOIN statements (in e2e).

@paulwer
paulwer force-pushed the feat-temp-tables branch 2 times, most recently from dcb72e9 to e2bb113 Compare June 16, 2026 13:13
@paulwer

paulwer commented Jun 16, 2026

Copy link
Copy Markdown
Author

@aqrln I added a spec, please review and give me feedback

@paulwer
paulwer force-pushed the feat-temp-tables branch from e2bb113 to 00f7c16 Compare June 16, 2026 13:24

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/3-extensions/postgres/src/runtime/postgres.ts`:
- Around line 274-277: The getJoinOuterScope function contains a bare `as
Record<string, Row>` cast which violates coding guidelines that forbid bare `as`
casts in production code. Replace this bare cast by either creating a typed
local variable with explicit type annotation for the namespaces object or by
using a cast helper function like blindCast or castAs to safely convert the type
while maintaining code quality standards.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 8c787472-2cd7-4cae-b28f-07224a662517

📥 Commits

Reviewing files that changed from the base of the PR and between dcb72e9 and 00f7c16.

⛔ Files ignored due to path filters (1)
  • projects/temp-tables-in-transactions/spec.md is excluded by !projects/**
📒 Files selected for processing (12)
  • packages/3-extensions/postgres/src/runtime/postgres.ts
  • packages/3-extensions/postgres/test/postgres.test.ts
  • packages/3-extensions/postgres/test/transaction.types.test-d.ts
  • packages/3-extensions/sql-orm-client/src/collection.ts
  • packages/3-extensions/sql-orm-client/src/exports/index.ts
  • packages/3-extensions/sql-orm-client/src/internal-temp-table-source.ts
  • packages/3-extensions/sql-orm-client/test/collection.as-subquery.test.ts
  • packages/3-extensions/sqlite/src/runtime/sqlite.ts
  • packages/3-extensions/sqlite/test/transaction.test.ts
  • packages/3-extensions/sqlite/test/transaction.types.test-d.ts
  • test/e2e/framework/test/sqlite/transaction.test.ts
  • test/e2e/framework/test/transaction-orm.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/3-extensions/sql-orm-client/src/exports/index.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/3-extensions/sqlite/test/transaction.types.test-d.ts
  • packages/3-extensions/postgres/test/transaction.types.test-d.ts
  • packages/3-extensions/sqlite/test/transaction.test.ts
  • packages/3-extensions/postgres/test/postgres.test.ts
  • packages/3-extensions/sqlite/src/runtime/sqlite.ts

Comment on lines +274 to +277
getJoinOuterScope: () => ({
topLevel: rowFields,
namespaces: { [alias]: rowFields } as Record<string, Row>,
}),

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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace the new bare cast with a typed local (or cast helper).

This adds a bare as in production code. Use explicit typing (or blindCast/castAs) instead.

♻️ Proposed fix
-      getJoinOuterScope: () => ({
-        topLevel: rowFields,
-        namespaces: { [alias]: rowFields } as Record<string, Row>,
-      }),
+      getJoinOuterScope: () => {
+        const namespaces: Record<string, Row> = { [alias]: rowFields };
+        return {
+          topLevel: rowFields,
+          namespaces,
+        };
+      },

As per coding guidelines, **/*.{ts,tsx,js,jsx} forbids bare as in production code and requires cast helpers or narrower typing.

📝 Committable suggestion

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

Suggested change
getJoinOuterScope: () => ({
topLevel: rowFields,
namespaces: { [alias]: rowFields } as Record<string, Row>,
}),
getJoinOuterScope: () => {
const namespaces: Record<string, Row> = { [alias]: rowFields };
return {
topLevel: rowFields,
namespaces,
};
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/3-extensions/postgres/src/runtime/postgres.ts` around lines 274 -
277, The getJoinOuterScope function contains a bare `as Record<string, Row>`
cast which violates coding guidelines that forbid bare `as` casts in production
code. Replace this bare cast by either creating a typed local variable with
explicit type annotation for the namespaces object or by using a cast helper
function like blindCast or castAs to safely convert the type while maintaining
code quality standards.

Source: Coding guidelines

@pkg-pr-new

pkg-pr-new Bot commented Jun 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/extension-author-tools

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-author-tools@836

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-runtime@836

@prisma-next/family-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-mongo@836

@prisma-next/sql-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-runtime@836

@prisma-next/family-sql

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-sql@836

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-arktype-json@836

@prisma-next/middleware-cache

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/middleware-cache@836

@prisma-next/mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo@836

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-paradedb@836

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-pgvector@836

@prisma-next/extension-postgis

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-postgis@836

@prisma-next/postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/postgres@836

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-orm-client@836

@prisma-next/sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sqlite@836

@prisma-next/extension-supabase

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-supabase@836

@prisma-next/target-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-mongo@836

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-mongo@836

@prisma-next/driver-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-mongo@836

@prisma-next/contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract@836

@prisma-next/utils

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/utils@836

@prisma-next/config

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/config@836

@prisma-next/errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/errors@836

@prisma-next/framework-components

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/framework-components@836

@prisma-next/operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/operations@836

@prisma-next/ts-render

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/ts-render@836

@prisma-next/contract-authoring

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract-authoring@836

@prisma-next/ids

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/ids@836

@prisma-next/psl-parser

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-parser@836

@prisma-next/psl-printer

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-printer@836

@prisma-next/cli

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/cli@836

@prisma-next/cli-telemetry

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/cli-telemetry@836

@prisma-next/config-loader

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/config-loader@836

@prisma-next/emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/emitter@836

@prisma-next/language-server

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/language-server@836

@prisma-next/migration-tools

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/migration-tools@836

prisma-next

npm i https://pkg.pr.new/prisma/prisma-next@836

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/vite-plugin-contract-emit@836

@prisma-next/mongo-codec

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-codec@836

@prisma-next/mongo-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract@836

@prisma-next/mongo-value

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-value@836

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract-psl@836

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract-ts@836

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-emitter@836

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-schema-ir@836

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-query-ast@836

@prisma-next/mongo-orm

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-orm@836

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-query-builder@836

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-lowering@836

@prisma-next/mongo-wire

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-wire@836

@prisma-next/sql-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract@836

@prisma-next/sql-errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-errors@836

@prisma-next/sql-operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-operations@836

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-schema-ir@836

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-psl@836

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-ts@836

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-emitter@836

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-lane-query-builder@836

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-relational-core@836

@prisma-next/sql-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-builder@836

@prisma-next/target-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-postgres@836

@prisma-next/target-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-sqlite@836

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-postgres@836

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-sqlite@836

@prisma-next/driver-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-postgres@836

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-sqlite@836

commit: 4b189fa

@aqrln aqrln left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great, this is shaping up very nicely, I like the direction! Nice work 🙌

I left a bunch of comments in the spec, let's finalize the design and then update the implementation.

Also heads up that some jobs are failing on CI.

Comment thread projects/temp-tables-in-transactions/spec.md Outdated
Comment thread projects/temp-tables-in-transactions/spec.md Outdated
.build(),
).toArray();

await temp.drop(); // or: await using temp = ...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should be encouraging the use of the safer await using temp = ... pattern in the examples and docs.

This code snippet, as currently written, has a bug and does not always call drop(). It needs to use a try/finally block to do so consistently.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

(on the other hand, transaction rollback will also drop the table automatically, so 🤷)

The builder exposes:

```ts
as(query: TempTableQuerySource | OrmCollection): Promise<TempTableHandle<Row>>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This must not know anything about OrmCollection on this level. There should be a common interface that as() accepts and all query DSLs implement.


### Query sources accepted by `as(...)`

`as(...)` accepts two kinds of inputs:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It must accept infinite kinds of inputs that implement a common interface to get the AST. Query DSLs are pluggable and could even be implemented as third-party packages.

Comment thread projects/temp-tables-in-transactions/spec.md Outdated

## Behavior

- Temp tables are scoped to the active transaction connection. They do not exist outside the transaction and cannot leak across requests or connection-pool members.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we actually guarantee that they are transaction scoped or session/connection scoped?

This influences the SQL statement we need to emit to create the temp table, and how important the drop API is.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh and ON COMMIT DROP is a Postgres extension I think, so we can't actually guarantee transaction scope, unless we move the whole API into the target layer.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@aqrln as mentioned below this works for postgres completly, but sqllite needs a cleanup process. I therefore introduced preCommitHooks and implemented a suiteable solution for that.

please review :)

- `select(...)` on the collection limits the columns that appear in the temp table and in `handle.fields`.
- If no `select(...)` is provided, the ORM's default scalar projection is used.
- The normalisation is performed via an internal Symbol protocol (`INTERNAL_TO_TEMP_TABLE_QUERY_SOURCE`) on `Collection`. This is **not a public API** and is not exported as part of the ORM's user-facing surface.
- A public, standalone `asSubquery()` API on `Collection` is **out of scope for this PR** and is tracked as a follow-up if a general ORM-to-query-source conversion primitive is needed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No, it's a blocker, the temp table API must not know anything about the ORM or the Connection class.

@paulwer paulwer Jun 19, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@aqrln should there be a asSubquery handler, which returns the AST?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not sure yet about the name and whether it should return the raw AST, but let's do this for now


- No `asRaw(...)` API — raw SQL strings are not accepted as a source; use the SQL builder's `raw\`...\`` tag to compose raw expressions into a structured query first.
- No temp-table alias method on the returned handle — the handle's `name` is the canonical table name and also serves as the default namespace alias in join field proxies.
- No support outside a transaction — the API is intentionally limited to transaction contexts; outside a transaction, different connection pool members may serve sequential queries, making a temp-table session guarantee impossible.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It is still session scoped by default even if you create it within a transaction (except the rollback case)

## Name semantics

- If `name` is omitted, a collision-resistant name is generated: `pn_temp_<20 hex chars>`.
- If provided, the name must satisfy `[A-Za-z_][A-Za-z0-9_]*` and must not exceed 63 characters (Postgres `NAMEDATALEN` limit; SQLite applies the same cap for consistency).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should these rules be owned by the adapter, e.g. in AdapterProfile?

@paulwer paulwer Jun 19, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@aqrln i mentioned it below already, but do you see any real benefit for the user to change the name used to create the temp table? in my opinion we could completly let the implementation descide the name.

by default a name currently is optional and can be user, otherwise the name is created to the according rule described in the adapter impl

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Agree. It's easy to add it later if a use case is found and someone requests it.

@paulwer

paulwer commented Jun 18, 2026

Copy link
Copy Markdown
Author

ON COMMIT DROP is postgres default and no extension if I understand it correctly, so for postgres we could enforce temp table only valid in execution context, if defined in statement. Its one of the base features and exists at least < PostgreSQL 7.x.
So it seems very stable to use for this usecase.
The real point of interest seems to be sqlite, i did some research and temp tables are not able to support automatic cleanup like in postgres.
I also do think, we should not rely on the user using the right syntax and instead create a automatic cleanup after the transaction context closes. I will explore this further.
Important for Cleanup: SIGKILL does not guarantee execution of
asyncDispose, but end of db session will cleanup the temp tables in both postgres and sqlite
Savepoints are not counted as commits and therefore will not drop the temp table in postgres, only when commiting the transaction.

Things I am thinking about which I wanted to share:

  • not allowing user to change name of the temp table to enforce unique table names to prevent name race conditions (exspecialy in environments, like sqlite)
    -> I cant think of a situation, where the user has a benefit of changing the name
  • enabling prisma to create real tables instead of temp tables would allow usage outside of transactions: f.ex. _prisma_temp_table_xxxxxxxxxxxxxxxxxxx
    -> but more complexity and we would need a better cleanup process, when user messed up the manual drop / table lifecycle
    => BUT: It would make the feature accessable even in a pooled connection scope without persistent sessions within the execution flow (if thats wanted)
  • temp tables in postgres can hugely benefit of indexes, we should think about supporting this in the future as well

@aqrln any thoughts on those topics?

@paulwer
paulwer force-pushed the feat-temp-tables branch 2 times, most recently from 0161372 to 1ff479e Compare June 19, 2026 09:14
@paulwer

paulwer commented Jun 19, 2026

Copy link
Copy Markdown
Author

@aqrln i have added an additional spec tempTable().from() + append() which enables users to "upload" data into temp tables for reuse in further calculations.

please review :)

Bulk import pipelines — materialise the incoming data rows once, then run validation queries, conflict detection, and inserts all against the temp table rather than re-parsing the input multiple times. Use from(columns) to define the table schema and append(rows) to stream in the data:

const staging = await tx.tempTable({ name: 'staging' }).from([
  { name: 'id',    type: 'int4' },
  { name: 'email', type: 'text' },
]);
await staging.append(importedRows);       // load data
await staging.append(moreRows);           // add more data if needed
// … run validations, conflict checks, final INSERT …

@aqrln

aqrln commented Jun 19, 2026

Copy link
Copy Markdown
Member

enabling prisma to create real tables instead of temp tables would allow usage outside of transactions: f.ex. _prisma_temp_table_xxxxxxxxxxxxxxxxxxx
=> BUT: It would make the feature accessable even in a pooled connection scope without persistent sessions within the execution flow (if thats wanted)

It is also possible for users to obtain a pinned connection without a transaction. Those could make use of temporary tables too.

@paulwer
paulwer force-pushed the feat-temp-tables branch from 1ff479e to 7331f81 Compare June 21, 2026 10:14
@paulwer

paulwer commented Jun 21, 2026

Copy link
Copy Markdown
Author

enabling prisma to create real tables instead of temp tables would allow usage outside of transactions: f.ex. _prisma_temp_table_xxxxxxxxxxxxxxxxxxx
=> BUT: It would make the feature accessable even in a pooled connection scope without persistent sessions within the execution flow (if thats wanted)

It is also possible for users to obtain a pinned connection without a transaction. Those could make use of temporary tables too.

@aqrln There wasnt a native withTransaction wrapper available yet, so i added it. Please check if thats realy wanted all in this PR or if thats out of scope tbh?!

If thats not the aproach of waht you meant please iterate a bit more on it.

@paulwer
paulwer force-pushed the feat-temp-tables branch 3 times, most recently from 022e813 to 4b189fa Compare June 22, 2026 10:23
@aqrln

aqrln commented Jul 7, 2026

Copy link
Copy Markdown
Member

Hey @paulwer, sorry for the delay! I think the feature is great and I'd like to get it finished and merged.

ON COMMIT DROP is postgres default

It's not, you need to opt in to it explicitly, see https://www.postgresql.org/docs/current/sql-createtable.html#SQL-CREATETABLE-PARMS-ON-COMMIT

and no extension

Extension in the sense of Postgres-specific addition to SQL, not in the sense of loadable PostgreSQL extensions. It would still be a non-standard extension regardless of whether it's the default or not because it does not exist in standard SQL. Only ON COMMIT PRESERVE ROWS (i.e. keep the temp table outside of the transaction but delete the data) and ON COMMIT DELETE ROWS (i.e. keep the table together with data after commit) do.

Notably, even though Postgres doesn't use the non-standard ON COMMIT DROP as the default, it still deviates from the standard. SQL standard prescribes that DELETE ROWS should be the default but Postgres uses PRESERVE ROWS. Other databases are even worse in this regard though (see below).

If we don't allow custom temp table names and never reuse the same name twice, then I think the practical difference between DROP and DELETE ROWS is not huge (but also not negligible). The problem is SQLite and MySQL support neither. Again, at least not a correctness issue if the JS-side cleanup doesn't run as long as we don't use static temp table names, but extra memory/disk usage (which can blow up if the connection/session never gets closed with an idle timeout and keeps being picked up from the pool for new transactions).

So I think we should not allow custom/static names for transaction-scoped temp tables, and we should definitely have the cleanup API via Symbol.asyncDispose (+ optionally an explicit method). We can and should use ON COMMIT DROP or ON COMMIT DELETE ROWS (in this order of priority) for targets that support them as a safety net.

We could have a different session-scoped temp table API, where customizing the name would make sense, but I don't think we need that right now.

enabling prisma to create real tables instead of temp tables would allow usage outside of transactions: f.ex. _prisma_temp_table_xxxxxxxxxxxxxxxxxxx

Let's not do that right now. Session-scoped temp tables outside of transactions are okay for a pinned connection, but creating and dropping real tables accessible from multiple connections sounds like a recipe for race conditions. Those should probably be just normal tables defined in the contract and managed with migrations, even if the data is periodically cleaned. Or materialized views.

There wasnt a native withTransaction wrapper available yet, so i added it.

I'll have a look!

@paulwer

paulwer commented Jul 7, 2026

Copy link
Copy Markdown
Author

@aqrln sounds lgtm, I will have a look in the next couple of days :)

  1. regarding extension: i now get your point entirely (I guess)
  2. I will remove all custom table naming options
  3. I had already implemented the asyncDisposal, but I do think its not necessarily needed, other than the user will declare it inside a transaction in another context, which gets cleaned earlier, before the transaction completes/rolls back.
    => thats the reason, why I implemented those transaction hooks. you might take a look. this should help us enforce deleting the temp tables definatly, id the db does not support ON COMMIT DROP (or simmilar)
    => same for pinned connection => also take a look please
  4. got it, no real tables. indeed this was just an idea I wanted to share :)

Please also review my comment regarding Bulk import pipelines

Thank you very much :)

@paulwer
paulwer force-pushed the feat-temp-tables branch from ad67ec6 to bbd8207 Compare July 12, 2026 12:07
@paulwer
paulwer force-pushed the feat-temp-tables branch from bbd8207 to 889dd8c Compare July 12, 2026 12:07
@paulwer

paulwer commented Jul 12, 2026

Copy link
Copy Markdown
Author

@aqrln I updated the branch based on the discussed points above

@paulwer
paulwer requested a review from aqrln July 12, 2026 12:09
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