Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quick-sqlite-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sideshow": patch
---

Speed up SQLite-backed workspaces by indexing session posts, recent posts, comment lookups, and session assets.
22 changes: 22 additions & 0 deletions server/sqlStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,28 @@ export class SqlStore implements Store {
this.migrateToSurfaces();
this.migrateToPosts();
this.migrateSurfaceIds();
this.createIndexes();
}

// Add indexes after the column/table migrations above: older workspaces may
// still call comments' post columns snippetId/surfaceId when the base schema
// is first opened. IF NOT EXISTS makes this an in-place, idempotent migration
// for deployed Durable Objects as well as local SQLite databases.
private createIndexes() {
this.sql.exec(`
CREATE INDEX IF NOT EXISTS sideshow_posts_session_created_at_idx
ON posts (sessionId, createdAt);
CREATE INDEX IF NOT EXISTS sideshow_posts_updated_at_idx
ON posts (updatedAt DESC);
CREATE INDEX IF NOT EXISTS sideshow_comments_session_seq_idx
ON comments (sessionId, seq);
CREATE INDEX IF NOT EXISTS sideshow_comments_post_seq_idx
ON comments (postId, seq);
CREATE INDEX IF NOT EXISTS sideshow_comments_id_idx
ON comments (id);
CREATE INDEX IF NOT EXISTS sideshow_assets_session_idx
ON assets (sessionId);
`);
}

// Pre-0.5.0 workspaces stored a `snippets` table and `comments.snippetId`. Lift
Expand Down
84 changes: 84 additions & 0 deletions test/sqlStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,90 @@ import { runStoreContract } from "./storeContract.ts";
// real Node SQLite path rather than a bespoke shim.
runStoreContract("SqlStore", () => new SqlStore(createSqliteStorage()));

const hotPathIndexes = {
sideshow_assets_session_idx: ["sessionId"],
sideshow_comments_id_idx: ["id"],
sideshow_comments_post_seq_idx: ["postId", "seq"],
sideshow_comments_session_seq_idx: ["sessionId", "seq"],
sideshow_posts_session_created_at_idx: ["sessionId", "createdAt"],
sideshow_posts_updated_at_idx: ["updatedAt"],
} as const;

test("SqlStore adds hot-path indexes to existing workspaces idempotently", () => {
const storage = createSqliteStorage();
new SqlStore(storage);

// Model a database created by an older release, before these indexes existed.
for (const name of Object.keys(hotPathIndexes)) storage.exec(`DROP INDEX ${name}`);

new SqlStore(storage);
new SqlStore(storage);

const indexes = storage
.exec(
"SELECT name, tbl_name FROM sqlite_master WHERE type = 'index' AND name LIKE 'sideshow_%' ORDER BY name",
)
.toArray();
assert.deepEqual(
indexes.map((row) => row.name),
Object.keys(hotPathIndexes),
);
for (const [name, columns] of Object.entries(hotPathIndexes)) {
const actual = storage
.exec(`SELECT name FROM pragma_index_info('${name}') ORDER BY seqno`)
.toArray()
.map((row) => row.name);
assert.deepEqual(actual, columns, `${name} column order`);
}
});

test("SqlStore hot queries use their covering or ordering indexes", () => {
const storage = createSqliteStorage();
new SqlStore(storage);

const assertUsesIndex = (query: string, index: string, ...bindings: (string | number)[]) => {
const plan = storage
.exec(`EXPLAIN QUERY PLAN ${query}`, ...bindings)
.toArray()
.map((row) => row.detail)
.join("\n");
assert.match(plan, new RegExp(`\\b${index}\\b`), `${query}\n${plan}`);
};

assertUsesIndex(
"SELECT * FROM posts WHERE sessionId = ? ORDER BY createdAt ASC",
"sideshow_posts_session_created_at_idx",
"session",
);
assertUsesIndex(
"SELECT sessionId, COUNT(*) AS count FROM posts GROUP BY sessionId",
"sideshow_posts_session_created_at_idx",
);
assertUsesIndex(
"SELECT * FROM posts ORDER BY updatedAt DESC LIMIT ?",
"sideshow_posts_updated_at_idx",
20,
);
assertUsesIndex(
"SELECT * FROM comments WHERE sessionId = ? AND seq > ? ORDER BY seq ASC",
"sideshow_comments_session_seq_idx",
"session",
10,
);
assertUsesIndex(
"SELECT * FROM comments WHERE postId = ? AND seq > ? ORDER BY seq ASC",
"sideshow_comments_post_seq_idx",
"post",
10,
);
assertUsesIndex("SELECT * FROM comments WHERE id = ?", "sideshow_comments_id_idx", "comment");
assertUsesIndex(
"SELECT * FROM assets WHERE sessionId = ?",
"sideshow_assets_session_idx",
"session",
);
});

test("SqlStore counts posts with one aggregate query and never selects body columns", async () => {
const storage = createSqliteStorage();
const queries: string[] = [];
Expand Down
Loading