Skip to content

Commit 90aabe6

Browse files
committed
perf(gtfs): cache service_days matview existence per schema
getDepartures/getArrivals/getDeparturesByDate each ran a pg_matviews EXISTS check on every call. Cache it per schema, mirroring the existing hasOriginalStopIdColumn pattern, invalidated by invalidateSchemaCaches on feed (re)import.
1 parent 288ab94 commit 90aabe6

2 files changed

Lines changed: 80 additions & 28 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const unsafe = vi.fn();
4+
vi.mock("./db", () => ({ sql: { unsafe: (...args: unknown[]) => unsafe(...args) } }));
5+
6+
function isMatviewCheck(query: string): boolean {
7+
return query.includes("pg_matviews");
8+
}
9+
10+
beforeEach(() => {
11+
unsafe.mockReset();
12+
// Matview check → exists; any other query → empty result set.
13+
unsafe.mockImplementation(async (query: string) =>
14+
isMatviewCheck(query) ? [{ exists: true }] : [],
15+
);
16+
});
17+
18+
afterEach(() => {
19+
vi.resetModules();
20+
});
21+
22+
describe("service_days matview existence cache", () => {
23+
it("checks pg_matviews once per schema across repeated departure queries", async () => {
24+
vi.resetModules();
25+
const q = await import("./queries");
26+
await q.getDepartures("gtfs_ch", "stop-1", 60);
27+
await q.getDepartures("gtfs_ch", "stop-1", 60);
28+
await q.getArrivals("gtfs_ch", "stop-1", 60);
29+
30+
const matviewCalls = unsafe.mock.calls.filter(([query]) => isMatviewCheck(query as string));
31+
expect(matviewCalls).toHaveLength(1);
32+
});
33+
34+
it("re-checks pg_matviews after invalidateSchemaCaches", async () => {
35+
vi.resetModules();
36+
const q = await import("./queries");
37+
await q.getDepartures("gtfs_ch", "stop-1", 60);
38+
q.invalidateSchemaCaches("gtfs_ch");
39+
await q.getDepartures("gtfs_ch", "stop-1", 60);
40+
41+
const matviewCalls = unsafe.mock.calls.filter(([query]) => isMatviewCheck(query as string));
42+
expect(matviewCalls).toHaveLength(2);
43+
});
44+
45+
it("caches the negative result (matview missing → empty, no re-check)", async () => {
46+
unsafe.mockImplementation(async (query: string) =>
47+
isMatviewCheck(query) ? [{ exists: false }] : [],
48+
);
49+
vi.resetModules();
50+
const q = await import("./queries");
51+
expect(await q.getDepartures("gtfs_none", "stop-1", 60)).toEqual([]);
52+
expect(await q.getDepartures("gtfs_none", "stop-1", 60)).toEqual([]);
53+
54+
const matviewCalls = unsafe.mock.calls.filter(([query]) => isMatviewCheck(query as string));
55+
expect(matviewCalls).toHaveLength(1);
56+
});
57+
});

apps/api/src/services/gtfs/queries.ts

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -140,16 +140,7 @@ export async function getDepartures(
140140
minutes: number,
141141
): Promise<GtfsDepartureRow[]> {
142142
assertValidGtfsSchema(schema);
143-
// Check if service_days view exists
144-
const viewCheck = await sql.unsafe(
145-
`
146-
SELECT EXISTS (
147-
SELECT 1 FROM pg_matviews WHERE schemaname = $1 AND matviewname = 'service_days'
148-
) as exists
149-
`,
150-
[schema],
151-
);
152-
if (!viewCheck[0]?.exists) return [];
143+
if (!(await hasServiceDaysView(schema))) return [];
153144

154145
const rows = await sql.unsafe(
155146
`
@@ -189,15 +180,7 @@ export async function getArrivals(
189180
minutes: number,
190181
): Promise<GtfsDepartureRow[]> {
191182
assertValidGtfsSchema(schema);
192-
const viewCheck = await sql.unsafe(
193-
`
194-
SELECT EXISTS (
195-
SELECT 1 FROM pg_matviews WHERE schemaname = $1 AND matviewname = 'service_days'
196-
) as exists
197-
`,
198-
[schema],
199-
);
200-
if (!viewCheck[0]?.exists) return [];
183+
if (!(await hasServiceDaysView(schema))) return [];
201184

202185
const rows = await sql.unsafe(
203186
`
@@ -258,15 +241,7 @@ export async function getDeparturesByDate(
258241
date: string,
259242
): Promise<GtfsDepartureRow[]> {
260243
assertValidGtfsSchema(schema);
261-
const viewCheck = await sql.unsafe(
262-
`
263-
SELECT EXISTS (
264-
SELECT 1 FROM pg_matviews WHERE schemaname = $1 AND matviewname = 'service_days'
265-
) as exists
266-
`,
267-
[schema],
268-
);
269-
if (!viewCheck[0]?.exists) return [];
244+
if (!(await hasServiceDaysView(schema))) return [];
270245

271246
const rows = await sql.unsafe(
272247
`
@@ -324,10 +299,30 @@ export async function getSchemaStats(
324299
}
325300

326301
const schemaStopOriginalIdSupport = new Map<string, boolean>();
302+
const schemaServiceDaysSupport = new Map<string, boolean>();
327303

328304
/** Invalidate per-schema metadata caches. Call before/after a schema is dropped or re-imported. */
329305
export function invalidateSchemaCaches(schema: string): void {
330306
schemaStopOriginalIdSupport.delete(schema);
307+
schemaServiceDaysSupport.delete(schema);
308+
}
309+
310+
async function hasServiceDaysView(schema: string): Promise<boolean> {
311+
assertValidGtfsSchema(schema);
312+
const cached = schemaServiceDaysSupport.get(schema);
313+
if (cached !== undefined) return cached;
314+
315+
const rows = await sql.unsafe(
316+
`
317+
SELECT EXISTS (
318+
SELECT 1 FROM pg_matviews WHERE schemaname = $1 AND matviewname = 'service_days'
319+
) as exists
320+
`,
321+
[schema],
322+
);
323+
const exists = rows[0]?.exists === true;
324+
schemaServiceDaysSupport.set(schema, exists);
325+
return exists;
331326
}
332327

333328
async function hasOriginalStopIdColumn(schema: string): Promise<boolean> {

0 commit comments

Comments
 (0)