diff --git a/demo/mcp/db.ts b/demo/mcp/db.ts index 8d5b1372f..8d776af49 100644 --- a/demo/mcp/db.ts +++ b/demo/mcp/db.ts @@ -22,6 +22,10 @@ sqlite.exec(` dek TEXT NULL ); + -- Account lookup runs before every entity op; index its access pattern. + CREATE INDEX IF NOT EXISTS corsair_accounts_tenant_integration_idx + ON corsair_accounts (tenant_id, integration_id); + CREATE TABLE IF NOT EXISTS corsair_entities ( id TEXT PRIMARY KEY, created_at TEXT NOT NULL, diff --git a/demo/testing/drizzle/0001_entities_account_index.sql b/demo/testing/drizzle/0001_entities_account_index.sql new file mode 100644 index 000000000..3922dff17 --- /dev/null +++ b/demo/testing/drizzle/0001_entities_account_index.sql @@ -0,0 +1,2 @@ +CREATE INDEX `corsair_accounts_tenant_integration_idx` ON `corsair_accounts` (`tenant_id`,`integration_id`);--> statement-breakpoint +CREATE INDEX `corsair_entities_account_type_entity_idx` ON `corsair_entities` (`account_id`,`entity_type`,`entity_id`); diff --git a/demo/testing/drizzle/meta/0001_snapshot.json b/demo/testing/drizzle/meta/0001_snapshot.json new file mode 100644 index 000000000..fa71cd524 --- /dev/null +++ b/demo/testing/drizzle/meta/0001_snapshot.json @@ -0,0 +1,377 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "303100e3-b988-44e0-9bab-73bdaa722aaa", + "prevId": "a5e0ee72-cd9f-4f83-b44b-d0ebc1b17246", + "tables": { + "corsair_accounts": { + "name": "corsair_accounts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dek": { + "name": "dek", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "corsair_accounts_tenant_integration_idx": { + "name": "corsair_accounts_tenant_integration_idx", + "columns": ["tenant_id", "integration_id"], + "isUnique": false + } + }, + "foreignKeys": { + "corsair_accounts_integration_id_corsair_integrations_id_fk": { + "name": "corsair_accounts_integration_id_corsair_integrations_id_fk", + "tableFrom": "corsair_accounts", + "tableTo": "corsair_integrations", + "columnsFrom": ["integration_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "corsair_entities": { + "name": "corsair_entities", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "data": { + "name": "data", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "corsair_entities_account_type_entity_idx": { + "name": "corsair_entities_account_type_entity_idx", + "columns": ["account_id", "entity_type", "entity_id"], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "corsair_events": { + "name": "corsair_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "corsair_integrations": { + "name": "corsair_integrations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config": { + "name": "config", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dek": { + "name": "dek", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "corsair_permissions": { + "name": "corsair_permissions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "plugin": { + "name": "plugin", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "args": { + "name": "args", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "callback_url": { + "name": "callback_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "review_url": { + "name": "review_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tenant_id": { + "name": "tenant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} diff --git a/demo/testing/drizzle/meta/_journal.json b/demo/testing/drizzle/meta/_journal.json index 2e34c00ea..33cd67ce6 100644 --- a/demo/testing/drizzle/meta/_journal.json +++ b/demo/testing/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1773529091251, "tag": "0000_nasty_moondragon", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1787808505608, + "tag": "0001_entities_account_index", + "breakpoints": true } ] } diff --git a/demo/testing/migration.sql b/demo/testing/migration.sql index e4aebd574..1f0a5c168 100644 --- a/demo/testing/migration.sql +++ b/demo/testing/migration.sql @@ -41,6 +41,11 @@ CREATE TABLE IF NOT EXISTS corsair_events ( FOREIGN KEY (account_id) REFERENCES corsair_accounts(id) ); +CREATE INDEX IF NOT EXISTS corsair_accounts_tenant_integration_idx + ON corsair_accounts (tenant_id, integration_id); +CREATE INDEX IF NOT EXISTS corsair_entities_account_type_entity_idx + ON corsair_entities (account_id, entity_type, entity_id); + CREATE TABLE IF NOT EXISTS corsair_permissions ( id TEXT PRIMARY KEY, created_at INTEGER NOT NULL, diff --git a/demo/testing/src/db/schema.ts b/demo/testing/src/db/schema.ts index 8cf1a6dd3..436dc44ed 100644 --- a/demo/testing/src/db/schema.ts +++ b/demo/testing/src/db/schema.ts @@ -1,4 +1,4 @@ -import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; export const corsair_integrations = sqliteTable('corsair_integrations', { id: text('id') @@ -15,40 +15,59 @@ export const corsair_integrations = sqliteTable('corsair_integrations', { dek: text('dek'), }); -export const corsair_accounts = sqliteTable('corsair_accounts', { - id: text('id') - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - created_at: integer('created_at', { mode: 'timestamp' }) - .notNull() - .$defaultFn(() => new Date()), - updated_at: integer('updated_at', { mode: 'timestamp' }) - .notNull() - .$defaultFn(() => new Date()), - tenant_id: text('tenant_id').notNull(), - integration_id: text('integration_id') - .notNull() - .references(() => corsair_integrations.id), - config: text('config', { mode: 'json' }), - dek: text('dek'), -}); +export const corsair_accounts = sqliteTable( + 'corsair_accounts', + { + id: text('id') + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + created_at: integer('created_at', { mode: 'timestamp' }) + .notNull() + .$defaultFn(() => new Date()), + updated_at: integer('updated_at', { mode: 'timestamp' }) + .notNull() + .$defaultFn(() => new Date()), + tenant_id: text('tenant_id').notNull(), + integration_id: text('integration_id') + .notNull() + .references(() => corsair_integrations.id), + config: text('config', { mode: 'json' }), + dek: text('dek'), + }, + (table) => [ + index('corsair_accounts_tenant_integration_idx').on( + table.tenant_id, + table.integration_id, + ), + ], +); -export const corsair_entities = sqliteTable('corsair_entities', { - id: text('id') - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - created_at: integer('created_at', { mode: 'timestamp' }) - .notNull() - .$defaultFn(() => new Date()), - updated_at: integer('updated_at', { mode: 'timestamp' }) - .notNull() - .$defaultFn(() => new Date()), - account_id: text('account_id').notNull(), - entity_id: text('entity_id').notNull(), - entity_type: text('entity_type').notNull(), - version: text('version').notNull(), - data: text('data', { mode: 'json' }).notNull(), -}); +export const corsair_entities = sqliteTable( + 'corsair_entities', + { + id: text('id') + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + created_at: integer('created_at', { mode: 'timestamp' }) + .notNull() + .$defaultFn(() => new Date()), + updated_at: integer('updated_at', { mode: 'timestamp' }) + .notNull() + .$defaultFn(() => new Date()), + account_id: text('account_id').notNull(), + entity_id: text('entity_id').notNull(), + entity_type: text('entity_type').notNull(), + version: text('version').notNull(), + data: text('data', { mode: 'json' }).notNull(), + }, + (table) => [ + index('corsair_entities_account_type_entity_idx').on( + table.account_id, + table.entity_type, + table.entity_id, + ), + ], +); export const corsair_events = sqliteTable('corsair_events', { id: text('id') diff --git a/docs/concepts/database.mdx b/docs/concepts/database.mdx index d38970974..1267dc5b3 100644 --- a/docs/concepts/database.mdx +++ b/docs/concepts/database.mdx @@ -9,7 +9,7 @@ Every API call and webhook that flows through Corsair is stored in your database ## Get started -Run the migration once, then pass your connection to `createCorsair({ database, ... })`. See [Quick Start](/quick-start) for a full setup example. +Run the migration once, then pass your connection to `createCorsair({ database, ... })`. See [Quick Start](/quick-start) for a full setup example. If the tables already exist, run only the two `CREATE INDEX` statements. @@ -145,6 +145,11 @@ CREATE TABLE IF NOT EXISTS corsair_permissions ( expires_at TEXT NOT NULL, error TEXT ); + +CREATE INDEX IF NOT EXISTS corsair_accounts_tenant_integration_idx + ON corsair_accounts (tenant_id, integration_id); +CREATE INDEX IF NOT EXISTS corsair_entities_account_type_entity_idx + ON corsair_entities (account_id, entity_type, entity_id); ``` @@ -232,6 +237,11 @@ CREATE TABLE IF NOT EXISTS corsair_permissions ( expires_at TEXT NOT NULL, error TEXT ); + +CREATE INDEX IF NOT EXISTS corsair_accounts_tenant_integration_idx + ON corsair_accounts (tenant_id, integration_id); +CREATE INDEX IF NOT EXISTS corsair_entities_account_type_entity_idx + ON corsair_entities (account_id, entity_type, entity_id); ``` @@ -263,7 +273,7 @@ psql $env:DATABASE_URL -f migration.sql ```ts src/server/db/schema.ts -import { pgTable, text, jsonb, timestamp } from 'drizzle-orm/pg-core'; +import { pgTable, text, jsonb, timestamp, index } from 'drizzle-orm/pg-core'; export const corsairIntegrations = pgTable('corsair_integrations', { id: text('id').primaryKey(), @@ -282,7 +292,9 @@ export const corsairAccounts = pgTable('corsair_accounts', { integrationId: text('integration_id').notNull().references(() => corsairIntegrations.id), config: jsonb('config').notNull().default({}), dek: text('dek'), -}); +}, (table) => [ + index('corsair_accounts_tenant_integration_idx').on(table.tenantId, table.integrationId), +]); export const corsairEntities = pgTable('corsair_entities', { id: text('id').primaryKey(), @@ -293,7 +305,9 @@ export const corsairEntities = pgTable('corsair_entities', { entityType: text('entity_type').notNull(), version: text('version').notNull(), data: jsonb('data').notNull().default({}), -}); +}, (table) => [ + index('corsair_entities_account_type_entity_idx').on(table.accountId, table.entityType, table.entityId), +]); export const corsairEvents = pgTable('corsair_events', { id: text('id').primaryKey(), @@ -360,6 +374,7 @@ model CorsairAccount { entities CorsairEntity[] events CorsairEvent[] + @@index([tenantId, integrationId], map: "corsair_accounts_tenant_integration_idx") @@map("corsair_accounts") } @@ -374,6 +389,7 @@ model CorsairEntity { data Json @default("{}") account CorsairAccount @relation(fields: [accountId], references: [id]) + @@index([accountId, entityType, entityId], map: "corsair_entities_account_type_entity_idx") @@map("corsair_entities") } diff --git a/docs/guides/dashboard.mdx b/docs/guides/dashboard.mdx index 490e0d421..9af287a80 100644 --- a/docs/guides/dashboard.mdx +++ b/docs/guides/dashboard.mdx @@ -176,6 +176,11 @@ CREATE TABLE IF NOT EXISTS corsair_events ( status TEXT, FOREIGN KEY (account_id) REFERENCES corsair_accounts(id) ); + +CREATE INDEX IF NOT EXISTS corsair_accounts_tenant_integration_idx + ON corsair_accounts (tenant_id, integration_id); +CREATE INDEX IF NOT EXISTS corsair_entities_account_type_entity_idx + ON corsair_entities (account_id, entity_type, entity_id); ``` @@ -200,6 +205,8 @@ Get-Content migration.sql | sqlite3 corsair.db +If the tables already exist, run only the two `CREATE INDEX` statements. + diff --git a/docs/quick-start.mdx b/docs/quick-start.mdx index 906997ed8..f09ef1186 100644 --- a/docs/quick-start.mdx +++ b/docs/quick-start.mdx @@ -136,6 +136,11 @@ CREATE TABLE IF NOT EXISTS corsair_permissions ( expires_at TEXT NOT NULL, error TEXT ); + +CREATE INDEX IF NOT EXISTS corsair_accounts_tenant_integration_idx + ON corsair_accounts (tenant_id, integration_id); +CREATE INDEX IF NOT EXISTS corsair_entities_account_type_entity_idx + ON corsair_entities (account_id, entity_type, entity_id); ``` @@ -159,6 +164,8 @@ Get-Content migration.sql | sqlite3 corsair.db +If the tables already exist, run only the two `CREATE INDEX` statements. + Using Postgres, Drizzle, or Prisma instead? See [Database](/concepts/database) for each option.