55// Renders the real prune statements against the real drizzle dialect and
66// schema. These are raw `sql` templates, so a rendering bug only surfaces at
77// execution time — the global drizzle/schema mocks would hide it entirely.
8+ //
9+ // The shared client sets `fetch_types: false` (packages/db/db.ts), which skips
10+ // loading array type OIDs. That makes an array *parameter* unserializable —
11+ // postgres.js sends it in a form Postgres rejects with 22P02. Only scalar bind
12+ // params are safe here, so these tests assert no array parameter is emitted.
813import { describe , expect , it , vi } from 'vitest'
914
1015vi . unmock ( 'drizzle-orm' )
@@ -16,13 +21,18 @@ process.env.DATABASE_URL ??= 'postgresql://user:pass@localhost:5432/test'
1621const { PgDialect } = await import ( 'drizzle-orm/pg-core' )
1722const { pruneLargeValueMetadata } = await import ( '@/lib/execution/payloads/large-value-metadata' )
1823
24+ interface RenderedQuery {
25+ sql : string
26+ params : unknown [ ]
27+ }
28+
1929/** Captures the raw statements `pruneLargeValueMetadata` issues, without a database. */
20- async function renderPruneStatements ( workspaceIds : string [ ] ) : Promise < string [ ] > {
30+ async function renderPruneStatements ( workspaceIds : string [ ] ) : Promise < RenderedQuery [ ] > {
2131 const dialect = new PgDialect ( )
22- const rendered : string [ ] = [ ]
32+ const rendered : RenderedQuery [ ] = [ ]
2333 const capturingClient = {
2434 execute : async ( query : Parameters < PgDialect [ 'sqlToQuery' ] > [ 0 ] ) => {
25- rendered . push ( dialect . sqlToQuery ( query ) . sql )
35+ rendered . push ( dialect . sqlToQuery ( query ) )
2636 return [ { count : 0 } ]
2737 } ,
2838 }
@@ -39,25 +49,33 @@ async function renderPruneStatements(workspaceIds: string[]): Promise<string[]>
3949}
4050
4151describe ( 'pruneLargeValueMetadata SQL' , ( ) => {
42- it ( 'binds workspace ids as one array parameter, not a value list' , async ( ) => {
43- const statements = await renderPruneStatements ( [ 'ws-1' , 'ws-2' ] )
44-
45- expect ( statements . length ) . toBeGreaterThan ( 0 )
46- for ( const statement of statements ) {
47- // `ANY(($1, $2)::text[])` is what interpolating the raw JS array yields —
48- // Postgres rejects it ("cannot cast type record to text[]"), and with a
49- // single id `ANY(($1)::text[])` fails as 22P02 instead.
50- expect ( statement ) . not . toMatch ( / A N Y \( \( \$ \d + / )
51- expect ( statement ) . toMatch ( / A N Y \( \$ \d + : : t e x t \[ \] \) / )
52- }
53- } )
52+ for ( const [ label , ids ] of [
53+ [ 'multiple workspace ids' , [ 'ws-1' , 'ws-2' ] ] ,
54+ [ 'a single workspace id' , [ 'ws-only' ] ] ,
55+ ] as const ) {
56+ describe ( label , ( ) => {
57+ it ( 'matches workspaces with an IN list of scalar params' , async ( ) => {
58+ const statements = await renderPruneStatements ( [ ...ids ] )
5459
55- it ( 'renders identically for a single workspace id' , async ( ) => {
56- const statements = await renderPruneStatements ( [ 'ws-only' ] )
60+ expect ( statements . length ) . toBeGreaterThan ( 0 )
61+ for ( const { sql : text } of statements ) {
62+ expect ( text ) . toMatch ( / w o r k s p a c e _ i d I N \( \$ \d + / )
63+ // `= IN (...)` is a syntax error Postgres only reports at execution.
64+ expect ( text ) . not . toMatch ( / = \s * I N \s * \( / )
65+ // `ANY(($1)::text[])` / `ANY(($1, $2)::text[])` — the row-constructor cast.
66+ expect ( text ) . not . toMatch ( / A N Y \( \( \$ \d + / )
67+ }
68+ } )
5769
58- expect ( statements . length ) . toBeGreaterThan ( 0 )
59- for ( const statement of statements ) {
60- expect ( statement ) . toMatch ( / A N Y \( \$ \d + : : t e x t \[ \] \) / )
61- }
62- } )
70+ it ( 'never binds an array as a parameter' , async ( ) => {
71+ const statements = await renderPruneStatements ( [ ...ids ] )
72+
73+ for ( const { params } of statements ) {
74+ for ( const param of params ) {
75+ expect ( Array . isArray ( param ) ) . toBe ( false )
76+ }
77+ }
78+ } )
79+ } )
80+ }
6381} )
0 commit comments