Skip to content

Commit 5edc55f

Browse files
committed
chore: improve coverage
1 parent 69e0cf8 commit 5edc55f

File tree

4 files changed

+219
-63
lines changed

4 files changed

+219
-63
lines changed

src/lib/db.ts

Lines changed: 34 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ const poolerQueryHandleError = (pool: pg.Pool, sql: string): Promise<pg.QueryRes
2828
const connectionErrorHandler = (err: any) => {
2929
// If the error hasn't already be propagated to the catch
3030
if (!rejected) {
31-
rejected = true
32-
reject(err)
31+
// This is a trick to wait for the next tick, leaving a chance for handled errors such as
32+
// RESULT_SIZE_LIMIT to take over other stream errors such as `unexpected commandComplete message`
33+
setTimeout(() => {
34+
rejected = true
35+
return reject(err)
36+
})
3337
}
3438
}
3539
// This listened avoid getting uncaught exceptions for errors happening at connection level within the stream
@@ -178,73 +182,42 @@ ${' '.repeat(5 + lineNumber.toString().length + 2 + lineOffset)}^
178182
},
179183
}
180184
}
181-
182-
// Handle stream errors and result size exceeded errors
183-
if (error.code === 'RESULT_SIZE_EXCEEDED') {
184-
// Force kill the connection without waiting for graceful shutdown
185-
const _pool = pool
186-
pool = null
187-
try {
188-
if (_pool) {
189-
// Force kill the connection by destroying the socket
190-
const client = (_pool as any)._clients?.[0]
191-
if (client?.connection?.stream) {
192-
client.connection.stream.destroy()
193-
}
185+
try {
186+
// Handle stream errors and result size exceeded errors
187+
if (error.code === 'RESULT_SIZE_EXCEEDED') {
188+
// Force kill the connection without waiting for graceful shutdown
189+
return {
190+
data: null,
191+
error: {
192+
message: `Query result size (${error.resultSize} bytes) exceeded the configured limit (${error.maxResultSize} bytes)`,
193+
code: error.code,
194+
resultSize: error.resultSize,
195+
maxResultSize: error.maxResultSize,
196+
},
194197
}
195-
} catch (endError) {
196-
// Ignore any errors during cleanup
197198
}
198-
return {
199-
data: null,
200-
error: {
201-
message: `Query result size (${error.resultSize} bytes) exceeded the configured limit (${error.maxResultSize} bytes)`,
202-
code: error.code,
203-
resultSize: error.resultSize,
204-
maxResultSize: error.maxResultSize,
205-
},
206-
}
207-
}
208199

209-
// Handle other stream errors
210-
if (error.code === 'STREAM_ERROR') {
211-
// Force kill the connection without waiting for graceful shutdown
212-
const _pool = pool
213-
pool = null
214-
try {
215-
if (_pool) {
216-
// Force kill the connection by destroying the socket
217-
const client = (_pool as any)._clients?.[0]
218-
if (client?.connection?.stream) {
219-
client.connection.stream.destroy()
220-
}
221-
}
222-
} catch (endError) {
223-
// Ignore any errors during cleanup
224-
}
225-
return {
226-
data: null,
227-
error: {
228-
message: 'Stream error occurred while processing query',
229-
code: error.code,
230-
details: error.message,
231-
},
232-
}
200+
return { data: null, error: { code: error.code, message: error.message } }
201+
} finally {
202+
// If the error isn't a "DatabaseError" assume it's a connection related we kill the connection
203+
// To attempt a clean reconnect on next try
204+
await this.end()
233205
}
234-
235-
return { data: null, error: { ...error, message: error.message } }
236206
}
237207
},
238208

239209
async end() {
240-
const _pool = pool
241-
pool = null
242-
// Gracefully wait for active connections to be idle, then close all
243-
// connections in the pool.
244-
if (_pool) {
245-
// Remove all listeners before ending to prevent memory leaks
246-
_pool.removeAllListeners()
247-
await _pool.end()
210+
try {
211+
const _pool = pool
212+
pool = null
213+
// Gracefully wait for active connections to be idle, then close all
214+
// connections in the pool.
215+
if (_pool) {
216+
await _pool.end()
217+
}
218+
} catch (endError) {
219+
// Ignore any errors during cleanup just log them
220+
console.error('Failed ending connection pool', endError)
248221
}
249222
},
250223
}

test/server/query.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,3 +557,176 @@ test('return interval as string', async () => {
557557
]
558558
`)
559559
})
560+
561+
test('line position error', async () => {
562+
const res = await app.inject({
563+
method: 'POST',
564+
path: '/query',
565+
payload: { query: 'SELECT *\nFROM pg_class\nWHERE relname = missing_quotes;' },
566+
})
567+
expect(res.json()).toMatchInlineSnapshot(`
568+
{
569+
"code": "42703",
570+
"error": "ERROR: 42703: column "missing_quotes" does not exist
571+
LINE 3: WHERE relname = missing_quotes;
572+
^
573+
",
574+
"file": "parse_relation.c",
575+
"formattedError": "ERROR: 42703: column "missing_quotes" does not exist
576+
LINE 3: WHERE relname = missing_quotes;
577+
^
578+
",
579+
"length": 114,
580+
"line": "3589",
581+
"message": "column "missing_quotes" does not exist",
582+
"name": "error",
583+
"position": "40",
584+
"routine": "errorMissingColumn",
585+
"severity": "ERROR",
586+
}
587+
`)
588+
})
589+
590+
test('error with additional details', async () => {
591+
// This query will generate an error with details
592+
const res = await app.inject({
593+
method: 'POST',
594+
path: '/query',
595+
payload: {
596+
query: `DO $$
597+
DECLARE
598+
my_var int;
599+
BEGIN
600+
-- This will trigger an error with detail, hint, and context
601+
SELECT * INTO STRICT my_var FROM (VALUES (1), (2)) AS t(v);
602+
END $$;`,
603+
},
604+
})
605+
606+
expect(res.json()).toMatchInlineSnapshot(`
607+
{
608+
"code": "P0003",
609+
"error": "ERROR: P0003: query returned more than one row
610+
HINT: Make sure the query returns a single row, or use LIMIT 1.
611+
CONTEXT: PL/pgSQL function inline_code_block line 6 at SQL statement
612+
",
613+
"file": "pl_exec.c",
614+
"formattedError": "ERROR: P0003: query returned more than one row
615+
HINT: Make sure the query returns a single row, or use LIMIT 1.
616+
CONTEXT: PL/pgSQL function inline_code_block line 6 at SQL statement
617+
",
618+
"hint": "Make sure the query returns a single row, or use LIMIT 1.",
619+
"length": 216,
620+
"line": "4349",
621+
"message": "query returned more than one row",
622+
"name": "error",
623+
"routine": "exec_stmt_execsql",
624+
"severity": "ERROR",
625+
"where": "PL/pgSQL function inline_code_block line 6 at SQL statement",
626+
}
627+
`)
628+
})
629+
630+
test('error with all formatting properties', async () => {
631+
// This query will generate an error with all formatting properties
632+
const res = await app.inject({
633+
method: 'POST',
634+
path: '/query',
635+
payload: {
636+
query: `
637+
DO $$
638+
BEGIN
639+
-- Using EXECUTE to force internal query to appear
640+
EXECUTE 'SELECT * FROM nonexistent_table WHERE id = 1';
641+
EXCEPTION WHEN OTHERS THEN
642+
-- Re-raise with added context
643+
RAISE EXCEPTION USING
644+
ERRCODE = SQLSTATE,
645+
MESSAGE = SQLERRM,
646+
DETAIL = 'This is additional detail information',
647+
HINT = 'This is a hint for fixing the issue',
648+
SCHEMA = 'public';
649+
END $$;
650+
`,
651+
},
652+
})
653+
654+
expect(res.json()).toMatchInlineSnapshot(`
655+
{
656+
"code": "42P01",
657+
"detail": "This is additional detail information",
658+
"error": "ERROR: 42P01: relation "nonexistent_table" does not exist
659+
DETAIL: This is additional detail information
660+
HINT: This is a hint for fixing the issue
661+
CONTEXT: PL/pgSQL function inline_code_block line 7 at RAISE
662+
",
663+
"file": "pl_exec.c",
664+
"formattedError": "ERROR: 42P01: relation "nonexistent_table" does not exist
665+
DETAIL: This is additional detail information
666+
HINT: This is a hint for fixing the issue
667+
CONTEXT: PL/pgSQL function inline_code_block line 7 at RAISE
668+
",
669+
"hint": "This is a hint for fixing the issue",
670+
"length": 242,
671+
"line": "3859",
672+
"message": "relation "nonexistent_table" does not exist",
673+
"name": "error",
674+
"routine": "exec_stmt_raise",
675+
"schema": "public",
676+
"severity": "ERROR",
677+
"where": "PL/pgSQL function inline_code_block line 7 at RAISE",
678+
}
679+
`)
680+
})
681+
682+
test('error with internalQuery property', async () => {
683+
// First create a function that will execute a query internally
684+
await app.inject({
685+
method: 'POST',
686+
path: '/query',
687+
payload: {
688+
query: `
689+
CREATE OR REPLACE FUNCTION test_internal_query() RETURNS void AS $$
690+
BEGIN
691+
-- This query will be the "internal query" when it fails
692+
EXECUTE 'SELECT * FROM nonexistent_table';
693+
RETURN;
694+
END;
695+
$$ LANGUAGE plpgsql;
696+
`,
697+
},
698+
})
699+
700+
// Now call the function to trigger the error with internalQuery
701+
const res = await app.inject({
702+
method: 'POST',
703+
path: '/query',
704+
payload: {
705+
query: 'SELECT test_internal_query();',
706+
},
707+
})
708+
709+
expect(res.json()).toMatchInlineSnapshot(`
710+
{
711+
"code": "42P01",
712+
"error": "ERROR: 42P01: relation "nonexistent_table" does not exist
713+
QUERY: SELECT * FROM nonexistent_table
714+
CONTEXT: PL/pgSQL function test_internal_query() line 4 at EXECUTE
715+
",
716+
"file": "parse_relation.c",
717+
"formattedError": "ERROR: 42P01: relation "nonexistent_table" does not exist
718+
QUERY: SELECT * FROM nonexistent_table
719+
CONTEXT: PL/pgSQL function test_internal_query() line 4 at EXECUTE
720+
",
721+
"internalPosition": "15",
722+
"internalQuery": "SELECT * FROM nonexistent_table",
723+
"length": 208,
724+
"line": "1381",
725+
"message": "relation "nonexistent_table" does not exist",
726+
"name": "error",
727+
"routine": "parserOpenTable",
728+
"severity": "ERROR",
729+
"where": "PL/pgSQL function test_internal_query() line 4 at EXECUTE",
730+
}
731+
`)
732+
})

test/server/result-size-limit.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ describe('test max-result-size limit', () => {
4343
const res = await app.inject({
4444
method: 'POST',
4545
path: '/query',
46-
headers: { pg: 'postgresql://postgres:postgres@localhost:5432/postgres' },
4746
payload: { query: 'SELECT * FROM large_data;' },
4847
})
4948

@@ -60,7 +59,6 @@ describe('test max-result-size limit', () => {
6059
const nextRes = await app.inject({
6160
method: 'POST',
6261
path: '/query',
63-
headers: { pg: 'postgresql://postgres:postgres@localhost:5432/postgres' },
6462
payload: { query: 'SELECT * FROM small_data;' },
6563
})
6664

test/server/typegen.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,10 @@ test('typegen: typescript', async () => {
472472
Args: Record<PropertyKey, never>
473473
Returns: unknown
474474
}
475+
test_internal_query: {
476+
Args: Record<PropertyKey, never>
477+
Returns: undefined
478+
}
475479
}
476480
Enums: {
477481
meme_status: "new" | "old" | "retired"
@@ -1101,6 +1105,10 @@ test('typegen w/ one-to-one relationships', async () => {
11011105
Args: Record<PropertyKey, never>
11021106
Returns: unknown
11031107
}
1108+
test_internal_query: {
1109+
Args: Record<PropertyKey, never>
1110+
Returns: undefined
1111+
}
11041112
}
11051113
Enums: {
11061114
meme_status: "new" | "old" | "retired"
@@ -1730,6 +1738,10 @@ test('typegen: typescript w/ one-to-one relationships', async () => {
17301738
Args: Record<PropertyKey, never>
17311739
Returns: unknown
17321740
}
1741+
test_internal_query: {
1742+
Args: Record<PropertyKey, never>
1743+
Returns: undefined
1744+
}
17331745
}
17341746
Enums: {
17351747
meme_status: "new" | "old" | "retired"

0 commit comments

Comments
 (0)