@malloydata/db-postgres 0.0.434 → 0.0.435

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,23 @@ function addTlsHint(err) {
32
32
  }
33
33
  return err;
34
34
  }
35
+ // Closes an unpooled client without making the caller wait for it. In pg 8.7.3,
36
+ // end() resolves on the connection's next 'end' event and never checks whether
37
+ // one already fired, so after the socket has closed on its own (the server or
38
+ // the network dropped it) an awaited end() never returns.
39
+ function closeClient(client) {
40
+ client.end().catch(() => { });
41
+ }
42
+ // Fails a row stream when the connection under it drops. pg-query-stream 4.2.3
43
+ // tears a failed stream down by closing its cursor, which waits for a server
44
+ // reply that a dead connection never sends, so without this the stream neither
45
+ // ends nor errors. Returns a function that detaches the listener; call it before
46
+ // the client is closed or released.
47
+ function failStreamOnClientError(client, stream) {
48
+ const onError = (err) => stream.emit('error', err);
49
+ client.on('error', onError);
50
+ return () => client.removeListener('error', onError);
51
+ }
35
52
  /**
36
53
  * Decode a canonical Postgres dotted-table path into its underlying
37
54
  * identifier strings as they appear in `information_schema`. The schema
@@ -134,26 +151,36 @@ class PostgresConnection extends connection_1.BaseConnection {
134
151
  }
135
152
  }
136
153
  async getClient() {
137
- return new pg_1.Client(this.buildClientConfig(await this.readConfig()));
154
+ const client = new pg_1.Client(this.buildClientConfig(await this.readConfig()));
155
+ // When the socket drops, pg emits 'error' on the client whether or not a
156
+ // query is running. A running query fails on its own (a row stream through
157
+ // failStreamOnClientError); without this listener the event itself would
158
+ // be an uncaught exception.
159
+ client.on('error', () => { });
160
+ return client;
138
161
  }
139
162
  async runPostgresQuery(sqlCommand, _pageSize, _rowIndex, deJSON, values) {
140
163
  const client = await this.getClient();
141
- await this.withTlsHint(() => client.connect());
142
- await this.connectionSetup(client);
143
- let result = await client.query(sqlCommand, values);
144
- if (Array.isArray(result)) {
145
- result = result.pop();
146
- }
147
- if (deJSON) {
148
- for (let i = 0; i < result.rows.length; i++) {
149
- result.rows[i] = result.rows[i].row;
164
+ try {
165
+ await this.withTlsHint(() => client.connect());
166
+ await this.connectionSetup(client);
167
+ let result = await client.query(sqlCommand, values);
168
+ if (Array.isArray(result)) {
169
+ result = result.pop();
170
+ }
171
+ if (deJSON) {
172
+ for (let i = 0; i < result.rows.length; i++) {
173
+ result.rows[i] = result.rows[i].row;
174
+ }
150
175
  }
176
+ return {
177
+ rows: result.rows,
178
+ totalRows: result.rows.length,
179
+ };
180
+ }
181
+ finally {
182
+ closeClient(client);
151
183
  }
152
- await client.end();
153
- return {
154
- rows: result.rows,
155
- totalRows: result.rows.length,
156
- };
157
184
  }
158
185
  async fetchSelectSchema(sqlRef) {
159
186
  const structDef = {
@@ -164,23 +191,24 @@ class PostgresConnection extends connection_1.BaseConnection {
164
191
  name: (0, malloy_1.sqlKey)(sqlRef.connection, sqlRef.selectStr),
165
192
  };
166
193
  const client = await this.getClient();
167
- await this.withTlsHint(() => client.connect());
168
- await this.connectionSetup(client);
169
- // 1) Get row-descriptor without fetching data
170
- const res = await client.query({
171
- text: `SELECT * FROM (${sqlRef.selectStr}) _t LIMIT 0`,
172
- });
173
- // 2) Resolve every OID we might touch (field, array element, domain base)
174
- const neededOids = new Set();
175
- res.fields.forEach(f => neededOids.add(f.dataTypeID));
176
- // we'll add more OIDs later (typelem / typebasetype) lazily
177
- // helper to fetch pg_type rows on demand, with cache
178
- const pgTypeCache = new Map();
179
- const loadTypes = async (oids) => {
180
- if (oids.length === 0)
181
- return;
182
- const params = oids.map((_, i) => `$${i + 1}`).join(',');
183
- const { rows } = await client.query(`
194
+ try {
195
+ await this.withTlsHint(() => client.connect());
196
+ await this.connectionSetup(client);
197
+ // 1) Get row-descriptor without fetching data
198
+ const res = await client.query({
199
+ text: `SELECT * FROM (${sqlRef.selectStr}) _t LIMIT 0`,
200
+ });
201
+ // 2) Resolve every OID we might touch (field, array element, domain base)
202
+ const neededOids = new Set();
203
+ res.fields.forEach(f => neededOids.add(f.dataTypeID));
204
+ // we'll add more OIDs later (typelem / typebasetype) lazily
205
+ // helper to fetch pg_type rows on demand, with cache
206
+ const pgTypeCache = new Map();
207
+ const loadTypes = async (oids) => {
208
+ if (oids.length === 0)
209
+ return;
210
+ const params = oids.map((_, i) => `$${i + 1}`).join(',');
211
+ const { rows } = await client.query(`
184
212
  SELECT
185
213
  oid,
186
214
  typname,
@@ -192,65 +220,68 @@ class PostgresConnection extends connection_1.BaseConnection {
192
220
  FROM pg_type
193
221
  WHERE oid IN (${params})
194
222
  `, oids);
195
- rows.forEach(r => pgTypeCache.set(r.oid, r));
196
- };
197
- // Prime the cache
198
- await loadTypes([...neededOids]);
199
- // 3) recursive mapper → info-schema compliant strings
200
- const mapDataType = async (oid) => {
201
- let t = pgTypeCache.get(oid);
202
- if (!t) {
203
- await loadTypes([oid]);
204
- t = pgTypeCache.get(oid);
223
+ rows.forEach(r => pgTypeCache.set(r.oid, r));
224
+ };
225
+ // Prime the cache
226
+ await loadTypes([...neededOids]);
227
+ // 3) recursive mapper → info-schema compliant strings
228
+ const mapDataType = async (oid) => {
229
+ let t = pgTypeCache.get(oid);
230
+ if (!t) {
231
+ await loadTypes([oid]);
232
+ t = pgTypeCache.get(oid);
233
+ }
234
+ // ARRAY?
235
+ if (t.typcategory === 'A')
236
+ return 'ARRAY';
237
+ // DOMAIN? recurse to its base type
238
+ if (t.typtype === 'd')
239
+ return mapDataType(t.typbasetype);
240
+ // ENUM, COMPOSITE, RANGE, MULTIRANGE, PSEUDO → USER-DEFINED
241
+ if (['e', 'c', 'r', 'm', 'p'].includes(t.typtype))
242
+ return 'USER-DEFINED';
243
+ // built-in scalar or base type of domain
244
+ return t.formatted;
245
+ };
246
+ // helper to resolve element_type (NULL for scalars)
247
+ const mapElementType = async (oid) => {
248
+ let t = pgTypeCache.get(oid);
249
+ if (!t) {
250
+ await loadTypes([oid]);
251
+ t = pgTypeCache.get(oid);
252
+ }
253
+ if (t.typcategory !== 'A')
254
+ return null; // not an array
255
+ // Ensure element row cached
256
+ if (!pgTypeCache.has(t.typelem))
257
+ await loadTypes([t.typelem]);
258
+ return mapDataType(t.typelem);
259
+ };
260
+ // 4) Build final array in original column order
261
+ const result = [];
262
+ for (const field of res.fields) {
263
+ result.push({
264
+ columnName: field.name,
265
+ dataType: await mapDataType(field.dataTypeID),
266
+ elementType: await mapElementType(field.dataTypeID),
267
+ });
205
268
  }
206
- // ARRAY?
207
- if (t.typcategory === 'A')
208
- return 'ARRAY';
209
- // DOMAIN? recurse to its base type
210
- if (t.typtype === 'd')
211
- return mapDataType(t.typbasetype);
212
- // ENUM, COMPOSITE, RANGE, MULTIRANGE, PSEUDO → USER-DEFINED
213
- if (['e', 'c', 'r', 'm', 'p'].includes(t.typtype))
214
- return 'USER-DEFINED';
215
- // built-in scalar or base type of domain
216
- return t.formatted;
217
- };
218
- // helper to resolve element_type (NULL for scalars)
219
- const mapElementType = async (oid) => {
220
- let t = pgTypeCache.get(oid);
221
- if (!t) {
222
- await loadTypes([oid]);
223
- t = pgTypeCache.get(oid);
269
+ for (const row of result) {
270
+ const postgresDataType = row.dataType;
271
+ const name = row.columnName;
272
+ if (postgresDataType === 'ARRAY') {
273
+ const elementType = this.dialect.sqlTypeToMalloyType(row.elementType);
274
+ structDef.fields.push((0, malloy_1.mkArrayDef)(elementType, name));
275
+ }
276
+ else {
277
+ const malloyType = this.dialect.sqlTypeToMalloyType(postgresDataType);
278
+ structDef.fields.push({ ...malloyType, name });
279
+ }
224
280
  }
225
- if (t.typcategory !== 'A')
226
- return null; // not an array
227
- // Ensure element row cached
228
- if (!pgTypeCache.has(t.typelem))
229
- await loadTypes([t.typelem]);
230
- return mapDataType(t.typelem);
231
- };
232
- // 4) Build final array in original column order
233
- const result = [];
234
- for (const field of res.fields) {
235
- result.push({
236
- columnName: field.name,
237
- dataType: await mapDataType(field.dataTypeID),
238
- elementType: await mapElementType(field.dataTypeID),
239
- });
240
281
  }
241
- for (const row of result) {
242
- const postgresDataType = row.dataType;
243
- const name = row.columnName;
244
- if (postgresDataType === 'ARRAY') {
245
- const elementType = this.dialect.sqlTypeToMalloyType(row.elementType);
246
- structDef.fields.push((0, malloy_1.mkArrayDef)(elementType, name));
247
- }
248
- else {
249
- const malloyType = this.dialect.sqlTypeToMalloyType(postgresDataType);
250
- structDef.fields.push({ ...malloyType, name });
251
- }
282
+ finally {
283
+ closeClient(client);
252
284
  }
253
- await client.end();
254
285
  return structDef;
255
286
  }
256
287
  async schemaFromQuery(infoQuery, structDef, values) {
@@ -328,20 +359,27 @@ class PostgresConnection extends connection_1.BaseConnection {
328
359
  const { rowLimit, abortSignal } = options;
329
360
  const query = new pg_query_stream_1.default(this.sqlWithQueryMetadata(sqlCommand, options.queryMetadata));
330
361
  const client = await this.getClient();
331
- await this.withTlsHint(() => client.connect());
332
- await this.connectionSetup(client);
333
- const rowStream = client.query(query);
334
- let index = 0;
335
- for await (const row of rowStream) {
336
- yield row.row;
337
- index += 1;
338
- if ((rowLimit !== undefined && index >= rowLimit) ||
339
- (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted)) {
340
- query.destroy();
341
- break;
362
+ let detach = () => { };
363
+ try {
364
+ await this.withTlsHint(() => client.connect());
365
+ await this.connectionSetup(client);
366
+ const rowStream = client.query(query);
367
+ detach = failStreamOnClientError(client, rowStream);
368
+ let index = 0;
369
+ for await (const row of rowStream) {
370
+ yield row.row;
371
+ index += 1;
372
+ if ((rowLimit !== undefined && index >= rowLimit) ||
373
+ (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted)) {
374
+ query.destroy();
375
+ break;
376
+ }
342
377
  }
343
378
  }
344
- await client.end();
379
+ finally {
380
+ detach();
381
+ closeClient(client);
382
+ }
345
383
  }
346
384
  async estimateQueryCost(_) {
347
385
  return {};
@@ -378,6 +416,11 @@ class PooledPostgresConnection extends PostgresConnection {
378
416
  async getPool() {
379
417
  if (!this._pool) {
380
418
  this._pool = new pg_1.Pool(this.buildClientConfig(await this.readConfig()));
419
+ // An idle client whose socket drops reaches the pool as an 'error' event.
420
+ // pg-pool has already removed the dead client by then, and there is no
421
+ // caller to report to, so there is nothing to act on; without a listener
422
+ // the event would be an uncaught exception.
423
+ this._pool.on('error', () => { });
381
424
  this._pool.on('acquire', client => {
382
425
  client.query("SET TIME ZONE 'UTC'");
383
426
  if (this.setupSQL) {
@@ -418,17 +461,42 @@ class PooledPostgresConnection extends PostgresConnection {
418
461
  // `client.query(query)`, which does what it's supposed to.
419
462
  const pool = await this.getPool();
420
463
  const client = await this.withTlsHint(() => pool.connect());
421
- const resultStream = client.query(query);
422
- for await (const row of resultStream) {
423
- yield row.row;
424
- index += 1;
425
- if ((rowLimit !== undefined && index >= rowLimit) ||
426
- (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted)) {
427
- query.destroy();
428
- break;
464
+ // pg-pool takes its own 'error' listener off a client while it is checked
465
+ // out, so this listener is also what keeps a dropped connection from
466
+ // surfacing as an uncaught exception.
467
+ const detach = failStreamOnClientError(client, query);
468
+ let drained = false;
469
+ try {
470
+ const resultStream = client.query(query);
471
+ for await (const row of resultStream) {
472
+ yield row.row;
473
+ index += 1;
474
+ if ((rowLimit !== undefined && index >= rowLimit) ||
475
+ (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted)) {
476
+ // The rows of a result that fits in one batch arrive with its end.
477
+ // Once pg-cursor 2.7.3 has that end its state is 'done': no fetch is
478
+ // in flight and closing it sends nothing, so the client can be reused.
479
+ drained = query.cursor.state === 'done';
480
+ query.destroy();
481
+ return;
482
+ }
429
483
  }
484
+ drained = true;
485
+ }
486
+ finally {
487
+ // release(), not end(): this client came from pool.connect(), so it goes
488
+ // back to the pool rather than closing its session. Nothing else returns
489
+ // it on a throw or an early consumer exit, and a client that is never
490
+ // released holds one of the pool's `max` slots for good.
491
+ //
492
+ // A stream that stopped before its end can leave a fetch in flight on
493
+ // the client, so it is released with an error, which makes the pool
494
+ // discard it instead of handing the next caller a client mid-query.
495
+ detach();
496
+ client.release(drained
497
+ ? undefined
498
+ : new Error('row stream stopped before reading all its rows'));
430
499
  }
431
- client.release();
432
500
  }
433
501
  async close() {
434
502
  await this.drain();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/db-postgres",
3
- "version": "0.0.434",
3
+ "version": "0.0.435",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,7 +15,7 @@
15
15
  "scripts": {
16
16
  "lint": "eslint '**/*.ts{,x}'",
17
17
  "lint-fix": "eslint '**/*.ts{,x}' --fix",
18
- "test": "jest --config=../../jest.config.js",
18
+ "test": "cd ../.. && ts-node scripts/check_dialect_connection.ts postgres && jest packages/malloy-db-postgres",
19
19
  "build": "tsc --build",
20
20
  "dev": "tsc --build",
21
21
  "clean": "tsc --build --clean && rm -f tsconfig.tsbuildinfo",
@@ -23,7 +23,7 @@
23
23
  "prepublishOnly": "npm run build"
24
24
  },
25
25
  "dependencies": {
26
- "@malloydata/malloy": "0.0.434",
26
+ "@malloydata/malloy": "0.0.435",
27
27
  "@types/pg": "^8.6.1",
28
28
  "pg": "8.7.3",
29
29
  "pg-query-stream": "4.2.3"