diff --git a/migrations/0136_ai_ready_route_order_index.sql b/migrations/0136_ai_ready_route_order_index.sql new file mode 100644 index 00000000..e893c956 --- /dev/null +++ b/migrations/0136_ai_ready_route_order_index.sql @@ -0,0 +1,4 @@ +-- Match the AI Ready listing filter and route order so LIMIT stops early. +-- Existing status indexes still require sorting every eligible page. +CREATE INDEX IF NOT EXISTS idx_ai_ready_pages_status_route +ON ai_ready_pages(indexed, is_error, route); diff --git a/scripts/lib/d1-schema-verifier.ts b/scripts/lib/d1-schema-verifier.ts index f893f8cd..554c1c13 100644 --- a/scripts/lib/d1-schema-verifier.ts +++ b/scripts/lib/d1-schema-verifier.ts @@ -251,6 +251,7 @@ export const APPLIED_DUPLICATE_MIGRATION_NUMBERS: Readonly> /** diff --git a/test/unit/ai-ready-route-cost.test.ts b/test/unit/ai-ready-route-cost.test.ts new file mode 100644 index 00000000..7cf7100a --- /dev/null +++ b/test/unit/ai-ready-route-cost.test.ts @@ -0,0 +1,37 @@ +import { readFileSync } from 'node:fs' +import Database from 'better-sqlite3' +import { describe, expect, it } from 'vitest' + +describe('ai-ready route listing cost', () => { + it('returns ordered eligible pages without scanning and sorting every eligible route', () => { + const db = new Database(':memory:') + db.exec(` + CREATE TABLE ai_ready_pages ( + route TEXT PRIMARY KEY, title TEXT, description TEXT, headings TEXT, + keywords TEXT, updated_at INTEGER, is_error INTEGER, locale TEXT, + indexed INTEGER, indexnow_synced_at INTEGER, indexed_at INTEGER + ); + CREATE INDEX idx_ai_ready_pages_indexed_is_error ON ai_ready_pages(indexed, is_error); + CREATE INDEX idx_ai_ready_pages_indexnow_scan ON ai_ready_pages(indexed, is_error, indexnow_synced_at, indexed_at); + `) + const insert = db.prepare('INSERT INTO ai_ready_pages(route,title,indexed,is_error) VALUES(?,?,?,?)') + db.transaction(() => { + for (let i = 0; i < 20_000; i++) { + const route = `/page/${String(i).padStart(5, '0')}` + insert.run(route, `Page ${i}`, i % 2, i % 5 === 0 ? 1 : 0) + } + })() + const migration = readFileSync('migrations/0136_ai_ready_route_order_index.sql', 'utf8') + db.exec(migration) + db.exec(migration) + const sql = 'SELECT route, title, description, headings, keywords, updated_at, is_error, locale FROM ai_ready_pages WHERE indexed = ? AND is_error = 0 ORDER BY route LIMIT ?' + const plan = db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(1, 3) as Array<{ detail: string }> + expect(plan.map(row => row.detail).join('\n')).not.toContain('TEMP B-TREE') + expect(db.prepare(sql).all(1, 3)).toMatchObject([ + { route: '/page/00001', title: 'Page 1' }, + { route: '/page/00003', title: 'Page 3' }, + { route: '/page/00007', title: 'Page 7' }, + ]) + db.close() + }) +})