@@ -4,12 +4,20 @@ import type { Contract } from '@prisma-next/contract/types';
44import postgresDriver from '@prisma-next/driver-postgres/runtime' ;
55import { instantiateExecutionStack } from '@prisma-next/framework-components/execution' ;
66import { sql as sqlBuilder } from '@prisma-next/sql-builder/runtime' ;
7- import type { Db } from '@prisma-next/sql-builder/types' ;
7+ import type {
8+ Db ,
9+ QueryContext ,
10+ Scope ,
11+ ScopeField ,
12+ SelectQuery ,
13+ Subquery ,
14+ } from '@prisma-next/sql-builder/types' ;
815import type { ExtractCodecTypes , SqlStorage } from '@prisma-next/sql-contract/types' ;
916import { orm as ormBuilder } from '@prisma-next/sql-orm-client' ;
17+ import { RawSqlExpr , TableSource } from '@prisma-next/sql-relational-core/ast' ;
1018import type { CodecTypesBase , RawSqlTag } from '@prisma-next/sql-relational-core/expression' ;
1119import { createRawSql } from '@prisma-next/sql-relational-core/expression' ;
12- import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan' ;
20+ import { planFromAst , type SqlQueryPlan } from '@prisma-next/sql-relational-core/plan' ;
1321import type {
1422 BindSiteParams ,
1523 Declaration ,
@@ -43,11 +51,34 @@ import { PostgresRuntimeImpl } from './postgres-runtime';
4351export type PostgresTargetId = 'postgres' ;
4452type OrmClient < TContract extends Contract < SqlStorage > > = ReturnType < typeof ormBuilder < TContract > > ;
4553
54+ const TEMP_TABLE_NAME_RE = / ^ [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * $ / ;
55+ const TEMP_TABLE_NAME_MAX_LEN = 63 ;
56+
57+ export interface TempTableCreateOptions {
58+ readonly name ?: string ;
59+ }
60+
61+ type TempTableJoinSource < Row extends Record < string , ScopeField > > = ReturnType <
62+ SelectQuery < QueryContext , Scope , Row > [ 'as' ]
63+ > ;
64+
65+ export type TempTableHandle < Row extends Record < string , ScopeField > = Record < string , ScopeField > > =
66+ TempTableJoinSource < Row > & {
67+ readonly name : string ;
68+ readonly fields : Row ;
69+ drop ( ) : Promise < void > ;
70+ } ;
71+
72+ export interface TempTableBuilder {
73+ as < Row extends Record < string , ScopeField > > ( query : Subquery < Row > ) : Promise < TempTableHandle < Row > > ;
74+ }
75+
4676export interface PostgresTransactionContext < TContract extends Contract < SqlStorage > >
4777 extends TransactionContext {
4878 readonly sql : Db < TContract > ;
4979 readonly orm : OrmClient < TContract > ;
5080 readonly enums : NamespacedEnums < TContract > ;
81+ tempTable ( options ?: string | TempTableCreateOptions ) : TempTableBuilder ;
5182}
5283
5384export interface PostgresClient < TContract extends Contract < SqlStorage > > {
@@ -140,6 +171,106 @@ function toRuntimeBinding<TContract extends Contract<SqlStorage>>(
140171 } as const ;
141172}
142173
174+ function quoteIdentifier ( name : string ) : string {
175+ return `"${ name . replaceAll ( '"' , '""' ) } "` ;
176+ }
177+
178+ function resolveTempTableName ( options ?: string | TempTableCreateOptions ) : string {
179+ const requestedName = typeof options === 'string' ? options : options ?. name ;
180+ if ( requestedName !== undefined ) {
181+ const trimmed = requestedName . trim ( ) ;
182+ if ( ! TEMP_TABLE_NAME_RE . test ( trimmed ) ) {
183+ throw new Error (
184+ 'Invalid temp table name. Use only letters, numbers, and underscore, and start with a letter/underscore.' ,
185+ ) ;
186+ }
187+ if ( trimmed . length > TEMP_TABLE_NAME_MAX_LEN ) {
188+ throw new Error ( `Invalid temp table name. Maximum length is ${ TEMP_TABLE_NAME_MAX_LEN } .` ) ;
189+ }
190+ return trimmed ;
191+ }
192+
193+ const suffix = crypto . randomUUID ( ) . replaceAll ( '-' , '' ) . slice ( 0 , 20 ) ;
194+ return `pn_temp_${ suffix } ` ;
195+ }
196+
197+ async function executeRawSql (
198+ txCtx : TransactionContext ,
199+ contract : Contract < SqlStorage > ,
200+ sql : string ,
201+ ) : Promise < void > {
202+ await txCtx . execute ( planFromAst ( RawSqlExpr . of ( [ sql ] , [ ] ) , contract , 'raw.temp-table' ) ) . toArray ( ) ;
203+ }
204+
205+ function createTempTableBuilder (
206+ txCtx : TransactionContext ,
207+ contract : Contract < SqlStorage > ,
208+ stack : SqlExecutionStackWithDriver < PostgresTargetId > ,
209+ options ?: string | TempTableCreateOptions ,
210+ ) : TempTableBuilder {
211+ const asJoinSource = < Row extends Record < string , ScopeField > > (
212+ tableName : string ,
213+ alias : string ,
214+ rowFields : Row ,
215+ ) : TempTableJoinSource < Row > => {
216+ const source = {
217+ getJoinOuterScope : ( ) => ( {
218+ topLevel : rowFields ,
219+ namespaces : { [ alias ] : rowFields } as Record < string , Row > ,
220+ } ) ,
221+ buildAst : ( ) => TableSource . named ( tableName , alias ) ,
222+ } ;
223+ return blindCast < TempTableJoinSource < Row > , 'source implements TempTableJoinSource duck-type' > (
224+ source ,
225+ ) ;
226+ } ;
227+
228+ return {
229+ async as < Row extends Record < string , ScopeField > > (
230+ query : Subquery < Row > ,
231+ ) : Promise < TempTableHandle < Row > > {
232+ const tableName = resolveTempTableName ( options ) ;
233+ const quotedTableName = quoteIdentifier ( tableName ) ;
234+ const queryPlan = planFromAst ( query . buildAst ( ) , contract , 'dsl' ) ;
235+ const adapter = instantiateExecutionStack ( stack ) . adapter ;
236+ const lowered = adapter . lower ( queryPlan . ast , {
237+ contract,
238+ params : queryPlan . params ,
239+ } ) ;
240+ const params = lowered . params . map ( ( slot ) => {
241+ if ( slot . kind === 'literal' ) return slot . value ;
242+ throw new Error ( 'tempTable.as(...) does not accept bind-site parameters.' ) ;
243+ } ) ;
244+
245+ const createPlan = Object . freeze ( {
246+ sql : `CREATE TEMP TABLE ${ quoteIdentifier ( tableName ) } AS ${ lowered . sql } ` ,
247+ params,
248+ ast : queryPlan . ast ,
249+ meta : queryPlan . meta ,
250+ } ) ;
251+
252+ await txCtx . execute ( createPlan ) . toArray ( ) ;
253+
254+ const rowFields = blindCast < Row , 'subquery row fields align with Subquery<Row> generic' > (
255+ query . getRowFields ( ) ,
256+ ) ;
257+ const defaultJoin = asJoinSource < Row > ( tableName , tableName , rowFields ) ;
258+
259+ return blindCast <
260+ TempTableHandle < Row > ,
261+ 'temp table handle created from Subquery<Row> preserves the same row field shape'
262+ > ( {
263+ ...defaultJoin ,
264+ name : tableName ,
265+ fields : rowFields ,
266+ async drop ( ) : Promise < void > {
267+ await executeRawSql ( txCtx , contract , `DROP TABLE IF EXISTS ${ quotedTableName } ` ) ;
268+ } ,
269+ } ) ;
270+ } ,
271+ } ;
272+ }
273+
143274/**
144275 * Creates a lazy Postgres client from either `contractJson` or a TypeScript-authored `contract`.
145276 * Static query surfaces are available immediately, while `runtime()` instantiates the driver/pool on first call.
@@ -337,7 +468,14 @@ export default function postgres<TContract extends Contract<SqlStorage>>(
337468 // Spreading would evaluate the getter once and freeze its value.
338469 const tx : PostgresTransactionContext < TContract > = Object . assign (
339470 Object . create ( txCtx ) as TransactionContext ,
340- { sql : txSql , orm : txOrm , enums } ,
471+ {
472+ sql : txSql ,
473+ orm : txOrm ,
474+ enums,
475+ tempTable ( options ?: string | TempTableCreateOptions ) : TempTableBuilder {
476+ return createTempTableBuilder ( txCtx , context . contract , stack , options ) ;
477+ } ,
478+ } ,
341479 ) ;
342480
343481 return fn ( tx ) ;
0 commit comments