Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
it back in. With the permissions the startup check and invite URL ask for,
every new ticket failed with *Missing Access* and the user got "an internal
error has occurred". The bot now adds an overwrite for itself.
- **Transcripts work on PostgreSQL.** pg returns `BIGINT` columns as strings,
so the ticket timestamps reached the transcript as text and every transcript
failed with `RangeError: Invalid time value` (no file in the log channel,
nothing stored). The PostgreSQL pool now returns `BIGINT` as a number, like
SQLite and MySQL do.

### Added

Expand Down
15 changes: 13 additions & 2 deletions src/database/drivers/postgres.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,20 @@
* @param {{ options: object }} descriptor
*/
function createPostgresDriver(descriptor) {
const { Pool } = require('pg');
const { Pool, types } = require('pg');
let pool;

// pg returns BIGINT (int8, OID 20) as a string, SQLite and mysql2 return a
// number. Every BIGINT column here is a millisecond timestamp or a row id,
// well inside Number.MAX_SAFE_INTEGER (Discord ids are VARCHAR), so parse
// them as numbers for this pool only. As strings, new Date(created_at) was
// an Invalid Date and every transcript failed to build.
const INT8_OID = 20;
const typeParsers = {
getTypeParser: (oid, format) =>
(oid === INT8_OID && format !== 'binary') ? Number : types.getTypeParser(oid, format),
};

const norm = (params) => (params || []).map(p => (p === undefined ? null : p));

// ? → $1, $2, $3 …
Expand All @@ -22,7 +33,7 @@ function createPostgresDriver(descriptor) {
async connect() {
// connectionTimeoutMillis: without it a connect to an unreachable host can
// hang indefinitely instead of failing the query.
pool = new Pool({ connectionTimeoutMillis: 10_000, ...descriptor.options, max: 10 });
pool = new Pool({ connectionTimeoutMillis: 10_000, ...descriptor.options, max: 10, types: typeParsers });

// An idle client that loses its connection (Postgres restart, network blip)
// emits 'error' on the pool. Unhandled, that event crashes the process; the
Expand Down
30 changes: 30 additions & 0 deletions tests/postgres-int8.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* pg returns BIGINT as a string. The timestamps are BIGINT, and a string
* timestamp is an Invalid Date (every transcript failed on PostgreSQL), so the
* driver's pool parses int8 as a number. No server needed: pg is replaced
* with a fake that records the pool config.
*/
const test = require('node:test');
const assert = require('node:assert/strict');

const realPg = require('pg');
let poolConfig;
class FakePool {
constructor(config) { poolConfig = config; }
on() {}
async connect() { return { release() {} }; }
}
require.cache[require.resolve('pg')].exports = { ...realPg, Pool: FakePool };

const { createPostgresDriver } = require('../src/database/drivers/postgres');

test('BIGINT (int8) comes back as a number, other types are unchanged', async () => {
await createPostgresDriver({ options: {} }).connect();

const parse = (oid) => poolConfig.types.getTypeParser(oid, 'text');
assert.equal(parse(20)('1792781224429'), 1792781224429);
assert.ok(!Number.isNaN(new Date(parse(20)('1792781224429')).getTime()));
// NUMERIC (AVG results) and TEXT keep pg's defaults.
assert.equal(parse(1700)('4.50'), '4.50');
assert.equal(parse(25)('1558357487801999400'), '1558357487801999400');
});
Loading