From 575986a4486aa1a486b6da77b01a3e2e99f5dc51 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:24:01 -0700 Subject: [PATCH 1/2] perf(db): add published_at composite index for frontend listing queries Adds idx_{table}_deleted_published_id on (deleted_at, published_at DESC, id DESC) to all ec_* content tables. The frontend listing query orders by published_at but no existing index covers it, causing full table scans on D1 (~15,883 rows per query on a 16k post site). Also registers migration 033 (optimize_content_indexes) which existed on disk but was missing from the StaticMigrationProvider. Fixes #277 --- .../migrations/034_published_at_index.ts | 29 +++++++++++++++++++ .../core/src/database/migrations/runner.ts | 2 ++ packages/core/src/schema/registry.ts | 5 ++++ 3 files changed, 36 insertions(+) create mode 100644 packages/core/src/database/migrations/034_published_at_index.ts diff --git a/packages/core/src/database/migrations/034_published_at_index.ts b/packages/core/src/database/migrations/034_published_at_index.ts new file mode 100644 index 000000000..d7cbc6750 --- /dev/null +++ b/packages/core/src/database/migrations/034_published_at_index.ts @@ -0,0 +1,29 @@ +import type { Kysely } from "kysely"; +import { sql } from "kysely"; + +import { listTablesLike } from "../dialect-helpers.js"; + +export async function up(db: Kysely): Promise { + const tableNames = await listTablesLike(db, "ec_%"); + + for (const tableName of tableNames) { + const table = { name: tableName }; + + await sql` + CREATE INDEX ${sql.ref(`idx_${table.name}_deleted_published_id`)} + ON ${sql.ref(table.name)} (deleted_at, published_at DESC, id DESC) + `.execute(db); + } +} + +export async function down(db: Kysely): Promise { + const tableNames = await listTablesLike(db, "ec_%"); + + for (const tableName of tableNames) { + const table = { name: tableName }; + + await sql`DROP INDEX IF EXISTS ${sql.ref(`idx_${table.name}_deleted_published_id`)}`.execute( + db, + ); + } +} diff --git a/packages/core/src/database/migrations/runner.ts b/packages/core/src/database/migrations/runner.ts index 838c6b38a..51d43621d 100644 --- a/packages/core/src/database/migrations/runner.ts +++ b/packages/core/src/database/migrations/runner.ts @@ -34,6 +34,7 @@ import * as m030 from "./030_widen_scheduled_index.js"; import * as m031 from "./031_bylines.js"; import * as m032 from "./032_rate_limits.js"; import * as m033 from "./033_optimize_content_indexes.js"; +import * as m034 from "./034_published_at_index.js"; const MIGRATIONS: Readonly> = Object.freeze({ "001_initial": m001, @@ -68,6 +69,7 @@ const MIGRATIONS: Readonly> = Object.freeze({ "031_bylines": m031, "032_rate_limits": m032, "033_optimize_content_indexes": m033, + "034_published_at_index": m034, }); /** Total number of registered migrations. Exported for use in tests. */ diff --git a/packages/core/src/schema/registry.ts b/packages/core/src/schema/registry.ts index 97d8a2763..4b6666bac 100644 --- a/packages/core/src/schema/registry.ts +++ b/packages/core/src/schema/registry.ts @@ -595,6 +595,11 @@ export class SchemaRegistry { CREATE INDEX ${sql.ref(`idx_${tableName}_deleted_created_id`)} ON ${sql.ref(tableName)} (deleted_at, created_at DESC, id DESC) `.execute(conn); + + await sql` + CREATE INDEX ${sql.ref(`idx_${tableName}_deleted_published_id`)} + ON ${sql.ref(tableName)} (deleted_at, published_at DESC, id DESC) + `.execute(conn); } /** From beefc8ae5e96451277ddbde6c050c756bd9047e2 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:27:37 -0700 Subject: [PATCH 2/2] docs: add changeset for published_at index --- .changeset/shaky-berries-stay.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shaky-berries-stay.md diff --git a/.changeset/shaky-berries-stay.md b/.changeset/shaky-berries-stay.md new file mode 100644 index 000000000..e327c0d07 --- /dev/null +++ b/.changeset/shaky-berries-stay.md @@ -0,0 +1,5 @@ +--- +"emdash": patch +--- + +Adds composite index on (deleted_at, published_at DESC, id DESC) to eliminate full table scans for frontend listing queries that order by published_at.