From 85e947a337deb8188eeb547ce08c7d8f0149dcde Mon Sep 17 00:00:00 2001 From: Joaquim d'Souza Date: Wed, 4 Feb 2026 11:54:22 +0100 Subject: [PATCH 1/2] feat: add read replica for expensive geocode query --- src/server/commands/ensureOrganisationMap.ts | 9 ++++-- src/server/repositories/Area.ts | 5 +-- src/server/services/database/index.ts | 32 +++++++++++++++----- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/server/commands/ensureOrganisationMap.ts b/src/server/commands/ensureOrganisationMap.ts index b1990c66..f188e65c 100644 --- a/src/server/commands/ensureOrganisationMap.ts +++ b/src/server/commands/ensureOrganisationMap.ts @@ -20,7 +20,11 @@ import { } from "@/server/repositories/MapView"; import { upsertOrganisation } from "@/server/repositories/Organisation"; import { AreaSetCode, AreaSetGroupCode } from "../models/AreaSet"; -import { ColorScheme, MapStyleName } from "../models/MapView"; +import { + ColorScheme, + DEFAULT_CALCULATION_TYPE, + MapStyleName, +} from "../models/MapView"; import { countDataRecordsForDataSource } from "../repositories/DataRecord"; import type { MapView } from "../models/MapView"; import type { DataSource } from "@/server/models/DataSource"; @@ -87,11 +91,12 @@ const ensureOrganisationMap = async (orgId: string): Promise => { areaDataColumn: "Lab", areaDataSourceId: electionResultsDataSource.id, areaSetGroupCode: AreaSetGroupCode.WMC24, - calculationType: null, + calculationType: DEFAULT_CALCULATION_TYPE, colorScheme: ColorScheme.GreenYellowRed, mapStyleName: MapStyleName.Light, reverseColorScheme: false, showBoundaryOutline: true, + showChoropleth: true, showLabels: true, showLocations: true, showMembers: true, diff --git a/src/server/repositories/Area.ts b/src/server/repositories/Area.ts index c78dc35b..7f6bada0 100644 --- a/src/server/repositories/Area.ts +++ b/src/server/repositories/Area.ts @@ -1,5 +1,5 @@ import { sql } from "kysely"; -import { db } from "@/server/services/database"; +import { db, dbRead } from "@/server/services/database"; import type { Area } from "@/server/models/Area"; import type { AreaSetCode } from "@/server/models/AreaSet"; import type { Database } from "@/server/services/database"; @@ -82,7 +82,8 @@ export async function findAreasByPoint({ excludeAreaSetCode?: AreaSetCode | null | undefined; includeAreaSetCode?: AreaSetCode | null | undefined; }): Promise { - let query = db + // Use the read replica for this expensive read query + let query = dbRead .selectFrom("area") .innerJoin("areaSet", "area.areaSetId", "areaSet.id"); if (excludeAreaSetCode) { diff --git a/src/server/services/database/index.ts b/src/server/services/database/index.ts index 6e8b3732..38175047 100644 --- a/src/server/services/database/index.ts +++ b/src/server/services/database/index.ts @@ -25,11 +25,23 @@ export const pool = new Pool({ max: Number(process.env.DATABASE_POOL_SIZE) || undefined, }); +// Set up read replica pool with graceful fallback +const readReplicaPool = new Pool({ + connectionString: + process.env.DATABASE_READ_REPLICA_URL || process.env.DATABASE_URL, + max: Number(process.env.DATABASE_POOL_SIZE) || undefined, +}); + const dialect = new PostgresDialect({ cursor: Cursor, pool, }); +const readReplicaDialect = new PostgresDialect({ + cursor: Cursor, + pool: readReplicaPool, +}); + export interface Database { airtableWebhook: AirtableWebhookTable; area: AreaTable; @@ -49,14 +61,20 @@ export interface Database { "pgboss.job": JobTable; } +const sharedPlugins = [ + new CamelCasePlugin({ maintainNestedObjectKeys: true }), + new PointPlugin(), + new JSONPlugin(), +]; + export const db = new Kysely({ dialect, - plugins: [ - // Database `field_name` to TypeScript `fieldName`. - // `maintainNestedObjectKeys` prevents `data_record.json` being mangled - new CamelCasePlugin({ maintainNestedObjectKeys: true }), - new PointPlugin(), - new JSONPlugin(), - ], + plugins: sharedPlugins, + log: ["error"], +}); + +export const dbRead = new Kysely({ + dialect: readReplicaDialect, + plugins: sharedPlugins, log: ["error"], }); From a6c9bb86884306a972b89093021e0ae30c8a6cc9 Mon Sep 17 00:00:00 2001 From: Joaquim d'Souza Date: Wed, 4 Feb 2026 17:12:13 +0100 Subject: [PATCH 2/2] feat: add area (area_set_id, geography) combined index for improved geocoding queries --- .../1770221306067_area_set_id_geography_index.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 migrations/1770221306067_area_set_id_geography_index.ts diff --git a/migrations/1770221306067_area_set_id_geography_index.ts b/migrations/1770221306067_area_set_id_geography_index.ts new file mode 100644 index 00000000..e3d8133f --- /dev/null +++ b/migrations/1770221306067_area_set_id_geography_index.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { type Kysely, sql } from "kysely"; + +export async function up(db: Kysely): Promise { + await sql`CREATE EXTENSION IF NOT EXISTS btree_gist`.execute(db); + await sql`CREATE INDEX area_area_set_id_geography_gist ON area USING GIST (area_set_id, geography)`.execute( + db, + ); +} + +export async function down(db: Kysely): Promise { + await sql`DROP INDEX IF EXISTS area_area_set_id_geography_gist`.execute(db); + await sql`DROP EXTENSION IF EXISTS btree_gist`.execute(db); +}