Skip to content

Commit a5198d5

Browse files
committed
feat(postgres-driver): improve DX with connection error messages
Introduce PostgresError/ConnectionError classes and include the pool name in connection failures so users can identify which data source/pool failed. Pool name now encodes dataSource and preAggregations, and driver throws typed errors instead of generic Error.
1 parent 56c2f40 commit a5198d5

5 files changed

Lines changed: 47 additions & 16 deletions

File tree

packages/cubejs-backend-shared/src/pool.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77
import genericPool, { Pool as GenericPool, Factory, Options } from 'generic-pool';
88

99
export class PoolTimeoutError extends Error {
10-
public readonly poolName: string;
10+
public readonly poolName: string = 'PoolTimeoutError';
1111

1212
public constructor(poolName: string) {
13-
super(`ResourceRequest timed out (pool: ${poolName})`);
14-
this.name = 'PoolTimeoutError';
13+
super(`ResourceRequest timed out (${poolName})`);
1514
this.poolName = poolName;
1615
}
1716
}

packages/cubejs-base-driver/src/BaseDriver.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,14 @@ const DbTypeValueMatcher: Record<string, ((v: any) => boolean)> = {
147147
text: () => true
148148
};
149149

150-
/**
151-
* Base driver class.
152-
*/
150+
export function createPoolName(driverName: string, dataSource: string, preAggregations: boolean = false): string {
151+
if (preAggregations) {
152+
return `${driverName}#${dataSource}@preAggregations`;
153+
}
154+
155+
return `${driverName}#${dataSource}`;
156+
}
157+
153158
export abstract class BaseDriver implements DriverInterface {
154159
private readonly testConnectionTimeoutValue: number = 10000;
155160

packages/cubejs-postgres-driver/src/PostgresDriver.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import {
1313
BaseDriver,
1414
DownloadQueryResultsOptions, DownloadTableMemoryData, DriverInterface,
1515
GenericDataBaseType, IndexesSQL, TableStructure, StreamOptions,
16-
StreamTableDataWithTypes, QueryOptions, DownloadQueryResultsResult, DriverCapabilities, TableColumn,
16+
StreamTableDataWithTypes, QueryOptions, DownloadQueryResultsResult, DriverCapabilities, TableColumn, createPoolName,
1717
} from '@cubejs-backend/base-driver';
1818
import { QueryStream } from './QueryStream';
1919
import { PgClient, PgClientConfig } from './PgClient';
20+
import { ConnectionError, PostgresError } from './errors';
2021

2122
const GenericTypeToPostgres: Record<GenericDataBaseType, string> = {
2223
string: 'text',
@@ -138,8 +139,9 @@ export class PostgresDriver<Config extends PostgresDriverConfiguration = Postgre
138139
...config
139140
};
140141

141-
this.pool = new Pool<PgClient>('postgres', {
142-
create: async () => this.createConnection(poolConfig),
142+
const poolName = createPoolName('postgres', dataSource, preAggregations);
143+
this.pool = new Pool<PgClient>(poolName, {
144+
create: async () => this.createConnection(poolConfig, poolName),
143145
validate: async (client) => {
144146
if (client.isEnding() || client.isEnded()) {
145147
return false;
@@ -180,10 +182,15 @@ export class PostgresDriver<Config extends PostgresDriverConfiguration = Postgre
180182
this.enabled = true;
181183
}
182184

183-
protected async createConnection(poolConfig: PgClientConfig): Promise<PgClient> {
185+
protected async createConnection(poolConfig: PgClientConfig, poolName: string): Promise<PgClient> {
184186
const client = new PgClient(poolConfig);
185187
client.on('error', (err) => this.databasePoolError(err));
186-
await client.connect();
188+
189+
try {
190+
await client.connect();
191+
} catch (e: unknown) {
192+
throw new ConnectionError(e as Error, poolName);
193+
}
187194

188195
return client;
189196
}
@@ -275,7 +282,7 @@ export class PostgresDriver<Config extends PostgresDriverConfiguration = Postgre
275282
await conn.query('SELECT $1::int AS number', ['1']);
276283
} catch (e) {
277284
if ((e as Error).toString().indexOf('no pg_hba.conf entry for host') !== -1) {
278-
throw new Error(`Please use CUBEJS_DB_SSL=true to connect: ${(e as Error).toString()}`);
285+
throw new PostgresError(`Please use CUBEJS_DB_SSL=true to connect: ${(e as Error).toString()}`, { cause: e as Error });
279286
}
280287

281288
throw e;
@@ -328,7 +335,7 @@ export class PostgresDriver<Config extends PostgresDriverConfiguration = Postgre
328335
return fields.map((f) => {
329336
const postgresType = this.getPostgresTypeForField(f.dataTypeID);
330337
if (!postgresType) {
331-
throw new Error(
338+
throw new PostgresError(
332339
`Unable to detect type for field "${f.name}" with dataTypeID: ${f.dataTypeID}`
333340
);
334341
}
@@ -384,7 +391,7 @@ export class PostgresDriver<Config extends PostgresDriverConfiguration = Postgre
384391
// See https://github.com/brianc/node-postgres/blob/92cb640fd316972e323ced6256b2acd89b1b58e0/packages/pg-protocol/src/buffer-writer.ts#L32-L37
385392
const length = (values?.length ?? 0);
386393
if (length >= 65536) {
387-
throw new Error(`PostgreSQL protocol does not support more than 65535 parameters, but ${length} passed`);
394+
throw new PostgresError(`PostgreSQL protocol does not support more than 65535 parameters, but ${length} passed`);
388395
}
389396
}
390397

@@ -417,7 +424,7 @@ export class PostgresDriver<Config extends PostgresDriverConfiguration = Postgre
417424

418425
public async createTable(quotedTableName: string, columns: TableColumn[]): Promise<void> {
419426
if (quotedTableName.length > 63) {
420-
throw new Error('PostgreSQL can not work with table names longer than 63 symbols. ' +
427+
throw new PostgresError('PostgreSQL can not work with table names longer than 63 symbols. ' +
421428
`Consider using the 'sqlAlias' attribute in your cube definition for ${quotedTableName}.`);
422429
}
423430
return super.createTable(quotedTableName, columns);
@@ -460,7 +467,7 @@ export class PostgresDriver<Config extends PostgresDriverConfiguration = Postgre
460467
indexesSql: IndexesSQL
461468
) {
462469
if (!tableData.rows) {
463-
throw new Error(`${this.constructor} driver supports only rows upload`);
470+
throw new PostgresError(`${this.constructor} driver supports only rows upload`);
464471
}
465472

466473
await this.createTable(table, columns);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export class PostgresError extends Error {
2+
public name = 'PostgresError';
3+
4+
public constructor(message: string, options?: ErrorOptions) {
5+
super(message, options);
6+
}
7+
}
8+
9+
export class ConnectionError extends PostgresError {
10+
public readonly name = 'ConnectionError';
11+
12+
public constructor(cause: Error, poolName: string) {
13+
const message = cause instanceof AggregateError
14+
? cause.errors.map((e: Error) => e.message).join(', ')
15+
: cause.message;
16+
17+
super(`Unable to connect to the database (${poolName}): ${message}`, { cause });
18+
}
19+
}

packages/cubejs-postgres-driver/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ import { PostgresDriver } from './PostgresDriver';
22

33
export * from './PostgresDriver';
44
export * from './PgClient';
5+
export * from './errors';
56

67
export default PostgresDriver;

0 commit comments

Comments
 (0)