@@ -34,8 +34,7 @@ describe("postgres.js basic", function () {
3434 } ) ;
3535
3636 it ( "multiple rows" , async function ( ) {
37- const rows =
38- await sql `SELECT generate_series(1, 5)::int AS n ORDER BY n` ;
37+ const rows = await sql `SELECT generate_series(1, 5)::int AS n ORDER BY n` ;
3938 assert . strictEqual ( rows . length , 5 ) ;
4039 assert . strictEqual ( rows [ 0 ] . n , 1 ) ;
4140 assert . strictEqual ( rows [ 4 ] . n , 5 ) ;
@@ -63,8 +62,7 @@ describe("postgres.js CRUD", function () {
6362 assert . strictEqual ( item . name , "widget" ) ;
6463 assert . strictEqual ( item . quantity , 5 ) ;
6564
66- const [ found ] =
67- await sql `SELECT * FROM pjs_items_9k WHERE id = ${ item . id } ` ;
65+ const [ found ] = await sql `SELECT * FROM pjs_items_9k WHERE id = ${ item . id } ` ;
6866 assert . strictEqual ( found . name , "widget" ) ;
6967 } ) ;
7068
@@ -73,8 +71,7 @@ describe("postgres.js CRUD", function () {
7371 await sql `INSERT INTO pjs_items_9k (name, quantity) VALUES ('gizmo', 1) RETURNING *` ;
7472 await sql `UPDATE pjs_items_9k SET quantity = 10 WHERE id = ${ item . id } ` ;
7573
76- const [ found ] =
77- await sql `SELECT * FROM pjs_items_9k WHERE id = ${ item . id } ` ;
74+ const [ found ] = await sql `SELECT * FROM pjs_items_9k WHERE id = ${ item . id } ` ;
7875 assert . strictEqual ( found . quantity , 10 ) ;
7976 } ) ;
8077
@@ -83,8 +80,7 @@ describe("postgres.js CRUD", function () {
8380 await sql `INSERT INTO pjs_items_9k (name, quantity) VALUES ('doomed', 0) RETURNING *` ;
8481 await sql `DELETE FROM pjs_items_9k WHERE id = ${ item . id } ` ;
8582
86- const rows =
87- await sql `SELECT * FROM pjs_items_9k WHERE id = ${ item . id } ` ;
83+ const rows = await sql `SELECT * FROM pjs_items_9k WHERE id = ${ item . id } ` ;
8884 assert . strictEqual ( rows . length , 0 ) ;
8985 } ) ;
9086
@@ -122,8 +118,7 @@ describe("postgres.js transactions", function () {
122118 return await tx `INSERT INTO pjs_tx_9k (value) VALUES ('committed') RETURNING *` ;
123119 } ) ;
124120
125- const [ found ] =
126- await sql `SELECT * FROM pjs_tx_9k WHERE id = ${ item . id } ` ;
121+ const [ found ] = await sql `SELECT * FROM pjs_tx_9k WHERE id = ${ item . id } ` ;
127122 assert . strictEqual ( found . value , "committed" ) ;
128123 } ) ;
129124
@@ -140,8 +135,7 @@ describe("postgres.js transactions", function () {
140135 assert . strictEqual ( e . message , "force rollback" ) ;
141136 }
142137
143- const rows =
144- await sql `SELECT * FROM pjs_tx_9k WHERE id = ${ insertedId } ` ;
138+ const rows = await sql `SELECT * FROM pjs_tx_9k WHERE id = ${ insertedId } ` ;
145139 assert . strictEqual ( rows . length , 0 ) ;
146140 } ) ;
147141} ) ;
@@ -239,9 +233,7 @@ describe("postgres.js unsafe (simple protocol)", function () {
239233 } ) ;
240234
241235 it ( "unsafe insert and select" , async function ( ) {
242- await sql . unsafe (
243- "INSERT INTO pjs_unsafe_9k (name) VALUES ('unsafe_item')" ,
244- ) ;
236+ await sql . unsafe ( "INSERT INTO pjs_unsafe_9k (name) VALUES ('unsafe_item')" ) ;
245237 const rows = await sql . unsafe (
246238 "SELECT * FROM pjs_unsafe_9k WHERE name = 'unsafe_item'" ,
247239 ) ;
@@ -255,9 +247,7 @@ describe("postgres.js unsafe (simple protocol)", function () {
255247 } ) ;
256248
257249 it ( "unsafe multi-statement" , async function ( ) {
258- const rows = await sql . unsafe (
259- "SELECT 1 AS a; SELECT 2 AS b" ,
260- ) ;
250+ const rows = await sql . unsafe ( "SELECT 1 AS a; SELECT 2 AS b" ) ;
261251 // postgres.js returns the last result for multi-statement
262252 assert . ok ( rows . length >= 1 ) ;
263253 } ) ;
@@ -335,7 +325,8 @@ describe("postgres.js dynamic fragments", function () {
335325
336326 it ( "dynamic column names" , async function ( ) {
337327 const columns = [ "name" , "score" ] ;
338- const rows = await sql `SELECT ${ sql ( columns ) } FROM pjs_dyn_9k ORDER BY score` ;
328+ const rows =
329+ await sql `SELECT ${ sql ( columns ) } FROM pjs_dyn_9k ORDER BY score` ;
339330 assert . strictEqual ( rows . length , 3 ) ;
340331 assert . strictEqual ( rows [ 0 ] . name , "alice" ) ;
341332 assert . strictEqual ( rows [ 0 ] . score , 10 ) ;
@@ -350,7 +341,8 @@ describe("postgres.js dynamic fragments", function () {
350341
351342 it ( "dynamic ORDER BY" , async function ( ) {
352343 const orderCol = "score" ;
353- const rows = await sql `SELECT * FROM pjs_dyn_9k ORDER BY ${ sql ( orderCol ) } DESC` ;
344+ const rows =
345+ await sql `SELECT * FROM pjs_dyn_9k ORDER BY ${ sql ( orderCol ) } DESC` ;
354346 assert . strictEqual ( rows [ 0 ] . name , "charlie" ) ;
355347 } ) ;
356348} ) ;
@@ -414,7 +406,8 @@ describe("postgres.js reserve (dedicated connection)", function () {
414406 await reserved `CREATE TABLE IF NOT EXISTS pjs_reserve_9k (id SERIAL PRIMARY KEY, val TEXT)` ;
415407 await reserved `TRUNCATE TABLE pjs_reserve_9k` ;
416408 await reserved `INSERT INTO pjs_reserve_9k (val) VALUES ('reserved')` ;
417- const [ row ] = await reserved `SELECT * FROM pjs_reserve_9k WHERE val = 'reserved'` ;
409+ const [ row ] =
410+ await reserved `SELECT * FROM pjs_reserve_9k WHERE val = 'reserved'` ;
418411 assert . strictEqual ( row . val , "reserved" ) ;
419412 await reserved `DROP TABLE IF EXISTS pjs_reserve_9k` ;
420413 } finally {
@@ -440,7 +433,8 @@ describe("postgres.js sql.array()", function () {
440433
441434 it ( "WHERE id = ANY with sql.array()" , async function ( ) {
442435 const ids = [ 1 , 2 ] ;
443- const rows = await sql `SELECT * FROM pjs_arr_9k WHERE id = ANY(${ sql . array ( ids , 23 ) } )` ;
436+ const rows =
437+ await sql `SELECT * FROM pjs_arr_9k WHERE id = ANY(${ sql . array ( ids , 23 ) } )` ;
444438 assert . strictEqual ( rows . length , 2 ) ;
445439 } ) ;
446440
@@ -451,47 +445,236 @@ describe("postgres.js sql.array()", function () {
451445 } ) ;
452446} ) ;
453447
454- describe ( "postgres.js unsafe stress test (50k unique statements)" , function ( ) {
448+ describe ( "postgres.js LIMIT NULL" , function ( ) {
449+ before ( async function ( ) {
450+ await adminSet ( "prepared_statements" , "extended_anonymous" ) ;
451+ await sqlNoPrepare `CREATE TABLE IF NOT EXISTS pjs_limit_9k (
452+ id SERIAL PRIMARY KEY,
453+ value TEXT
454+ )` ;
455+ await sqlNoPrepare `TRUNCATE TABLE pjs_limit_9k` ;
456+ await sqlNoPrepare `INSERT INTO pjs_limit_9k (value) VALUES ('a'), ('b'), ('c'), ('d'), ('e')` ;
457+ } ) ;
458+
459+ after ( async function ( ) {
460+ await sqlNoPrepare `DROP TABLE IF EXISTS pjs_limit_9k` ;
461+ await adminSet ( "prepared_statements" , "extended" ) ;
462+ } ) ;
463+
464+ it ( "LIMIT with null parameter returns all rows" , async function ( ) {
465+ const limit = null ;
466+ const rows =
467+ await sqlNoPrepare `SELECT * FROM pjs_limit_9k ORDER BY id LIMIT ${ limit } ` ;
468+ assert . strictEqual ( rows . length , 5 ) ;
469+ } ) ;
470+
471+ it ( "LIMIT with non-null parameter limits rows" , async function ( ) {
472+ const limit = 2 ;
473+ const rows =
474+ await sqlNoPrepare `SELECT * FROM pjs_limit_9k ORDER BY id LIMIT ${ limit } ` ;
475+ assert . strictEqual ( rows . length , 2 ) ;
476+ } ) ;
477+
478+ it ( "LIMIT null with WHERE clause and multiple params" , async function ( ) {
479+ const value = "a" ;
480+ const limit = null ;
481+ const rows =
482+ await sqlNoPrepare `SELECT * FROM pjs_limit_9k WHERE value >= ${ value } ORDER BY id LIMIT ${ limit } ` ;
483+ assert . strictEqual ( rows . length , 5 ) ;
484+ } ) ;
485+
486+ it ( "LIMIT and OFFSET both null" , async function ( ) {
487+ const limit = null ;
488+ const offset = null ;
489+ const rows =
490+ await sqlNoPrepare `SELECT * FROM pjs_limit_9k ORDER BY id LIMIT ${ limit } OFFSET ${ offset } ` ;
491+ assert . strictEqual ( rows . length , 5 ) ;
492+ } ) ;
493+
494+ it ( "LIMIT null with OFFSET non-null" , async function ( ) {
495+ const limit = null ;
496+ const offset = 3 ;
497+ const rows =
498+ await sqlNoPrepare `SELECT * FROM pjs_limit_9k ORDER BY id LIMIT ${ limit } OFFSET ${ offset } ` ;
499+ assert . strictEqual ( rows . length , 2 ) ;
500+ } ) ;
501+
502+ it ( "LIMIT non-null with OFFSET null" , async function ( ) {
503+ const limit = 2 ;
504+ const offset = null ;
505+ const rows =
506+ await sqlNoPrepare `SELECT * FROM pjs_limit_9k ORDER BY id LIMIT ${ limit } OFFSET ${ offset } ` ;
507+ assert . strictEqual ( rows . length , 2 ) ;
508+ } ) ;
509+ } ) ;
510+
511+ describe ( "postgres.js unsafe stress test (50k unique statements, 5 clients)" , function ( ) {
455512 this . timeout ( 300000 ) ;
456513
514+ const TIMESTAMPTZ_OID = 1184 ;
515+ const timestampType = {
516+ to : TIMESTAMPTZ_OID ,
517+ from : [ TIMESTAMPTZ_OID ] ,
518+ serialize : ( value ) =>
519+ ( value instanceof Date ? value : new Date ( value ) ) . toISOString ( ) ,
520+ parse : ( value ) => new Date ( value ) ,
521+ } ;
522+
523+ const NUM_CLIENTS = 5 ;
524+ const clients = [ ] ;
525+
457526 before ( async function ( ) {
458527 await adminSet ( "prepared_statements" , "extended_anonymous" ) ;
459- // Warmup: ensure pool connections are established after databases::init()
460- // recreates backend pools (same pattern as other test suites).
461- await sql . unsafe ( "SELECT 1" ) ;
528+ for ( let i = 0 ; i < NUM_CLIENTS ; i ++ ) {
529+ const c = postgres ( "postgres://pgdog:pgdog@127.0.0.1:6432/pgdog" , {
530+ prepare : false ,
531+ connection : { application_name : `stress_client_${ i } ` } ,
532+ types : { timestamp : timestampType } ,
533+ } ) ;
534+ await c . unsafe ( "SELECT 1" ) ; // warmup
535+ clients . push ( c ) ;
536+ }
462537 } ) ;
463538
464539 after ( async function ( ) {
540+ await Promise . all ( clients . map ( ( c ) => c . end ( ) ) ) ;
465541 await adminSet ( "prepared_statements" , "extended" ) ;
466542 } ) ;
467543
468- it ( "50k unique query texts with 25 rotating parameters " , async function ( ) {
544+ it ( "50k mixed queries (unsafe + tagged template) across 5 clients " , async function ( ) {
469545 const TOTAL_QUERIES = 50000 ;
470- const NUM_PARAMS = 25 ;
471546 const BATCH_SIZE = 100 ;
472547
473- const params = Array . from ( { length : NUM_PARAMS } , ( _ , i ) => i * 7 + 1 ) ;
548+ // Build a query with 1..numParams parameters mixing ints and timestamps.
549+ function buildQuery ( i ) {
550+ const numParams = ( i % 8 ) + 1 ; // 1 to 8 parameters
551+ const useTimestamp = i % 3 === 0 ; // every 3rd query includes a timestamp
552+ const vals = [ ] ;
553+ const selectParts = [ ] ;
554+ const expected = { } ;
555+
556+ for ( let k = 0 ; k < numParams ; k ++ ) {
557+ const paramIdx = k + 1 ;
558+ if ( useTimestamp && k === numParams - 1 ) {
559+ const ts = new Date ( 1700000000000 + i * 1000 ) ;
560+ vals . push ( ts ) ;
561+ selectParts . push ( `$${ paramIdx } ::timestamptz AS ts_q${ i } ` ) ;
562+ expected [ `ts_q${ i } ` ] = ( val ) => {
563+ const got = val instanceof Date ? val : new Date ( val ) ;
564+ assert . ok ( ! isNaN ( got . getTime ( ) ) , `invalid timestamp at query ${ i } : ${ val } ` ) ;
565+ assert . ok ( Math . abs ( got . getTime ( ) - ts . getTime ( ) ) < 60000 , `timestamp mismatch at query ${ i } : expected ~${ ts . toISOString ( ) } , got ${ got . toISOString ( ) } ` ) ;
566+ } ;
567+ } else {
568+ const intVal = ( i + k ) * 7 + 1 ;
569+ vals . push ( intVal ) ;
570+ selectParts . push ( `$${ paramIdx } ::int * ${ k + 1 } AS c${ k } _q${ i } ` ) ;
571+ expected [ `c${ k } _q${ i } ` ] = intVal * ( k + 1 ) ;
572+ }
573+ }
574+
575+ const queryText = `SELECT ${ selectParts . join ( ", " ) } ` ;
576+ return { queryText, vals, expected } ;
577+ }
578+
579+ // Tagged template queries that exercise unnamed prepared statements.
580+ // These reuse the same SQL text with different parameter values,
581+ // which is the normal postgres.js pattern with prepare: false.
582+ function taggedQuery ( client , i ) {
583+ const variant = i % 6 ;
584+ switch ( variant ) {
585+ case 0 : {
586+ // Simple parameterized select
587+ const val = i * 3 + 1 ;
588+ return client `SELECT ${ val } ::int AS v` . then ( ( rows ) => {
589+ assert . strictEqual ( rows [ 0 ] . v , val ) ;
590+ } ) ;
591+ }
592+ case 1 : {
593+ // Multiple parameters
594+ const a = i % 100 ;
595+ const b = i % 50 ;
596+ return client `SELECT ${ a } ::int + ${ b } ::int AS sum` . then ( ( rows ) => {
597+ assert . strictEqual ( rows [ 0 ] . sum , a + b ) ;
598+ } ) ;
599+ }
600+ case 2 : {
601+ // String parameter
602+ const name = `item_${ i } ` ;
603+ return client `SELECT ${ name } ::text AS name` . then ( ( rows ) => {
604+ assert . strictEqual ( rows [ 0 ] . name , name ) ;
605+ } ) ;
606+ }
607+ case 3 : {
608+ // Boolean + int parameters
609+ const flag = i % 2 === 0 ;
610+ const num = i % 1000 ;
611+ return client `SELECT ${ flag } ::bool AS flag, ${ num } ::int AS num` . then ( ( rows ) => {
612+ assert . strictEqual ( rows [ 0 ] . flag , flag ) ;
613+ assert . strictEqual ( rows [ 0 ] . num , num ) ;
614+ } ) ;
615+ }
616+ case 4 : {
617+ // Timestamp parameter
618+ const ts = new Date ( 1700000000000 + i * 1000 ) ;
619+ return client `SELECT ${ ts } ::timestamptz AS ts` . then ( ( rows ) => {
620+ const got = rows [ 0 ] . ts instanceof Date ? rows [ 0 ] . ts : new Date ( rows [ 0 ] . ts ) ;
621+ assert . ok ( Math . abs ( got . getTime ( ) - ts . getTime ( ) ) < 60000 ) ;
622+ } ) ;
623+ }
624+ case 5 : {
625+ // Many parameters (4)
626+ const a = i % 100 , b = i % 50 , c = i % 25 , d = i % 10 ;
627+ return client `SELECT ${ a } ::int AS a, ${ b } ::int AS b, ${ c } ::int AS c, ${ d } ::int AS d` . then ( ( rows ) => {
628+ assert . strictEqual ( rows [ 0 ] . a , a ) ;
629+ assert . strictEqual ( rows [ 0 ] . b , b ) ;
630+ assert . strictEqual ( rows [ 0 ] . c , c ) ;
631+ assert . strictEqual ( rows [ 0 ] . d , d ) ;
632+ } ) ;
633+ }
634+ }
635+ }
474636
475637 let completed = 0 ;
476638 const errors = [ ] ;
477639
478- for ( let batchStart = 0 ; batchStart < TOTAL_QUERIES ; batchStart += BATCH_SIZE ) {
640+ for (
641+ let batchStart = 0 ;
642+ batchStart < TOTAL_QUERIES ;
643+ batchStart += BATCH_SIZE
644+ ) {
479645 const batchEnd = Math . min ( batchStart + BATCH_SIZE , TOTAL_QUERIES ) ;
480646 const promises = [ ] ;
481647
482648 for ( let i = batchStart ; i < batchEnd ; i ++ ) {
483- const paramVal = params [ i % NUM_PARAMS ] ;
484- const queryText = `SELECT $1::int AS r_${ i } ` ;
485-
486- const p = sql
487- . unsafe ( queryText , [ paramVal ] )
488- . then ( ( rows ) => {
489- assert . strictEqual ( rows [ 0 ] [ `r_${ i } ` ] , paramVal ) ;
490- completed ++ ;
491- } )
492- . catch ( ( err ) => {
493- errors . push ( { i, err : err . message } ) ;
494- } ) ;
649+ const client = clients [ i % NUM_CLIENTS ] ;
650+ let p ;
651+
652+ if ( i % 2 === 0 ) {
653+ // Even: unsafe query (unique query text each time)
654+ const { queryText, vals, expected } = buildQuery ( i ) ;
655+ p = client
656+ . unsafe ( queryText , vals )
657+ . then ( ( rows ) => {
658+ for ( const [ col , val ] of Object . entries ( expected ) ) {
659+ if ( typeof val === "function" ) {
660+ val ( rows [ 0 ] [ col ] ) ;
661+ } else {
662+ assert . strictEqual (
663+ rows [ 0 ] [ col ] ,
664+ val ,
665+ `mismatch at query ${ i } , col ${ col } ` ,
666+ ) ;
667+ }
668+ }
669+ } ) ;
670+ } else {
671+ // Odd: tagged template (reused SQL text, unnamed prepared statements)
672+ p = taggedQuery ( client , i ) ;
673+ }
674+
675+ p = p
676+ . then ( ( ) => { completed ++ ; } )
677+ . catch ( ( err ) => { errors . push ( { i, err : err . message } ) ; } ) ;
495678
496679 promises . push ( p ) ;
497680 }
@@ -507,8 +690,6 @@ describe("postgres.js unsafe stress test (50k unique statements)", function () {
507690 assert . strictEqual ( completed , TOTAL_QUERIES ) ;
508691
509692 // Verify backend prepared statement evictions are happening.
510- // With 50k unique statements, pool_size=10, and capacity=500,
511- // each connection handles ~5k queries → ~4500 evictions each.
512693 const res = await fetch ( "http://localhost:9090" ) ;
513694 const metrics = await res . text ( ) ;
514695 const evictions = metrics
0 commit comments