@jungjaehoon/mama-core 2.2.3 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/db/migrations/066-create-awareness-operational-issues.sql +32 -0
- package/db/migrations/067-add-connector-event-source-entity-id.sql +14 -0
- package/dist/connectors/event-index.js +8 -7
- package/dist/connectors/raw-query.d.ts +24 -2
- package/dist/connectors/raw-query.js +108 -1
- package/dist/connectors/types.d.ts +6 -0
- package/dist/db-adapter/node-sqlite-adapter.d.ts +2 -0
- package/dist/db-adapter/node-sqlite-adapter.js +51 -0
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
-- 066: awareness_operational_issues (One MAMA Phase 3 Task 2).
|
|
2
|
+
--
|
|
3
|
+
-- Failures become first-class evidence: gateway tool failures, envelope scope
|
|
4
|
+
-- mismatches, dead owner-event batches and ledger stagnation are recorded here,
|
|
5
|
+
-- aggregated by (surface, signature), and read into every packet so the agent
|
|
6
|
+
-- sees its own failures. The shape below is the exact shape the owner's
|
|
7
|
+
-- installed database already carries from the retired 043-060 chain, plus
|
|
8
|
+
-- `occurrences`. A fresh install gets the whole table here; an installed table
|
|
9
|
+
-- WITHOUT `occurrences` gets the column added at runtime (pragma-guarded ALTER
|
|
10
|
+
-- in packages/standalone/src/observability/operational-issues.ts), because
|
|
11
|
+
-- SQLite has no ADD COLUMN IF NOT EXISTS and this chain is plain SQL.
|
|
12
|
+
--
|
|
13
|
+
-- `status` carries no CHECK on purpose: the installed column has none, and a
|
|
14
|
+
-- CHECK would force a table rebuild for no gain. Values used: open,
|
|
15
|
+
-- repair_requested, closed - validated in TypeScript.
|
|
16
|
+
CREATE TABLE IF NOT EXISTS awareness_operational_issues (
|
|
17
|
+
issue_id TEXT PRIMARY KEY,
|
|
18
|
+
dedupe_key TEXT UNIQUE NOT NULL,
|
|
19
|
+
surface TEXT NOT NULL,
|
|
20
|
+
severity TEXT NOT NULL,
|
|
21
|
+
status TEXT NOT NULL,
|
|
22
|
+
source_delta_id TEXT,
|
|
23
|
+
first_seen_at INTEGER NOT NULL CHECK (first_seen_at >= 0),
|
|
24
|
+
last_seen_at INTEGER NOT NULL CHECK (last_seen_at >= first_seen_at),
|
|
25
|
+
next_retry_at INTEGER,
|
|
26
|
+
last_error TEXT,
|
|
27
|
+
owner_agent TEXT,
|
|
28
|
+
occurrences INTEGER NOT NULL DEFAULT 1
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_awareness_operational_issues_status_seen
|
|
32
|
+
ON awareness_operational_issues(status, last_seen_at DESC);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- Migration 067: give connector_event_index a first-class source_entity_id so an agent can read an
|
|
2
|
+
-- entity's change history (the revisions save() preserves) through the same grant-bounded core index
|
|
3
|
+
-- it already searches. Previously sourceEntityId lived only inside metadata_json, with no by-entity
|
|
4
|
+
-- query. Additive and backward-compatible: existing rows are backfilled from metadata_json.
|
|
5
|
+
ALTER TABLE connector_event_index ADD COLUMN source_entity_id TEXT;
|
|
6
|
+
|
|
7
|
+
UPDATE connector_event_index
|
|
8
|
+
SET source_entity_id = json_extract(metadata_json, '$.sourceEntityId')
|
|
9
|
+
WHERE source_entity_id IS NULL
|
|
10
|
+
AND metadata_json IS NOT NULL
|
|
11
|
+
AND json_extract(metadata_json, '$.sourceEntityId') IS NOT NULL;
|
|
12
|
+
|
|
13
|
+
CREATE INDEX IF NOT EXISTS idx_connector_event_source_entity
|
|
14
|
+
ON connector_event_index(source_connector, source_entity_id);
|
|
@@ -135,15 +135,16 @@ function upsertConnectorEventIndex(adapter, input) {
|
|
|
135
135
|
adapter
|
|
136
136
|
.prepare(`
|
|
137
137
|
INSERT INTO connector_event_index (
|
|
138
|
-
event_index_id, source_connector, source_type, source_id,
|
|
139
|
-
channel, author, title, content, event_datetime, event_date,
|
|
140
|
-
source_cursor, tenant_id, project_id, memory_scope_kind,
|
|
141
|
-
metadata_json, artifact_locator, artifact_title, content_hash,
|
|
142
|
-
expires_at
|
|
138
|
+
event_index_id, source_connector, source_type, source_id, source_entity_id,
|
|
139
|
+
source_locator, channel, author, title, content, event_datetime, event_date,
|
|
140
|
+
source_timestamp_ms, source_cursor, tenant_id, project_id, memory_scope_kind,
|
|
141
|
+
memory_scope_id, metadata_json, artifact_locator, artifact_title, content_hash,
|
|
142
|
+
indexed_at, updated_at, expires_at
|
|
143
143
|
)
|
|
144
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
144
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
145
145
|
ON CONFLICT(source_connector, source_id) DO UPDATE SET
|
|
146
146
|
source_type = excluded.source_type,
|
|
147
|
+
source_entity_id = excluded.source_entity_id,
|
|
147
148
|
source_locator = excluded.source_locator,
|
|
148
149
|
channel = excluded.channel,
|
|
149
150
|
author = excluded.author,
|
|
@@ -182,7 +183,7 @@ function upsertConnectorEventIndex(adapter, input) {
|
|
|
182
183
|
ELSE connector_event_index.operator_observation_seq
|
|
183
184
|
END
|
|
184
185
|
`)
|
|
185
|
-
.run(eventIndexId, input.source_connector, input.source_type, input.source_id, input.source_locator ?? null, input.channel ?? null, input.author ?? null, input.title ?? null, input.content, eventDatetime, eventDate, sourceTimestampMs, input.source_cursor ?? null, input.tenant_id ?? null, input.project_id ?? null, input.memory_scope_kind ?? null, input.memory_scope_id ?? null, metadataJson, input.artifact_locator ?? null, input.artifact_title ?? null, contentHash, input.indexed_at ?? timestamp, timestamp, input.expires_at ?? null);
|
|
186
|
+
.run(eventIndexId, input.source_connector, input.source_type, input.source_id, input.source_entity_id ?? input.source_id, input.source_locator ?? null, input.channel ?? null, input.author ?? null, input.title ?? null, input.content, eventDatetime, eventDate, sourceTimestampMs, input.source_cursor ?? null, input.tenant_id ?? null, input.project_id ?? null, input.memory_scope_kind ?? null, input.memory_scope_id ?? null, metadataJson, input.artifact_locator ?? null, input.artifact_title ?? null, contentHash, input.indexed_at ?? timestamp, timestamp, input.expires_at ?? null);
|
|
186
187
|
const saved = getConnectorEventIndexRecord(adapter, input.source_connector, input.source_id);
|
|
187
188
|
if (!saved) {
|
|
188
189
|
throw new Error(`Failed to read connector_event_index row after upsert: ${input.source_connector}/${input.source_id}`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DatabaseAdapter } from '../db-manager.js';
|
|
2
|
-
import type { RawSearchHit, RawSearchInput, RawSearchResult, RawSearchScopeFilter } from './types.js';
|
|
2
|
+
import type { RawSearchHit, RawDocument, RawSearchInput, RawSearchResult, RawSearchScopeFilter } from './types.js';
|
|
3
3
|
type RawQueryAdapter = Pick<DatabaseAdapter, 'prepare'>;
|
|
4
4
|
interface RawWindowInput {
|
|
5
5
|
connectors?: string[];
|
|
@@ -9,10 +9,32 @@ interface RawWindowInput {
|
|
|
9
9
|
}
|
|
10
10
|
export declare function searchRaw(adapter: RawQueryAdapter, input: RawSearchInput): RawSearchResult;
|
|
11
11
|
export declare function searchAllRaw(adapter: RawQueryAdapter, input: RawSearchInput): RawSearchResult;
|
|
12
|
-
export declare function getRawById(adapter: RawQueryAdapter, rawId: string, visibility: Pick<RawSearchInput, 'connectors' | 'scopes'>):
|
|
12
|
+
export declare function getRawById(adapter: RawQueryAdapter, rawId: string, visibility: Pick<RawSearchInput, 'connectors' | 'scopes'>): RawDocument | null;
|
|
13
13
|
export declare function getRawWindow(adapter: RawQueryAdapter, rawId: string, input: RawWindowInput): {
|
|
14
14
|
target: RawSearchHit;
|
|
15
15
|
items: RawSearchHit[];
|
|
16
16
|
} | null;
|
|
17
|
+
export interface RawHistoryInput {
|
|
18
|
+
/** The entity whose revisions to read. Provide this OR `rawId`. */
|
|
19
|
+
entityId?: string;
|
|
20
|
+
/**
|
|
21
|
+
* A raw event id (event_index_id) to anchor on: its entity is resolved under the SAME
|
|
22
|
+
* visibility, then that entity's revisions are returned. An anchor the caller may not see
|
|
23
|
+
* resolves to nothing, so a rawId cannot widen what the caller can read.
|
|
24
|
+
*/
|
|
25
|
+
rawId?: string;
|
|
26
|
+
connectors?: string[];
|
|
27
|
+
scopes?: RawSearchScopeFilter[];
|
|
28
|
+
fromMs?: number;
|
|
29
|
+
toMs?: number;
|
|
30
|
+
limit?: number;
|
|
31
|
+
cursor?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Chronological change history of one upstream entity (rows sharing source_entity_id), bounded by
|
|
35
|
+
* the SAME connector/scope visibility as search - a citation must not out-read reading. Oldest first,
|
|
36
|
+
* cursor-paged. Rows with a NULL source_entity_id (no revision grouping) never match an entity query.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getRawHistory(adapter: RawQueryAdapter, input: RawHistoryInput): RawSearchResult;
|
|
17
39
|
export type { RawSearchHit, RawSearchInput, RawSearchResult, RawSearchScopeFilter, } from './types.js';
|
|
18
40
|
//# sourceMappingURL=raw-query.d.ts.map
|
|
@@ -4,6 +4,7 @@ exports.searchRaw = searchRaw;
|
|
|
4
4
|
exports.searchAllRaw = searchAllRaw;
|
|
5
5
|
exports.getRawById = getRawById;
|
|
6
6
|
exports.getRawWindow = getRawWindow;
|
|
7
|
+
exports.getRawHistory = getRawHistory;
|
|
7
8
|
const types_js_1 = require("../memory/types.js");
|
|
8
9
|
const event_index_js_1 = require("./event-index.js");
|
|
9
10
|
const DEFAULT_RAW_LIMIT = 25;
|
|
@@ -220,7 +221,7 @@ function getRawById(adapter, rawId, visibility) {
|
|
|
220
221
|
LIMIT 1
|
|
221
222
|
`)
|
|
222
223
|
.get(...params);
|
|
223
|
-
return row ? toRawHit(row) : null;
|
|
224
|
+
return row ? { ...toRawHit(row), content: String(row.content) } : null;
|
|
224
225
|
}
|
|
225
226
|
function getRawWindow(adapter, rawId, input) {
|
|
226
227
|
const targetParams = [rawId];
|
|
@@ -302,4 +303,110 @@ function afterWindowRows(adapter, target, input, limit) {
|
|
|
302
303
|
`)
|
|
303
304
|
.all(...params, limit);
|
|
304
305
|
}
|
|
306
|
+
// The entity a raw event belongs to, plus its connector. source_entity_id is unique only WITHIN a
|
|
307
|
+
// connector, so history must carry the connector too or two connectors sharing an id would merge.
|
|
308
|
+
function resolveEntityAnchor(adapter, rawId, visibility) {
|
|
309
|
+
const clauses = ['e.event_index_id = ?'];
|
|
310
|
+
const params = [rawId];
|
|
311
|
+
appendFilters(clauses, params, { query: '*', ...visibility });
|
|
312
|
+
const row = adapter
|
|
313
|
+
.prepare(`SELECT source_entity_id, source_connector FROM connector_event_index e WHERE ${clauses.join(' AND ')} LIMIT 1`)
|
|
314
|
+
.get(...params);
|
|
315
|
+
return row &&
|
|
316
|
+
typeof row.source_entity_id === 'string' &&
|
|
317
|
+
row.source_entity_id.length > 0 &&
|
|
318
|
+
typeof row.source_connector === 'string' &&
|
|
319
|
+
row.source_connector.length > 0
|
|
320
|
+
? { entityId: row.source_entity_id, connector: row.source_connector }
|
|
321
|
+
: null;
|
|
322
|
+
}
|
|
323
|
+
function singleConnector(connectors) {
|
|
324
|
+
const normalized = normalizeConnectors(connectors);
|
|
325
|
+
return normalized.length === 1 ? normalized[0] : null;
|
|
326
|
+
}
|
|
327
|
+
function encodeHistoryCursor(row) {
|
|
328
|
+
return Buffer.from(JSON.stringify({ timestampMs: row.source_timestamp_ms, rawId: row.event_index_id }), 'utf8').toString('base64url');
|
|
329
|
+
}
|
|
330
|
+
function decodeHistoryCursor(cursor) {
|
|
331
|
+
if (!cursor) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
336
|
+
if (typeof parsed.timestampMs !== 'number' ||
|
|
337
|
+
!Number.isFinite(parsed.timestampMs) ||
|
|
338
|
+
typeof parsed.rawId !== 'string' ||
|
|
339
|
+
parsed.rawId.length === 0) {
|
|
340
|
+
throw new Error('invalid shape');
|
|
341
|
+
}
|
|
342
|
+
return { timestampMs: parsed.timestampMs, rawId: parsed.rawId };
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
346
|
+
throw new Error(`Invalid raw history cursor: ${message}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Chronological change history of one upstream entity (rows sharing source_entity_id), bounded by
|
|
351
|
+
* the SAME connector/scope visibility as search - a citation must not out-read reading. Oldest first,
|
|
352
|
+
* cursor-paged. Rows with a NULL source_entity_id (no revision grouping) never match an entity query.
|
|
353
|
+
*/
|
|
354
|
+
function getRawHistory(adapter, input) {
|
|
355
|
+
// Revision identity is connector-scoped. The rawId path takes the anchor's own connector; the
|
|
356
|
+
// entityId path requires the caller to name exactly one connector, since an entity id alone is
|
|
357
|
+
// ambiguous across connectors.
|
|
358
|
+
const trimmedEntityId = input.entityId?.trim() ?? '';
|
|
359
|
+
const anchor = trimmedEntityId.length > 0
|
|
360
|
+
? (() => {
|
|
361
|
+
const connector = singleConnector(input.connectors);
|
|
362
|
+
return connector ? { entityId: trimmedEntityId, connector } : null;
|
|
363
|
+
})()
|
|
364
|
+
: input.rawId && input.rawId.trim().length > 0
|
|
365
|
+
? resolveEntityAnchor(adapter, input.rawId.trim(), {
|
|
366
|
+
connectors: input.connectors,
|
|
367
|
+
scopes: input.scopes,
|
|
368
|
+
})
|
|
369
|
+
: null;
|
|
370
|
+
if (!anchor) {
|
|
371
|
+
return { hits: [], next_cursor: null };
|
|
372
|
+
}
|
|
373
|
+
const limit = normalizeLimit(input.limit);
|
|
374
|
+
if (limit === 0) {
|
|
375
|
+
return { hits: [], next_cursor: null };
|
|
376
|
+
}
|
|
377
|
+
const clauses = ['e.source_entity_id = ?'];
|
|
378
|
+
const params = [anchor.entityId];
|
|
379
|
+
appendFilters(clauses, params, {
|
|
380
|
+
query: '*',
|
|
381
|
+
connectors: [anchor.connector],
|
|
382
|
+
scopes: input.scopes,
|
|
383
|
+
fromMs: input.fromMs,
|
|
384
|
+
toMs: input.toMs,
|
|
385
|
+
});
|
|
386
|
+
const cursor = decodeHistoryCursor(input.cursor);
|
|
387
|
+
if (cursor) {
|
|
388
|
+
clauses.push('(e.source_timestamp_ms > ? OR (e.source_timestamp_ms = ? AND e.event_index_id > ?))');
|
|
389
|
+
params.push(cursor.timestampMs, cursor.timestampMs, cursor.rawId);
|
|
390
|
+
}
|
|
391
|
+
const rows = adapter
|
|
392
|
+
.prepare(`
|
|
393
|
+
SELECT e.*, 0 AS rank
|
|
394
|
+
FROM connector_event_index e
|
|
395
|
+
WHERE ${clauses.join(' AND ')}
|
|
396
|
+
ORDER BY e.source_timestamp_ms ASC, e.event_index_id ASC
|
|
397
|
+
LIMIT ?
|
|
398
|
+
`)
|
|
399
|
+
.all(...params, limit + 1);
|
|
400
|
+
const pageRows = rows.slice(0, limit);
|
|
401
|
+
const nextRow = rows.length > limit ? pageRows[pageRows.length - 1] : undefined;
|
|
402
|
+
return {
|
|
403
|
+
hits: pageRows.map(toRawHit),
|
|
404
|
+
next_cursor: nextRow
|
|
405
|
+
? encodeHistoryCursor({
|
|
406
|
+
source_timestamp_ms: Number(nextRow.source_timestamp_ms),
|
|
407
|
+
event_index_id: String(nextRow.event_index_id),
|
|
408
|
+
})
|
|
409
|
+
: null,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
305
412
|
//# sourceMappingURL=raw-query.js.map
|
|
@@ -32,6 +32,8 @@ export interface UpsertConnectorEventIndexInput {
|
|
|
32
32
|
source_connector: string;
|
|
33
33
|
source_type: string;
|
|
34
34
|
source_id: string;
|
|
35
|
+
/** Stable upstream entity across revisions; enables grant-bounded change-history reads. */
|
|
36
|
+
source_entity_id?: string | null;
|
|
35
37
|
source_locator?: string | null;
|
|
36
38
|
channel?: string | null;
|
|
37
39
|
author?: string | null;
|
|
@@ -103,6 +105,10 @@ export interface RawSearchHit {
|
|
|
103
105
|
source_ref: string | null;
|
|
104
106
|
metadata: Record<string, unknown>;
|
|
105
107
|
}
|
|
108
|
+
/** Explicit detail access includes the complete source; search results remain bounded previews. */
|
|
109
|
+
export interface RawDocument extends RawSearchHit {
|
|
110
|
+
content: string;
|
|
111
|
+
}
|
|
106
112
|
export interface RawSearchResult {
|
|
107
113
|
hits: RawSearchHit[];
|
|
108
114
|
next_cursor: string | null;
|
|
@@ -70,6 +70,8 @@ export declare class NodeSQLiteAdapter extends DatabaseAdapter {
|
|
|
70
70
|
private assertMigration032Complete;
|
|
71
71
|
private recoverConnectorEventScopeMigration034;
|
|
72
72
|
private assertMigration034Complete;
|
|
73
|
+
private recoverConnectorEventSourceEntityIdMigration067;
|
|
74
|
+
private assertMigration067Complete;
|
|
73
75
|
private recoverConnectorEventOperatorSeqMigration039;
|
|
74
76
|
private assertMigration039Complete;
|
|
75
77
|
private recoverConnectorEventSequencesMigration062;
|
|
@@ -409,6 +409,11 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
409
409
|
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
|
|
410
410
|
continue;
|
|
411
411
|
}
|
|
412
|
+
if (message.includes('duplicate column') && version === 67) {
|
|
413
|
+
this.recoverConnectorEventSourceEntityIdMigration067();
|
|
414
|
+
(0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
412
417
|
if (message.includes('no such column') && version === 62) {
|
|
413
418
|
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred until connector structure recovery (${message})`);
|
|
414
419
|
continue;
|
|
@@ -418,6 +423,11 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
418
423
|
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred until connector structure recovery (${message})`);
|
|
419
424
|
continue;
|
|
420
425
|
}
|
|
426
|
+
if (version === 67 &&
|
|
427
|
+
(message.includes('no such column') || message.includes('no such table'))) {
|
|
428
|
+
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred until connector structure recovery (${message})`);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
421
431
|
if (message.includes('duplicate column')) {
|
|
422
432
|
(0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} skipped (duplicate column - already applied)`);
|
|
423
433
|
this.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(version);
|
|
@@ -515,6 +525,13 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
515
525
|
(0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped legacy connector event refresh migration');
|
|
516
526
|
}
|
|
517
527
|
}
|
|
528
|
+
const hasMissingSourceEntityIdFeature = !this.tableColumns('connector_event_index').has('source_entity_id') ||
|
|
529
|
+
!this.indexExists('idx_connector_event_source_entity') ||
|
|
530
|
+
!this.schemaVersionExists(67);
|
|
531
|
+
if (hasMissingSourceEntityIdFeature) {
|
|
532
|
+
this.recoverConnectorEventSourceEntityIdMigration067();
|
|
533
|
+
(0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped connector event source_entity_id migration');
|
|
534
|
+
}
|
|
518
535
|
}
|
|
519
536
|
if (!this.tableExists('twin_edges')) {
|
|
520
537
|
this.applyRepairMigration(migrationsDir, '035-create-twin-edges.sql', 'twin edge ledger');
|
|
@@ -743,6 +760,40 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
|
|
|
743
760
|
}
|
|
744
761
|
}
|
|
745
762
|
}
|
|
763
|
+
recoverConnectorEventSourceEntityIdMigration067() {
|
|
764
|
+
this.transaction(() => {
|
|
765
|
+
const columns = this.tableColumns('connector_event_index');
|
|
766
|
+
if (!columns.has('source_entity_id')) {
|
|
767
|
+
this.exec('ALTER TABLE connector_event_index ADD COLUMN source_entity_id TEXT');
|
|
768
|
+
}
|
|
769
|
+
// Backfill only when metadata_json exists; a legacy/minimal table rebuilt by the repair path
|
|
770
|
+
// may not have it yet. New rows carry source_entity_id at write time regardless.
|
|
771
|
+
if (columns.has('metadata_json')) {
|
|
772
|
+
this.exec(`
|
|
773
|
+
UPDATE connector_event_index
|
|
774
|
+
SET source_entity_id = json_extract(metadata_json, '$.sourceEntityId')
|
|
775
|
+
WHERE source_entity_id IS NULL
|
|
776
|
+
AND metadata_json IS NOT NULL
|
|
777
|
+
AND json_extract(metadata_json, '$.sourceEntityId') IS NOT NULL
|
|
778
|
+
`);
|
|
779
|
+
}
|
|
780
|
+
this.exec(`
|
|
781
|
+
CREATE INDEX IF NOT EXISTS idx_connector_event_source_entity
|
|
782
|
+
ON connector_event_index(source_connector, source_entity_id)
|
|
783
|
+
`);
|
|
784
|
+
this.assertMigration067Complete();
|
|
785
|
+
this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(67, 'Add connector_event_index source_entity_id');
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
assertMigration067Complete() {
|
|
789
|
+
const columns = this.tableColumns('connector_event_index');
|
|
790
|
+
if (!columns.has('source_entity_id')) {
|
|
791
|
+
throw new Error('Migration 067 recovery failed: missing connector_event_index.source_entity_id');
|
|
792
|
+
}
|
|
793
|
+
if (!this.indexExists('idx_connector_event_source_entity')) {
|
|
794
|
+
throw new Error('Migration 067 recovery failed: missing index idx_connector_event_source_entity');
|
|
795
|
+
}
|
|
796
|
+
}
|
|
746
797
|
recoverConnectorEventOperatorSeqMigration039() {
|
|
747
798
|
this.transaction(() => {
|
|
748
799
|
const columns = this.tableColumns('connector_event_index');
|