fixup: federation_audit_log DESC indexes + reserved M4 columns
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

- Add .desc() to all three federation_audit_log created_at indexes for
  reverse-chronological scans (PRD section 7.3)
- Add reserved nullable columns query_hash, outcome, bytes_out per
  TASKS.md M2-01 spec (written by M4, columns reserved now to avoid
  retroactive migration)
- Regenerate migration 0008 in-place (replaces 0008_careless_lake.sql
  with 0008_smart_lyja.sql containing DESC indexes + new columns)
- Update integration test: add reserved columns to CREATE TABLE in
  beforeAll; add 7th test for peer→grant cascade delete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jarvis
2026-04-21 20:52:00 -05:00
parent a1ab4386fe
commit 0e0ad9defe
5 changed files with 107 additions and 17 deletions

View File

@@ -23,7 +23,10 @@ CREATE TABLE "federation_audit_log" (
"result_count" integer,
"denied_reason" text,
"latency_ms" integer,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"query_hash" text,
"outcome" text,
"bytes_out" integer
);
--> statement-breakpoint
CREATE TABLE "federation_grants" (
@@ -63,9 +66,9 @@ ALTER TABLE "federation_grants" ADD CONSTRAINT "federation_grants_subject_user_i
ALTER TABLE "federation_grants" ADD CONSTRAINT "federation_grants_peer_id_federation_peers_id_fk" FOREIGN KEY ("peer_id") REFERENCES "public"."federation_peers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "admin_tokens_user_id_idx" ON "admin_tokens" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "admin_tokens_hash_idx" ON "admin_tokens" USING btree ("token_hash");--> statement-breakpoint
CREATE INDEX "federation_audit_log_peer_created_at_idx" ON "federation_audit_log" USING btree ("peer_id","created_at");--> statement-breakpoint
CREATE INDEX "federation_audit_log_subject_created_at_idx" ON "federation_audit_log" USING btree ("subject_user_id","created_at");--> statement-breakpoint
CREATE INDEX "federation_audit_log_created_at_idx" ON "federation_audit_log" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "federation_audit_log_peer_created_at_idx" ON "federation_audit_log" USING btree ("peer_id","created_at" DESC NULLS LAST);--> statement-breakpoint
CREATE INDEX "federation_audit_log_subject_created_at_idx" ON "federation_audit_log" USING btree ("subject_user_id","created_at" DESC NULLS LAST);--> statement-breakpoint
CREATE INDEX "federation_audit_log_created_at_idx" ON "federation_audit_log" USING btree ("created_at" DESC NULLS LAST);--> statement-breakpoint
CREATE INDEX "federation_grants_subject_status_idx" ON "federation_grants" USING btree ("subject_user_id","status");--> statement-breakpoint
CREATE INDEX "federation_grants_peer_status_idx" ON "federation_grants" USING btree ("peer_id","status");--> statement-breakpoint
CREATE INDEX "federation_peers_cert_serial_idx" ON "federation_peers" USING btree ("cert_serial");--> statement-breakpoint

View File

@@ -1,5 +1,5 @@
{
"id": "e5d39db4-c672-4085-9f17-0f936c3e1143",
"id": "1ecd9663-a2eb-4819-a5a5-818a0e84fd95",
"prevId": "3431aafd-8ea0-499d-989c-d01e995f4764",
"version": "7",
"dialect": "postgresql",
@@ -967,6 +967,24 @@
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"query_hash": {
"name": "query_hash",
"type": "text",
"primaryKey": false,
"notNull": false
},
"outcome": {
"name": "outcome",
"type": "text",
"primaryKey": false,
"notNull": false
},
"bytes_out": {
"name": "bytes_out",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
@@ -982,7 +1000,7 @@
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"asc": false,
"nulls": "last"
}
],
@@ -1003,7 +1021,7 @@
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"asc": false,
"nulls": "last"
}
],
@@ -1018,7 +1036,7 @@
{
"expression": "created_at",
"isExpression": false,
"asc": true,
"asc": false,
"nulls": "last"
}
],

View File

@@ -61,8 +61,8 @@
{
"idx": 8,
"version": "7",
"when": 1776821378331,
"tag": "0008_careless_lake",
"when": 1776822435828,
"tag": "0008_smart_lyja",
"breakpoints": true
}
]

View File

@@ -114,20 +114,23 @@ beforeAll(async () => {
result_count integer,
denied_reason text,
latency_ms integer,
created_at timestamp with time zone NOT NULL DEFAULT now()
created_at timestamp with time zone NOT NULL DEFAULT now(),
query_hash text,
outcome text,
bytes_out integer
)
`;
await sql`
CREATE INDEX IF NOT EXISTS federation_audit_log_peer_created_at_idx
ON federation_audit_log (peer_id, created_at)
ON federation_audit_log (peer_id, created_at DESC NULLS LAST)
`;
await sql`
CREATE INDEX IF NOT EXISTS federation_audit_log_subject_created_at_idx
ON federation_audit_log (subject_user_id, created_at)
ON federation_audit_log (subject_user_id, created_at DESC NULLS LAST)
`;
await sql`
CREATE INDEX IF NOT EXISTS federation_audit_log_created_at_idx
ON federation_audit_log (created_at)
ON federation_audit_log (created_at DESC NULLS LAST)
`;
});
@@ -360,4 +363,62 @@ describe.skipIf(!run)('federation schema — integration', () => {
`,
).rejects.toThrow();
}, 10_000);
// ── 7. FK cascade: peer delete cascades to federation_grants ─────────────
it('cascade-deletes federation_grants when the owning peer is deleted', async () => {
const PEER3_ID = `f2000003-0000-4000-8000-000000000003`;
const cascadeGrantUserId = `${T}-cascade-grant-user`;
// Insert a dedicated user and peer for this test.
await sql!`
INSERT INTO users (id, name, email, email_verified, created_at, updated_at)
VALUES (${cascadeGrantUserId}, ${'Cascade Grant User'}, ${cascadeGrantUserId + '@example.com'}, false, now(), now())
ON CONFLICT (id) DO NOTHING
`;
await sql!`
INSERT INTO federation_peers
(id, common_name, display_name, cert_pem, cert_serial, cert_not_after, state, created_at)
VALUES (
${PEER3_ID},
${T + '-gateway-cascade-peer'},
${'Cascade Peer'},
${'cert-pem-cascade'},
${T + '-serial-003'},
now() + interval '1 year',
${'active'},
now()
)
ON CONFLICT (id) DO NOTHING
`;
const scopeJson = JSON.stringify({ resources: ['tasks'] });
await sql!`
INSERT INTO federation_grants
(subject_user_id, peer_id, scope, status, created_at)
VALUES (
${cascadeGrantUserId},
${PEER3_ID},
${scopeJson}::jsonb,
${'active'},
now()
)
`;
const before = await sql!`
SELECT count(*)::int AS cnt FROM federation_grants WHERE peer_id = ${PEER3_ID}
`;
expect(before[0]!['cnt']).toBe(1);
// Delete peer → grants should cascade-delete.
await sql!`DELETE FROM federation_peers WHERE id = ${PEER3_ID}`;
const after = await sql!`
SELECT count(*)::int AS cnt FROM federation_grants WHERE peer_id = ${PEER3_ID}
`;
expect(after[0]!['cnt']).toBe(0);
// Cleanup
await sql!`DELETE FROM users WHERE id = ${cascadeGrantUserId}`.catch(() => {});
}, 15_000);
});

View File

@@ -759,13 +759,21 @@ export const federationAuditLog = pgTable(
latencyMs: integer('latency_ms'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
// Reserved for M4 — see PRD 7.3
/** SHA-256 of the normalised GraphQL/REST query string; written by M4 search. */
queryHash: text('query_hash'),
/** Request outcome: "allowed" | "denied" | "partial"; written by M4. */
outcome: text('outcome'),
/** Response payload size in bytes; written by M4. */
bytesOut: integer('bytes_out'),
},
(t) => [
// Per-peer request history in reverse chronological order.
index('federation_audit_log_peer_created_at_idx').on(t.peerId, t.createdAt),
index('federation_audit_log_peer_created_at_idx').on(t.peerId, t.createdAt.desc()),
// Per-user access log in reverse chronological order.
index('federation_audit_log_subject_created_at_idx').on(t.subjectUserId, t.createdAt),
index('federation_audit_log_subject_created_at_idx').on(t.subjectUserId, t.createdAt.desc()),
// Global time-range scans (dashboards, rate-limit windows).
index('federation_audit_log_created_at_idx').on(t.createdAt),
index('federation_audit_log_created_at_idx').on(t.createdAt.desc()),
],
);