Skip to content

Commit 0d1f971

Browse files
committed
patch pg to handle non-UTC timestamps automatically.
- this helps a lot when the server is running on a non-UTC timezone
1 parent e186dfb commit 0d1f971

File tree

5 files changed

+81
-6
lines changed

5 files changed

+81
-6
lines changed

src/packages/database/pool/cached.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ of multiple projects.
2424

2525
import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
2626
import LRU from "lru-cache";
27-
import { Pool } from "pg";
28-
27+
import { type Pool } from "pg";
2928
import getLogger from "@cocalc/backend/logger";
3029
import getPool from "./pool";
3130

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// see https://chatgpt.com/share/68c2df30-1f30-800e-94de-38fbbcdc88bd
2+
// Basically this ensures input Date objects are formated as iso strings,
3+
// so they are interpreted as UTC time properly. The root cause is that
4+
// our database schema uses "timestamp without timezone" everywhere, and
5+
// it would be painful to migrate everything. ANY query using
6+
// pool.query('...', params)
7+
// that potentially has Date's in params should pass the params through normalizeParams.
8+
// This is taken care of automatically in getPool and the db class.
9+
10+
import type { Pool, QueryConfig } from "pg";
11+
12+
function normalizeValue(v: any): any {
13+
if (v instanceof Date) return v.toISOString();
14+
if (Array.isArray(v)) return v.map(normalizeValue);
15+
return v;
16+
}
17+
18+
export function normalizeValues(values?: any[]): any[] | undefined {
19+
return Array.isArray(values) ? values.map(normalizeValue) : values;
20+
}
21+
22+
function normalizeQueryArgs(args: any[]): any[] {
23+
// Forms:
24+
// 1) query(text)
25+
// 2) query(text, values)
26+
// 3) query(text, values, callback)
27+
// 4) query(config)
28+
// 5) query(config, callback)
29+
if (typeof args[0] === "string") {
30+
if (Array.isArray(args[1])) {
31+
const v = normalizeValues(args[1]);
32+
if (args.length === 2) return [args[0], v];
33+
// callback in position 2
34+
return [args[0], v, args[2]];
35+
}
36+
// only text (or text, callback)
37+
return args;
38+
} else {
39+
// config object path
40+
const cfg: QueryConfig = { ...args[0] };
41+
if ("values" in cfg && Array.isArray(cfg.values)) {
42+
cfg.values = normalizeValues(cfg.values)!;
43+
}
44+
if (args.length === 1) return [cfg];
45+
return [cfg, args[1]]; // callback passthrough
46+
}
47+
}
48+
49+
export function patchPoolForUtc(pool: Pool): Pool {
50+
if ((pool as any).__utcNormalized) return pool;
51+
52+
// Patch pool.query
53+
const origPoolQuery = pool.query.bind(pool);
54+
(pool as any).query = function (...args: any[]) {
55+
return origPoolQuery(...normalizeQueryArgs(args));
56+
} as typeof pool.query;
57+
58+
pool.on("connect", (client) => {
59+
if ((client as any).__utcNormalized) return;
60+
const origQuery = client.query.bind(client);
61+
client.query = function (...args: any[]) {
62+
return origQuery(...normalizeQueryArgs(args));
63+
} as typeof client.query;
64+
(client as any).__utcNormalized = true;
65+
});
66+
67+
(pool as any).__utcNormalized = true;
68+
return pool;
69+
}

src/packages/database/pool/pool.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,10 @@ import getCachedPool, { CacheTime } from "./cached";
1717
import dbPassword from "./password";
1818
import { types } from "pg";
1919
export * from "./util";
20-
20+
import { patchPoolForUtc } from "./pg-utc-normalize";
2121

2222
const L = getLogger("db:pool");
2323

24-
2524
let pool: Pool | undefined = undefined;
2625

2726
// This makes it so when we read dates out, if they are in a "timestamp with no timezone" field in the
@@ -48,6 +47,9 @@ export default function getPool(cacheTime?: CacheTime): Pool {
4847
options: "-c timezone=UTC", // ← make the session time zone UTC
4948
});
5049

50+
// make Dates always UTC ISO going in
51+
patchPoolForUtc(pool);
52+
5153
pool.on("error", (err: Error) => {
5254
L.debug("WARNING: Unexpected error on idle client in PG pool", {
5355
err: err.message,
@@ -114,6 +116,8 @@ export async function initEphemeralDatabase({
114116
ssl,
115117
options: "-c timezone=UTC", // ← make the session time zone UTC
116118
});
119+
patchPoolForUtc(db);
120+
117121
db.on("error", (err: Error) => {
118122
L.debug("WARNING: Unexpected error on idle client in PG pool", {
119123
err: err.message,

src/packages/database/postgres-base.coffee

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ winston = require('@cocalc/backend/logger').getLogger('postgres')
4141
{ quoteField } = require('./postgres/schema/util')
4242
{ primaryKey, primaryKeys } = require('./postgres/schema/table')
4343

44+
{ normalizeValues } = require('./pool/pg-utc-normalize')
45+
4446
misc_node = require('@cocalc/backend/misc_node')
4547
{ sslConfigToPsqlEnv, pghost, pgdatabase, pguser, pgssl } = require("@cocalc/backend/data")
4648

@@ -818,7 +820,7 @@ class exports.PostgreSQL extends EventEmitter # emits a 'connect' event whene
818820
dbg("run query with specific postgres parameters in a transaction")
819821
do_query_with_pg_params(client: client, query: opts.query, params: opts.params, pg_params:opts.pg_params, cb: query_cb)
820822
else
821-
client.query(opts.query, opts.params, query_cb)
823+
client.query(opts.query, normalizeValues(opts.params), query_cb)
822824

823825
catch e
824826
# this should never ever happen

src/packages/database/postgres/set-pg-params.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import { Client } from "pg";
1010
import { getLogger } from "@cocalc/backend/logger";
11+
import { normalizeValues } from "../pool/pg-utc-normalize";
1112

1213
const L = getLogger("db:set-pg-params").debug;
1314

@@ -33,7 +34,7 @@ export async function do_query_with_pg_params(opts: Opts): Promise<void> {
3334
L(`Setting query param: ${k}=${v}`);
3435
await client.query(q);
3536
}
36-
const res = await client.query(query, params);
37+
const res = await client.query(query, normalizeValues(params));
3738
await client.query("COMMIT");
3839
cb(undefined, res);
3940
} catch (err) {

0 commit comments

Comments
 (0)