Files
Termix/src/backend/database/db/performance-indexes.ts
T
a89ccc6b07 v26.10.3 (#1692)
* feat: shell controller, TabStage, layout and theme plugins with repair view

* feat: move the classic layout into its own bundled plugin

* feat: move color themes into a bundled themes plugin

* fix: plugin tests and type checks work with a linked sdk

* fix: home tab, layout switch and missing layout fallbacks for looks

* chore: add classic layout and themes to the readme

* chore: drop example plugin links from the plugin guide

* feat: plugin filters by type, paged browse, hide plugins needing newer termix

* feat: move desktop, status, import, audit and branding features into plugins

* feat: host lock api so a plugin can load hosts from a config file (#1581)

* chore: add hosts config to the plugin table

* feat: login policy hook for plugins and optional https redirect (#1584)

* chore: add login security to the plugin table

* feat: api key socket tickets so a master can open remote sessions (#1654)

* feat: sync moves to a plugin, desktop gets multiple accounts (#1580, #1654)

* chore: add sync to the plugin table

* feat: move the sync screens and wizard out of core into the plugin (#1580)

* fix: add a neutral linked desktop hook for plugins (#1580)

* fix: show the linked account's name and admin flag in the app (#1580)

* feat: ssh transport providers, core ssh relay and plugin folders api

* feat: support FIDO2 security keys through ssh agent

* feat: add companion panels beside host tabs

* feat: mTLS client certificates for desktop server links

* feat: native web view for plugins in the desktop app

* fix: detect the desktop link window by Electron's default user agent

Fixes #1675

* fix: changelog for desktop link fix

* fix: keep text primary keys in the plugin data export

* fix: ship sharp binaries for Windows arm64 builds

* feat: client cert on the sync link types (sdk 1.0.18)

* chore: update translations

* feat: themed title bar for the desktop app (#1690)

* chore: changelog for the themed title bar

* fix: draw the title bar in the sidebar color

* feat: per host layout for the terminal panel bar

* chore: sdk 1.0.19 for the panel bar layout

* chore: bundle all 43 plugins for 26.10.3

* chore: update translations

---------

Co-authored-by: SirHephaistos <60616553+SirHephaistos@users.noreply.github.com>
Co-authored-by: Giovanny Aranda <garanda21@gmail.com>
2026-10-20 16:03:29 -05:00

166 lines
4.9 KiB
TypeScript

import { databaseLogger } from "../../utils/logger.js";
/**
* Indexes for the columns the app filters, joins and sorts on.
*
* Most tables here are scoped per user or per host and were previously reached
* with a full table scan on every read: the foreign keys existed, but SQLite
* does not index the child side of a foreign key on its own. That is fine for a
* home install with a handful of hosts and invisible in testing; on an install
* with thousands of hosts and a large audit trail it is the dominant cost of a
* request.
*
* Ordering within a composite matters: the equality column comes first and the
* range/sort column second, so the same index serves both the filter and the
* ORDER BY.
*/
export interface PerformanceIndex {
name: string;
table: string;
columns: string;
/** Enforces a constraint as well as serving reads, e.g. one row per key. */
unique?: boolean;
}
export const PERFORMANCE_INDEXES: PerformanceIndex[] = [
// Host list: the single hottest read in the app.
{ name: "idx_ssh_data_user_id", table: "ssh_data", columns: "user_id" },
{
name: "idx_ssh_data_parent_host",
table: "ssh_data",
columns: "parent_host_id",
},
{
name: "idx_ssh_data_credential",
table: "ssh_data",
columns: "credential_id",
},
// Sharing: resolved for every host list request and every permission check.
{ name: "idx_host_access_user_id", table: "host_access", columns: "user_id" },
{ name: "idx_host_access_role_id", table: "host_access", columns: "role_id" },
{ name: "idx_host_access_host_id", table: "host_access", columns: "host_id" },
{
name: "idx_host_access_expires_at",
table: "host_access",
columns: "expires_at",
},
// Audit log: grows without bound and is always read newest-first.
{
name: "idx_audit_logs_timestamp",
table: "audit_logs",
columns: "timestamp",
},
{
name: "idx_audit_logs_user_ts",
table: "audit_logs",
columns: "user_id, timestamp",
},
{
name: "idx_audit_logs_action_ts",
table: "audit_logs",
columns: "action, timestamp",
},
{
name: "idx_audit_logs_resource_ts",
table: "audit_logs",
columns: "resource_type, timestamp",
},
// Auth hot path: touched on every authenticated request.
{ name: "idx_sessions_user_id", table: "sessions", columns: "user_id" },
{ name: "idx_sessions_expires_at", table: "sessions", columns: "expires_at" },
{ name: "idx_user_roles_user_id", table: "user_roles", columns: "user_id" },
{ name: "idx_user_roles_role_id", table: "user_roles", columns: "role_id" },
{
name: "idx_trusted_devices_user_id",
table: "trusted_devices",
columns: "user_id",
},
{ name: "idx_api_keys_user_id", table: "api_keys", columns: "user_id" },
// Credentials and folders.
{
name: "idx_ssh_credentials_user_id",
table: "ssh_credentials",
columns: "user_id",
},
{ name: "idx_ssh_folders_user_id", table: "ssh_folders", columns: "user_id" },
{
name: "idx_ssh_credential_usage_credential",
table: "ssh_credential_usage",
columns: "credential_id",
},
{
name: "idx_ssh_credential_usage_user",
table: "ssh_credential_usage",
columns: "user_id",
},
// Per-user open tabs.
{
name: "idx_user_open_tabs_user_id",
table: "user_open_tabs",
columns: "user_id",
},
];
interface IndexableDatabase {
exec(sql: string): unknown;
}
export interface IndexCreationSummary {
created: number;
skipped: number;
failed: number;
}
/**
* Creates any missing index, leaving existing ones untouched.
*
* A failure here is logged and skipped rather than thrown: an index is a pure
* optimisation, and a table that a given install has not created yet (or a
* column an older schema is missing) must not stop the server from booting.
*/
export function createPerformanceIndexes(
db: IndexableDatabase,
indexes: PerformanceIndex[] = PERFORMANCE_INDEXES,
): IndexCreationSummary {
const summary: IndexCreationSummary = { created: 0, skipped: 0, failed: 0 };
const startedAt = Date.now();
for (const index of indexes) {
try {
db.exec(
`CREATE ${index.unique ? "UNIQUE " : ""}INDEX IF NOT EXISTS ${index.name} ON ${index.table}(${index.columns})`,
);
summary.created++;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
// A missing table or column means this install does not have that
// feature's schema; nothing to index and nothing to warn loudly about.
if (/no such table|no such column/i.test(message)) {
summary.skipped++;
continue;
}
summary.failed++;
databaseLogger.warn(`Could not create index ${index.name}: ${message}`, {
operation: "performance_index_create",
index: index.name,
table: index.table,
});
}
}
databaseLogger.info(
`Performance indexes ready in ${Date.now() - startedAt}ms`,
{
operation: "performance_index_create",
...summary,
},
);
return summary;
}