@kaeawc/auto-mobile 0.0.85 → 0.0.87
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/README.md +3 -3
- package/README.md.backup +3 -3
- package/dist/build-info.json +10 -10
- package/dist/src/index.js +1173 -840
- package/dist/src/utils/workers/iosBundleExtractWorker.js +3 -3
- package/node_modules/strtok3/lib/stream/StreamReader.js +6 -1
- package/node_modules/strtok3/package.json +1 -1
- package/package.json +21 -17
- package/dist/schemas/managed-slot-contract.json +0 -142
- package/dist/src/db/eventTables.ts +0 -8
- package/dist/src/db/migrations/2025_12_28_000_initial_schema.ts +0 -28
- package/dist/src/db/migrations/2025_12_30_000_performance_thresholds.ts +0 -85
- package/dist/src/db/migrations/2025_12_30_001_navigation_graph.ts +0 -223
- package/dist/src/db/migrations/2025_12_31_000_accessibility_baselines.ts +0 -34
- package/dist/src/db/migrations/2025_12_31_001_memory_audit.ts +0 -120
- package/dist/src/db/migrations/2026_01_01_000_recomposition_metrics.ts +0 -44
- package/dist/src/db/migrations/2026_01_02_000_prediction_history.ts +0 -87
- package/dist/src/db/migrations/2026_01_03_000_feature_flags.ts +0 -17
- package/dist/src/db/migrations/2026_01_03_000_test_executions.ts +0 -45
- package/dist/src/db/migrations/2026_01_05_000_tool_calls.ts +0 -31
- package/dist/src/db/migrations/2026_01_08_000_test_coverage.ts +0 -109
- package/dist/src/db/migrations/2026_01_09_000_video_recordings.ts +0 -66
- package/dist/src/db/migrations/2026_01_10_000_device_snapshots.ts +0 -54
- package/dist/src/db/migrations/2026_01_11_000_installed_apps.ts +0 -33
- package/dist/src/db/migrations/2026_01_11_000_video_recording_highlights.ts +0 -24
- package/dist/src/db/migrations/2026_01_13_000_installed_apps_session_tracking.ts +0 -27
- package/dist/src/db/migrations/2026_01_14_000_appearance_config.ts +0 -16
- package/dist/src/db/migrations/2026_01_25_000_navigation_screenshots.ts +0 -27
- package/dist/src/db/migrations/2026_01_25_001_test_run_details.ts +0 -97
- package/dist/src/db/migrations/2026_01_27_000_crash_anr_monitoring.ts +0 -204
- package/dist/src/db/migrations/2026_01_27_000_failures.ts +0 -217
- package/dist/src/db/migrations/2026_01_29_000_named_nodes.ts +0 -88
- package/dist/src/db/migrations/2026_01_30_000_performance_live_metrics.ts +0 -108
- package/dist/src/db/migrations/2026_03_15_000_telemetry_events.ts +0 -184
- package/dist/src/db/migrations/2026_03_18_000_navigation_events.ts +0 -36
- package/dist/src/db/migrations/2026_03_19_000_storage_events.ts +0 -37
- package/dist/src/db/migrations/2026_03_19_001_layout_events.ts +0 -39
- package/dist/src/db/migrations/2026_03_19_002_storage_events_previous_value.ts +0 -23
- package/dist/src/db/migrations/2026_03_19_003_layout_events_screen_name.ts +0 -23
- package/dist/src/db/migrations/2026_03_20_000_network_event_details.ts +0 -32
- package/dist/src/db/migrations/2026_04_01_000_drop_custom_events.ts +0 -41
- package/dist/src/db/migrations/2026_04_02_000_device_sessions.ts +0 -51
- package/dist/src/db/migrations/2026_07_01_000_failure_groups_signature_unique.ts +0 -162
- package/dist/src/db/migrations/2026_07_02_000_event_composite_indexes.ts +0 -62
- package/dist/src/db/migrations/2026_07_03_000_drop_redundant_device_indexes.ts +0 -66
- package/dist/src/db/migrations/2026_07_03_000_repair_datetime_now_defaults.ts +0 -278
- package/dist/src/db/migrations/2026_07_04_000_storage_events_key_lookup.ts +0 -60
- package/dist/src/db/migrations/2026_07_05_000_repair_updated_at_defaults.ts +0 -239
- package/dist/src/db/migrations/2026_07_24_000_device_locks.ts +0 -27
- package/dist/src/db/migrations/2026_07_27_000_session_tool_capabilities.ts +0 -17
- package/dist/src/db/migrations/2026_07_31_000_video_recordings_owner_session.ts +0 -39
- package/dist/src/db/migrations/2026_08_02_000_navigation_provenance.ts +0 -159
- package/dist/src/db/migrations/2026_08_17_000_emulator_loss_incidents.ts +0 -39
- package/dist/src/db/migrations/2026_08_22_000_provision_device_operations.ts +0 -21
- package/dist/src/db/migrations/2026_08_22_001_session_tool_overrides.ts +0 -106
- package/dist/src/db/migrations/2026_08_22_002_navigation_observation_eviction_index.ts +0 -67
- package/dist/src/db/migrations/2026_08_22_003_device_teardown_operations.ts +0 -19
- package/dist/src/db/migrations/2026_08_23_000_device_teardown_operation_owner.ts +0 -19
- package/dist/src/db/migrations/2026_08_23_001_default_existing_delete_device_disabled.ts +0 -26
- package/dist/src/db/migrations/2026_08_24_000_installed_apps_profile_type.ts +0 -15
- package/dist/src/db/migrations/2026_09_06_000_tool_selection_profile_provenance.ts +0 -36
- package/dist/src/db/migrations/2026_09_08_000_provision_device_operations_expiry.ts +0 -25
- package/dist/src/db/migrations/2026_09_10_000_provision_device_operation_attempt.ts +0 -25
- package/dist/src/db/migrations/2026_09_10_001_device_session_retention_index.ts +0 -14
- package/dist/src/db/migrations/2026_09_13_000_device_snapshot_vm_reclaim.ts +0 -63
- package/dist/src/db/migrations/2026_09_14_000_device_session_stable_identity.ts +0 -9
- package/dist/src/db/migrations/2026_09_14_001_device_session_identity_writer_fence.ts +0 -45
- package/dist/src/db/migrations/2026_09_16_000_device_session_liveness_owner.ts +0 -9
- package/dist/src/db/migrations/2026_09_16_001_device_session_liveness_contract.ts +0 -37
- package/dist/src/db/migrations/2026_09_17_000_device_session_liveness_writer_fence.ts +0 -48
- package/dist/src/db/migrations/2026_09_17_001_device_session_identity_fence_same_serial.ts +0 -53
- package/dist/src/db/migrations/2026_09_20_000_provision_device_lifecycle_outcome.ts +0 -32
- package/dist/src/db/migrations/2026_09_29_000_navigation_suggestion_provenance.ts +0 -41
- package/dist/src/db/migrations/2026_09_30_000_video_recording_display.ts +0 -17
- package/dist/src/db/migrations/2026_09_30_001_network_capture_fields.ts +0 -30
- package/dist/src/db/migrations/2026_10_05_000_device_locks_device_identity.ts +0 -28
- package/dist/src/db/migrations/2026_10_05_001_storage_events_key_lookup_insertion_order.ts +0 -38
- package/dist/src/db/migrations/2026_10_05_002_test_edge_coverage_edge_id_index.ts +0 -19
- package/dist/src/db/migrations/2026_10_09_000_device_resource_applications.ts +0 -22
- package/dist/src/db/migrations/2026_10_09_001_drop_provision_device_operations.ts +0 -32
- package/dist/src/db/migrations/2026_10_09_002_drop_device_teardown_operations.ts +0 -28
- package/dist/src/db/migrations/2026_10_12_000_rename_recording_tool_names.ts +0 -44
- package/dist/src/db/migrations/2026_10_12_001_video_recording_capture_teardown.ts +0 -17
- package/dist/src/db/migrations/2026_10_13_000_video_recording_recorder_daemon.ts +0 -17
- package/dist/src/index.js.map +0 -1
- package/dist/src/utils/workers/iosBundleExtractWorker.js.map +0 -1
- package/schemas/managed-slot-contract.json +0 -142
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { type Kysely, sql } from "kysely";
|
|
2
|
-
|
|
3
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
4
|
-
await db.schema
|
|
5
|
-
.createTable("layout_events")
|
|
6
|
-
.ifNotExists()
|
|
7
|
-
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
|
|
8
|
-
.addColumn("device_id", "text")
|
|
9
|
-
.addColumn("timestamp", "integer", (col) => col.notNull())
|
|
10
|
-
.addColumn("application_id", "text")
|
|
11
|
-
.addColumn("session_id", "text")
|
|
12
|
-
.addColumn("sub_type", "text", (col) => col.notNull())
|
|
13
|
-
.addColumn("composable_name", "text")
|
|
14
|
-
.addColumn("composable_id", "text")
|
|
15
|
-
.addColumn("recomposition_count", "integer")
|
|
16
|
-
.addColumn("duration_ms", "integer")
|
|
17
|
-
.addColumn("likely_cause", "text")
|
|
18
|
-
.addColumn("details_json", "text")
|
|
19
|
-
.addColumn("created_at", "text", (col) => col.notNull().defaultTo(sql`(datetime('now'))`))
|
|
20
|
-
.execute();
|
|
21
|
-
|
|
22
|
-
await db.schema
|
|
23
|
-
.createIndex("idx_layout_events_timestamp")
|
|
24
|
-
.ifNotExists()
|
|
25
|
-
.on("layout_events")
|
|
26
|
-
.column("timestamp")
|
|
27
|
-
.execute();
|
|
28
|
-
|
|
29
|
-
await db.schema
|
|
30
|
-
.createIndex("idx_layout_events_device")
|
|
31
|
-
.ifNotExists()
|
|
32
|
-
.on("layout_events")
|
|
33
|
-
.column("device_id")
|
|
34
|
-
.execute();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
38
|
-
await db.schema.dropTable("layout_events").ifExists().execute();
|
|
39
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { Kysely } from "kysely";
|
|
2
|
-
import { sql } from "kysely";
|
|
3
|
-
|
|
4
|
-
async function columnExists(
|
|
5
|
-
db: Kysely<unknown>,
|
|
6
|
-
tableName: string,
|
|
7
|
-
columnName: string,
|
|
8
|
-
): Promise<boolean> {
|
|
9
|
-
const result = await sql<{ name: string }>`
|
|
10
|
-
SELECT name FROM pragma_table_info(${tableName}) WHERE name = ${columnName}
|
|
11
|
-
`.execute(db);
|
|
12
|
-
return result.rows.length > 0;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
16
|
-
if (!(await columnExists(db, "storage_events", "previous_value"))) {
|
|
17
|
-
await db.schema.alterTable("storage_events").addColumn("previous_value", "text").execute();
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
22
|
-
await db.schema.alterTable("storage_events").dropColumn("previous_value").execute();
|
|
23
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { Kysely } from "kysely";
|
|
2
|
-
import { sql } from "kysely";
|
|
3
|
-
|
|
4
|
-
async function columnExists(
|
|
5
|
-
db: Kysely<unknown>,
|
|
6
|
-
tableName: string,
|
|
7
|
-
columnName: string,
|
|
8
|
-
): Promise<boolean> {
|
|
9
|
-
const result = await sql<{ name: string }>`
|
|
10
|
-
SELECT name FROM pragma_table_info(${tableName}) WHERE name = ${columnName}
|
|
11
|
-
`.execute(db);
|
|
12
|
-
return result.rows.length > 0;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
16
|
-
if (!(await columnExists(db, "layout_events", "screen_name"))) {
|
|
17
|
-
await db.schema.alterTable("layout_events").addColumn("screen_name", "text").execute();
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
22
|
-
await db.schema.alterTable("layout_events").dropColumn("screen_name").execute();
|
|
23
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { Kysely } from "kysely";
|
|
2
|
-
import { sql } from "kysely";
|
|
3
|
-
|
|
4
|
-
async function columnExists(
|
|
5
|
-
db: Kysely<unknown>,
|
|
6
|
-
tableName: string,
|
|
7
|
-
columnName: string,
|
|
8
|
-
): Promise<boolean> {
|
|
9
|
-
const result = await sql<{ name: string }>`
|
|
10
|
-
SELECT name FROM pragma_table_info(${tableName}) WHERE name = ${columnName}
|
|
11
|
-
`.execute(db);
|
|
12
|
-
return result.rows.length > 0;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
16
|
-
const columns = [
|
|
17
|
-
"request_headers_json",
|
|
18
|
-
"response_headers_json",
|
|
19
|
-
"request_body",
|
|
20
|
-
"response_body",
|
|
21
|
-
"content_type",
|
|
22
|
-
];
|
|
23
|
-
for (const col of columns) {
|
|
24
|
-
if (!(await columnExists(db, "network_events", col))) {
|
|
25
|
-
await db.schema.alterTable("network_events").addColumn(col, "text").execute();
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
31
|
-
// SQLite doesn't support DROP COLUMN easily; these columns are nullable and safe to leave
|
|
32
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { type Kysely, sql } from "kysely";
|
|
2
|
-
|
|
3
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
4
|
-
await db.schema.dropTable("custom_events").ifExists().execute();
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
8
|
-
await db.schema
|
|
9
|
-
.createTable("custom_events")
|
|
10
|
-
.ifNotExists()
|
|
11
|
-
.addColumn("id", "integer", (col) => col.primaryKey().autoIncrement())
|
|
12
|
-
.addColumn("device_id", "text")
|
|
13
|
-
.addColumn("timestamp", "integer", (col) => col.notNull())
|
|
14
|
-
.addColumn("application_id", "text")
|
|
15
|
-
.addColumn("session_id", "text")
|
|
16
|
-
.addColumn("name", "text", (col) => col.notNull())
|
|
17
|
-
.addColumn("properties_json", "text")
|
|
18
|
-
.addColumn("created_at", "text", (col) => col.notNull().defaultTo(sql`(datetime('now'))`))
|
|
19
|
-
.execute();
|
|
20
|
-
|
|
21
|
-
await db.schema
|
|
22
|
-
.createIndex("idx_custom_events_timestamp")
|
|
23
|
-
.ifNotExists()
|
|
24
|
-
.on("custom_events")
|
|
25
|
-
.column("timestamp")
|
|
26
|
-
.execute();
|
|
27
|
-
|
|
28
|
-
await db.schema
|
|
29
|
-
.createIndex("idx_custom_events_name")
|
|
30
|
-
.ifNotExists()
|
|
31
|
-
.on("custom_events")
|
|
32
|
-
.column("name")
|
|
33
|
-
.execute();
|
|
34
|
-
|
|
35
|
-
await db.schema
|
|
36
|
-
.createIndex("idx_custom_events_device")
|
|
37
|
-
.ifNotExists()
|
|
38
|
-
.on("custom_events")
|
|
39
|
-
.column("device_id")
|
|
40
|
-
.execute();
|
|
41
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { type Kysely, sql } from "kysely";
|
|
2
|
-
|
|
3
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
4
|
-
await db.schema
|
|
5
|
-
.createTable("device_sessions")
|
|
6
|
-
.ifNotExists()
|
|
7
|
-
.addColumn("session_uuid", "text", (col) => col.primaryKey())
|
|
8
|
-
.addColumn("device_id", "text", (col) => col.notNull())
|
|
9
|
-
.addColumn("platform", "text", (col) => col.notNull())
|
|
10
|
-
.addColumn("status", "text", (col) => col.notNull())
|
|
11
|
-
.addColumn("source", "text")
|
|
12
|
-
.addColumn("autolock_enabled", "integer", (col) => col.notNull().defaultTo(0))
|
|
13
|
-
.addColumn("mcp_session_id", "text")
|
|
14
|
-
.addColumn("daemon_session_id", "text")
|
|
15
|
-
.addColumn("created_at_ms", "integer", (col) => col.notNull())
|
|
16
|
-
.addColumn("last_used_at_ms", "integer", (col) => col.notNull())
|
|
17
|
-
.addColumn("expires_at_ms", "integer", (col) => col.notNull())
|
|
18
|
-
.addColumn("released_at_ms", "integer")
|
|
19
|
-
.addColumn("release_reason", "text")
|
|
20
|
-
.addColumn("session_timeout_ms", "integer", (col) => col.notNull())
|
|
21
|
-
.addColumn("heartbeat_timeout_ms", "integer", (col) => col.notNull())
|
|
22
|
-
.addColumn("has_received_heartbeat", "integer", (col) => col.notNull().defaultTo(0))
|
|
23
|
-
.addColumn("created_at", "text", (col) => col.notNull().defaultTo(sql`(datetime('now'))`))
|
|
24
|
-
.addColumn("updated_at", "text", (col) => col.notNull().defaultTo(sql`(datetime('now'))`))
|
|
25
|
-
.execute();
|
|
26
|
-
|
|
27
|
-
await db.schema
|
|
28
|
-
.createIndex("idx_device_sessions_device_id")
|
|
29
|
-
.ifNotExists()
|
|
30
|
-
.on("device_sessions")
|
|
31
|
-
.column("device_id")
|
|
32
|
-
.execute();
|
|
33
|
-
|
|
34
|
-
await db.schema
|
|
35
|
-
.createIndex("idx_device_sessions_status")
|
|
36
|
-
.ifNotExists()
|
|
37
|
-
.on("device_sessions")
|
|
38
|
-
.column("status")
|
|
39
|
-
.execute();
|
|
40
|
-
|
|
41
|
-
await db.schema
|
|
42
|
-
.createIndex("idx_device_sessions_mcp_session")
|
|
43
|
-
.ifNotExists()
|
|
44
|
-
.on("device_sessions")
|
|
45
|
-
.column("mcp_session_id")
|
|
46
|
-
.execute();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
50
|
-
await db.schema.dropTable("device_sessions").execute();
|
|
51
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { Kysely, sql } from "kysely";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* #2789 — make `failure_groups.signature` uniquely indexed and back-fill any
|
|
5
|
-
* duplicate-signature rows that the original non-unique index allowed to
|
|
6
|
-
* accumulate (the racy get-or-create in `recordFailure` could INSERT two groups
|
|
7
|
-
* for one signature).
|
|
8
|
-
*
|
|
9
|
-
* ORDERING IS LOAD-BEARING. The migration connection runs with
|
|
10
|
-
* `PRAGMA foreign_keys = ON` (`configureSqliteDatabase`, src/db/database.ts) and
|
|
11
|
-
* these SQLite migrations are NOT wrapped in a DDL transaction by the migrator
|
|
12
|
-
* (`SqliteAdapter.supportsTransactionalDdl === false`). `failure_occurrences`
|
|
13
|
-
* and `failure_notifications` carry a CASCADE FK on `failure_occurrences.id` /
|
|
14
|
-
* a plain `group_id`. So we MUST repoint children onto the keeper BEFORE
|
|
15
|
-
* deleting loser groups — delete-first would CASCADE-wipe the occurrences we
|
|
16
|
-
* mean to keep. We wrap the whole de-dup + index swap in ONE transaction so a
|
|
17
|
-
* mid-way `CREATE UNIQUE INDEX` failure cannot leave losers already deleted.
|
|
18
|
-
*
|
|
19
|
-
* Keeper selection is a total order — `ORDER BY first_occurrence ASC, id ASC` —
|
|
20
|
-
* so a same-millisecond burst (two groups tied on `first_occurrence`, the exact
|
|
21
|
-
* scenario this bug produces) still collapses deterministically to one row and
|
|
22
|
-
* `CREATE UNIQUE INDEX` cannot throw on a residual duplicate.
|
|
23
|
-
*
|
|
24
|
-
* Idempotent / safe on an empty DB so it survives #2785's destructive-recovery
|
|
25
|
-
* replay (`resetDatabaseState` drops every table and re-runs all migrations):
|
|
26
|
-
* with no duplicates the de-dup statements are no-ops and the index create uses
|
|
27
|
-
* `IF NOT EXISTS`.
|
|
28
|
-
*/
|
|
29
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
30
|
-
await db.transaction().execute(async (trx) => {
|
|
31
|
-
// 1. Repoint occurrences from loser groups onto their signature's keeper —
|
|
32
|
-
// BEFORE any delete, so CASCADE cannot wipe them. A "loser" is any group
|
|
33
|
-
// that is not the keeper (earliest first_occurrence, min id tiebreak)
|
|
34
|
-
// for its signature.
|
|
35
|
-
await sql`
|
|
36
|
-
UPDATE failure_occurrences
|
|
37
|
-
SET group_id = (
|
|
38
|
-
SELECT k.id FROM failure_groups k
|
|
39
|
-
WHERE k.signature = (
|
|
40
|
-
SELECT g.signature FROM failure_groups g WHERE g.id = failure_occurrences.group_id
|
|
41
|
-
)
|
|
42
|
-
ORDER BY k.first_occurrence ASC, k.id ASC
|
|
43
|
-
LIMIT 1
|
|
44
|
-
)
|
|
45
|
-
WHERE group_id IN (
|
|
46
|
-
SELECT g.id FROM failure_groups g
|
|
47
|
-
WHERE g.id <> (
|
|
48
|
-
SELECT k.id FROM failure_groups k
|
|
49
|
-
WHERE k.signature = g.signature
|
|
50
|
-
ORDER BY k.first_occurrence ASC, k.id ASC
|
|
51
|
-
LIMIT 1
|
|
52
|
-
)
|
|
53
|
-
)
|
|
54
|
-
`.execute(trx);
|
|
55
|
-
|
|
56
|
-
// 2. Repoint notifications the same way. `failure_notifications.group_id` is
|
|
57
|
-
// NOT a foreign key, so nothing cascades and nothing stops us — forget
|
|
58
|
-
// this and notifications silently strand on a deleted group id.
|
|
59
|
-
await sql`
|
|
60
|
-
UPDATE failure_notifications
|
|
61
|
-
SET group_id = (
|
|
62
|
-
SELECT k.id FROM failure_groups k
|
|
63
|
-
WHERE k.signature = (
|
|
64
|
-
SELECT g.signature FROM failure_groups g WHERE g.id = failure_notifications.group_id
|
|
65
|
-
)
|
|
66
|
-
ORDER BY k.first_occurrence ASC, k.id ASC
|
|
67
|
-
LIMIT 1
|
|
68
|
-
)
|
|
69
|
-
WHERE group_id IN (
|
|
70
|
-
SELECT g.id FROM failure_groups g
|
|
71
|
-
WHERE g.id <> (
|
|
72
|
-
SELECT k.id FROM failure_groups k
|
|
73
|
-
WHERE k.signature = g.signature
|
|
74
|
-
ORDER BY k.first_occurrence ASC, k.id ASC
|
|
75
|
-
LIMIT 1
|
|
76
|
-
)
|
|
77
|
-
)
|
|
78
|
-
`.execute(trx);
|
|
79
|
-
|
|
80
|
-
// 3. Back-fill the keeper's aggregates (only for signatures that actually
|
|
81
|
-
// had duplicates). total_count is the SUM of the historical counts so we
|
|
82
|
-
// do not lose increments that predate retention pruning; last_occurrence
|
|
83
|
-
// is the MAX so a collapsed group is not sorted stale on the dashboard
|
|
84
|
-
// (getFailureGroups orders by last_occurrence); unique_sessions is
|
|
85
|
-
// DERIVED from the now-repointed occurrences via COUNT(DISTINCT), the
|
|
86
|
-
// same definition recordFailure uses post-fix (summing would double-count
|
|
87
|
-
// a session present in two duplicate groups).
|
|
88
|
-
//
|
|
89
|
-
// Note: single-group signatures whose stored counts drifted from the
|
|
90
|
-
// lost-increment path (issue scenario #2, which does NOT create a second
|
|
91
|
-
// row) are intentionally left untouched. total_count deliberately
|
|
92
|
-
// preserves increments that predate retention pruning, so a blanket
|
|
93
|
-
// recompute of COUNT(*) from surviving occurrences would DESTROY
|
|
94
|
-
// legitimately-retained historical counts — the two cases are
|
|
95
|
-
// indistinguishable after the fact, so we heal only where a duplicate row
|
|
96
|
-
// already forces a merge.
|
|
97
|
-
await sql`
|
|
98
|
-
UPDATE failure_groups
|
|
99
|
-
SET
|
|
100
|
-
total_count = (
|
|
101
|
-
SELECT COALESCE(SUM(g2.total_count), 0)
|
|
102
|
-
FROM failure_groups g2
|
|
103
|
-
WHERE g2.signature = failure_groups.signature
|
|
104
|
-
),
|
|
105
|
-
last_occurrence = (
|
|
106
|
-
SELECT MAX(g2.last_occurrence)
|
|
107
|
-
FROM failure_groups g2
|
|
108
|
-
WHERE g2.signature = failure_groups.signature
|
|
109
|
-
),
|
|
110
|
-
unique_sessions = (
|
|
111
|
-
SELECT COUNT(DISTINCT o.session_id)
|
|
112
|
-
FROM failure_occurrences o
|
|
113
|
-
WHERE o.group_id = failure_groups.id
|
|
114
|
-
)
|
|
115
|
-
WHERE
|
|
116
|
-
(SELECT COUNT(*) FROM failure_groups d WHERE d.signature = failure_groups.signature) > 1
|
|
117
|
-
AND id = (
|
|
118
|
-
SELECT k.id FROM failure_groups k
|
|
119
|
-
WHERE k.signature = failure_groups.signature
|
|
120
|
-
ORDER BY k.first_occurrence ASC, k.id ASC
|
|
121
|
-
LIMIT 1
|
|
122
|
-
)
|
|
123
|
-
`.execute(trx);
|
|
124
|
-
|
|
125
|
-
// 4. Delete the losers. Children were repointed in steps 1-2, so CASCADE has
|
|
126
|
-
// nothing left to take.
|
|
127
|
-
await sql`
|
|
128
|
-
DELETE FROM failure_groups
|
|
129
|
-
WHERE id <> (
|
|
130
|
-
SELECT k.id FROM failure_groups k
|
|
131
|
-
WHERE k.signature = failure_groups.signature
|
|
132
|
-
ORDER BY k.first_occurrence ASC, k.id ASC
|
|
133
|
-
LIMIT 1
|
|
134
|
-
)
|
|
135
|
-
`.execute(trx);
|
|
136
|
-
|
|
137
|
-
// 5. Swap the plain signature index for a UNIQUE one, and add a composite
|
|
138
|
-
// index so the post-fix `COUNT(DISTINCT session_id)` recompute per event
|
|
139
|
-
// does not scan every occurrence in the group.
|
|
140
|
-
await sql`DROP INDEX IF EXISTS idx_failure_groups_signature`.execute(trx);
|
|
141
|
-
await sql`
|
|
142
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_failure_groups_signature
|
|
143
|
-
ON failure_groups (signature)
|
|
144
|
-
`.execute(trx);
|
|
145
|
-
await sql`
|
|
146
|
-
CREATE INDEX IF NOT EXISTS idx_failure_occurrences_group_session
|
|
147
|
-
ON failure_occurrences (group_id, session_id)
|
|
148
|
-
`.execute(trx);
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
153
|
-
await db.transaction().execute(async (trx) => {
|
|
154
|
-
await sql`DROP INDEX IF EXISTS idx_failure_occurrences_group_session`.execute(trx);
|
|
155
|
-
await sql`DROP INDEX IF EXISTS idx_failure_groups_signature`.execute(trx);
|
|
156
|
-
// Restore the original non-unique index.
|
|
157
|
-
await sql`
|
|
158
|
-
CREATE INDEX IF NOT EXISTS idx_failure_groups_signature
|
|
159
|
-
ON failure_groups (signature)
|
|
160
|
-
`.execute(trx);
|
|
161
|
-
});
|
|
162
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { type Kysely, sql } from "kysely";
|
|
2
|
-
import { EVENT_TABLES } from "../eventTables";
|
|
3
|
-
|
|
4
|
-
async function tableExists(db: Kysely<unknown>, tableName: string): Promise<boolean> {
|
|
5
|
-
const result = await sql<{ name: string }>`
|
|
6
|
-
SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${tableName}
|
|
7
|
-
`.execute(db);
|
|
8
|
-
return result.rows.length > 0;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Composite (device_id, timestamp) indexes for the telemetry event tables (#2788).
|
|
13
|
-
*
|
|
14
|
-
* Every event getter queries `WHERE device_id=? [AND timestamp>=?]
|
|
15
|
-
* ORDER BY timestamp DESC LIMIT n`, but each table only carries single-column
|
|
16
|
-
* indexes on `device_id` and `timestamp` separately. SQLite uses at most one
|
|
17
|
-
* index per query, so the `device_id` equality filter forces a temp B-tree
|
|
18
|
-
* filesort for the `ORDER BY timestamp DESC`. A composite `(device_id,
|
|
19
|
-
* timestamp)` index lets the planner satisfy the equality filter and produce the
|
|
20
|
-
* ordering from a single index — scanning it backward for the DESC order — which
|
|
21
|
-
* drops the sort entirely.
|
|
22
|
-
*
|
|
23
|
-
* No DESC in the index columns: SQLite scans a plain ascending index backward for
|
|
24
|
-
* `ORDER BY ... DESC`, so a plain composite suffices (mirrors the precedent in
|
|
25
|
-
* 2025_12_30_000_performance_thresholds.ts).
|
|
26
|
-
*
|
|
27
|
-
* Additive and forward-only, per #2788's explicit scope ("do not modify the
|
|
28
|
-
* existing single-column indexes"):
|
|
29
|
-
* - The standalone `timestamp` index MUST stay — the retention cutoff query
|
|
30
|
-
* (`ORDER BY timestamp DESC LIMIT 1 OFFSET <cap>`, no device filter) uses it
|
|
31
|
-
* as a covering scan, and the composite cannot substitute because `device_id`
|
|
32
|
-
* leads the composite.
|
|
33
|
-
* - The standalone `device_id` index is now functionally redundant: the
|
|
34
|
-
* composite's `device_id` left-prefix serves every `device_id=?` access path
|
|
35
|
-
* (including `COUNT(*) WHERE device_id=?`). It is deliberately RETAINED here
|
|
36
|
-
* to keep this migration purely additive; dropping the six redundant
|
|
37
|
-
* `idx_<table>_device` indexes to cut per-insert write amplification is
|
|
38
|
-
* deferred to a follow-up so the change is reviewed on its own (see #2788).
|
|
39
|
-
*/
|
|
40
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
41
|
-
for (const table of EVENT_TABLES) {
|
|
42
|
-
if (!(await tableExists(db, table))) {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
// .ifNotExists() so the migration survives #2785's destructive-recovery
|
|
46
|
-
// replay (drop-all then replay every migration on an empty DB).
|
|
47
|
-
await db.schema
|
|
48
|
-
.createIndex(`idx_${table}_device_timestamp`)
|
|
49
|
-
.ifNotExists()
|
|
50
|
-
.on(table as never)
|
|
51
|
-
.columns(["device_id", "timestamp"] as never[])
|
|
52
|
-
.execute();
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
57
|
-
for (const table of EVENT_TABLES) {
|
|
58
|
-
// .ifExists() so a partial-up followed by down (recovery machinery can
|
|
59
|
-
// invoke migrations in unusual orders) does not throw.
|
|
60
|
-
await db.schema.dropIndex(`idx_${table}_device_timestamp`).ifExists().execute();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { type Kysely, sql } from "kysely";
|
|
2
|
-
import { EVENT_TABLES } from "../eventTables";
|
|
3
|
-
|
|
4
|
-
async function tableExists(db: Kysely<unknown>, tableName: string): Promise<boolean> {
|
|
5
|
-
const result = await sql<{ name: string }>`
|
|
6
|
-
SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${tableName}
|
|
7
|
-
`.execute(db);
|
|
8
|
-
return result.rows.length > 0;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Drop the now-redundant single-column `idx_<table>_device` indexes on the six
|
|
13
|
-
* telemetry event tables (#2893, follow-up to #2788 / PR #2890).
|
|
14
|
-
*
|
|
15
|
-
* PR #2890 added composite `(device_id, timestamp)` indexes
|
|
16
|
-
* (`idx_<table>_device_timestamp`) but was deliberately kept additive-only per
|
|
17
|
-
* #2788's scope ("do not modify the existing single-column indexes"). This
|
|
18
|
-
* migration completes the optimization it deferred.
|
|
19
|
-
*
|
|
20
|
-
* A composite `(device_id, timestamp)` index serves every access path a
|
|
21
|
-
* standalone `(device_id)` index can, via SQLite's left-prefix rule:
|
|
22
|
-
* - `WHERE device_id=?` — equality on the composite's leading column.
|
|
23
|
-
* - `WHERE device_id=? ORDER BY timestamp DESC` — filter + order from one index.
|
|
24
|
-
* - `COUNT(*) WHERE device_id=?` — covering scan of the composite.
|
|
25
|
-
* There is no `ANALYZE`/`sqlite_stat1` in `src/db/`, so the planner chooses
|
|
26
|
-
* structurally and deterministically; the standalone device index is dead weight
|
|
27
|
-
* maintained on every insert for no read benefit. At the retention cap with a
|
|
28
|
-
* high insert rate this is pure write amplification.
|
|
29
|
-
*
|
|
30
|
-
* Left intact (different query shapes — out of scope):
|
|
31
|
-
* - The standalone `timestamp` index: the retention cutoff query
|
|
32
|
-
* (`ORDER BY timestamp DESC LIMIT 1 OFFSET <cap>`, no device filter) uses it
|
|
33
|
-
* as a covering scan, and the composite CANNOT substitute because `device_id`
|
|
34
|
-
* leads the composite.
|
|
35
|
-
* - The category/content indexes (`idx_network_events_host`,
|
|
36
|
-
* `idx_log_events_tag`, `idx_os_events_category`).
|
|
37
|
-
*
|
|
38
|
-
* Forward-only, replay-safe (`.ifExists()` / `.ifNotExists()`) so the migration
|
|
39
|
-
* survives #2785's destructive-recovery replay (drop-all then replay every
|
|
40
|
-
* migration on an empty DB) in unusual orders. Sorts after
|
|
41
|
-
* `2026_07_02_000_event_composite_indexes.ts`, so the composite that subsumes
|
|
42
|
-
* each device index is guaranteed present when its device index is dropped.
|
|
43
|
-
*/
|
|
44
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
45
|
-
for (const table of EVENT_TABLES) {
|
|
46
|
-
// .ifExists() so replay on a DB that never had the standalone device index
|
|
47
|
-
// (or a partial recovery order) does not throw.
|
|
48
|
-
await db.schema.dropIndex(`idx_${table}_device`).ifExists().execute();
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
53
|
-
for (const table of EVENT_TABLES) {
|
|
54
|
-
if (!(await tableExists(db, table))) {
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
// Recreate the single-column device index exactly as the base migrations
|
|
58
|
-
// defined it. .ifNotExists() so a down after a partial-up is idempotent.
|
|
59
|
-
await db.schema
|
|
60
|
-
.createIndex(`idx_${table}_device`)
|
|
61
|
-
.ifNotExists()
|
|
62
|
-
.on(table as never)
|
|
63
|
-
.column("device_id" as never)
|
|
64
|
-
.execute();
|
|
65
|
-
}
|
|
66
|
-
}
|