diff --git a/package-lock.json b/package-lock.json index 6febd2ae..98a7f0b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "pg": "^8.7.1", "pg-connection-string": "^2.5.0", "pg-format": "^1.0.4", + "pg-protocol": "^1.6.0", "pgsql-parser": "^13.3.0", "pino": "^8.6.1", "postgres-array": "^3.0.1", diff --git a/package.json b/package.json index 3678ca7e..0e27997a 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "pg": "^8.7.1", "pg-connection-string": "^2.5.0", "pg-format": "^1.0.4", + "pg-protocol": "^1.6.0", "pgsql-parser": "^13.3.0", "pino": "^8.6.1", "postgres-array": "^3.0.1", diff --git a/src/lib/db.ts b/src/lib/db.ts index 52834b7a..4324b0be 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,4 +1,5 @@ import pg, { PoolConfig } from 'pg' +import { DatabaseError } from 'pg-protocol' import { parse as parseArray } from 'postgres-array' import { PostgresMetaResult } from './types.js' @@ -76,8 +77,76 @@ export const init: (config: PoolConfig) => { res = res.reverse().find((x) => x.rows.length !== 0) ?? { rows: [] } } return { data: res.rows, error: null } - } catch (e: any) { - return { data: null, error: { message: e.message } } + } catch (error: any) { + if (error instanceof DatabaseError) { + // Roughly based on: + // - https://github.com/postgres/postgres/blob/fc4089f3c65a5f1b413a3299ba02b66a8e5e37d0/src/interfaces/libpq/fe-protocol3.c#L1018 + // - https://github.com/brianc/node-postgres/blob/b1a8947738ce0af004cb926f79829bb2abc64aa6/packages/pg/lib/native/query.js#L33 + let formattedError = '' + { + if (error.severity) { + formattedError += `${error.severity}: ` + } + if (error.code) { + formattedError += `${error.code}: ` + } + if (error.message) { + formattedError += error.message + } + formattedError += '\n' + if (error.position) { + // error.position is 1-based + const position = Number(error.position) - 1 + + let line = '' + let lineNumber = 0 + let lineOffset = 0 + + const lines = sql.split('\n') + let currentOffset = 0 + for (let i = 0; i < lines.length; i++) { + if (currentOffset + lines[i].length > position) { + line = lines[i] + lineNumber = i + 1 // 1-based + lineOffset = position - currentOffset + break + } + currentOffset += lines[i].length + 1 // 1 extra offset for newline + } + formattedError += `LINE ${lineNumber}: ${line} +${' '.repeat(5 + lineNumber.toString().length + 2 + lineOffset)}^ +` + } + if (error.detail) { + formattedError += `DETAIL: ${error.detail} +` + } + if (error.hint) { + formattedError += `HINT: ${error.hint} +` + } + if (error.internalQuery) { + formattedError += `QUERY: ${error.internalQuery} +` + } + if (error.where) { + formattedError += `CONTEXT: ${error.where} +` + } + } + + return { + data: null, + error: { + ...error, + // error.message is non-enumerable + message: error.message, + formattedError, + }, + } + } + + return { data: null, error: { message: error.message } } } }, diff --git a/src/lib/types.ts b/src/lib/types.ts index 0e55b35a..333ce631 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,4 +1,5 @@ import { Static, Type } from '@sinclair/typebox' +import { DatabaseError } from 'pg-protocol' import { Options as PrettierOptions } from 'prettier' export interface FormatterOptions extends PrettierOptions {} @@ -10,9 +11,7 @@ export interface PostgresMetaOk { export interface PostgresMetaErr { data: null - error: { - message: string - } + error: Partial & { message: string; formattedError?: string } } export type PostgresMetaResult = PostgresMetaOk | PostgresMetaErr diff --git a/test/lib/query.ts b/test/lib/query.ts index f71ff187..9fe5ab0d 100644 --- a/test/lib/query.ts +++ b/test/lib/query.ts @@ -27,7 +27,27 @@ test('error', async () => { { "data": null, "error": { + "code": "42P01", + "column": undefined, + "constraint": undefined, + "dataType": undefined, + "detail": undefined, + "file": "tablecmds.c", + "formattedError": "ERROR: 42P01: table "missing_table" does not exist + ", + "hint": undefined, + "internalPosition": undefined, + "internalQuery": undefined, + "length": 108, + "line": "1259", "message": "table "missing_table" does not exist", + "name": "error", + "position": undefined, + "routine": "DropErrorMsgNonExistent", + "schema": undefined, + "severity": "ERROR", + "table": undefined, + "where": undefined, }, } `)