@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.
- package/dist/postgres_connection.js +176 -108
- package/package.json +3 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
index
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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"
|