@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.
Files changed (87) hide show
  1. package/README.md +3 -3
  2. package/README.md.backup +3 -3
  3. package/dist/build-info.json +10 -10
  4. package/dist/src/index.js +1173 -840
  5. package/dist/src/utils/workers/iosBundleExtractWorker.js +3 -3
  6. package/node_modules/strtok3/lib/stream/StreamReader.js +6 -1
  7. package/node_modules/strtok3/package.json +1 -1
  8. package/package.json +21 -17
  9. package/dist/schemas/managed-slot-contract.json +0 -142
  10. package/dist/src/db/eventTables.ts +0 -8
  11. package/dist/src/db/migrations/2025_12_28_000_initial_schema.ts +0 -28
  12. package/dist/src/db/migrations/2025_12_30_000_performance_thresholds.ts +0 -85
  13. package/dist/src/db/migrations/2025_12_30_001_navigation_graph.ts +0 -223
  14. package/dist/src/db/migrations/2025_12_31_000_accessibility_baselines.ts +0 -34
  15. package/dist/src/db/migrations/2025_12_31_001_memory_audit.ts +0 -120
  16. package/dist/src/db/migrations/2026_01_01_000_recomposition_metrics.ts +0 -44
  17. package/dist/src/db/migrations/2026_01_02_000_prediction_history.ts +0 -87
  18. package/dist/src/db/migrations/2026_01_03_000_feature_flags.ts +0 -17
  19. package/dist/src/db/migrations/2026_01_03_000_test_executions.ts +0 -45
  20. package/dist/src/db/migrations/2026_01_05_000_tool_calls.ts +0 -31
  21. package/dist/src/db/migrations/2026_01_08_000_test_coverage.ts +0 -109
  22. package/dist/src/db/migrations/2026_01_09_000_video_recordings.ts +0 -66
  23. package/dist/src/db/migrations/2026_01_10_000_device_snapshots.ts +0 -54
  24. package/dist/src/db/migrations/2026_01_11_000_installed_apps.ts +0 -33
  25. package/dist/src/db/migrations/2026_01_11_000_video_recording_highlights.ts +0 -24
  26. package/dist/src/db/migrations/2026_01_13_000_installed_apps_session_tracking.ts +0 -27
  27. package/dist/src/db/migrations/2026_01_14_000_appearance_config.ts +0 -16
  28. package/dist/src/db/migrations/2026_01_25_000_navigation_screenshots.ts +0 -27
  29. package/dist/src/db/migrations/2026_01_25_001_test_run_details.ts +0 -97
  30. package/dist/src/db/migrations/2026_01_27_000_crash_anr_monitoring.ts +0 -204
  31. package/dist/src/db/migrations/2026_01_27_000_failures.ts +0 -217
  32. package/dist/src/db/migrations/2026_01_29_000_named_nodes.ts +0 -88
  33. package/dist/src/db/migrations/2026_01_30_000_performance_live_metrics.ts +0 -108
  34. package/dist/src/db/migrations/2026_03_15_000_telemetry_events.ts +0 -184
  35. package/dist/src/db/migrations/2026_03_18_000_navigation_events.ts +0 -36
  36. package/dist/src/db/migrations/2026_03_19_000_storage_events.ts +0 -37
  37. package/dist/src/db/migrations/2026_03_19_001_layout_events.ts +0 -39
  38. package/dist/src/db/migrations/2026_03_19_002_storage_events_previous_value.ts +0 -23
  39. package/dist/src/db/migrations/2026_03_19_003_layout_events_screen_name.ts +0 -23
  40. package/dist/src/db/migrations/2026_03_20_000_network_event_details.ts +0 -32
  41. package/dist/src/db/migrations/2026_04_01_000_drop_custom_events.ts +0 -41
  42. package/dist/src/db/migrations/2026_04_02_000_device_sessions.ts +0 -51
  43. package/dist/src/db/migrations/2026_07_01_000_failure_groups_signature_unique.ts +0 -162
  44. package/dist/src/db/migrations/2026_07_02_000_event_composite_indexes.ts +0 -62
  45. package/dist/src/db/migrations/2026_07_03_000_drop_redundant_device_indexes.ts +0 -66
  46. package/dist/src/db/migrations/2026_07_03_000_repair_datetime_now_defaults.ts +0 -278
  47. package/dist/src/db/migrations/2026_07_04_000_storage_events_key_lookup.ts +0 -60
  48. package/dist/src/db/migrations/2026_07_05_000_repair_updated_at_defaults.ts +0 -239
  49. package/dist/src/db/migrations/2026_07_24_000_device_locks.ts +0 -27
  50. package/dist/src/db/migrations/2026_07_27_000_session_tool_capabilities.ts +0 -17
  51. package/dist/src/db/migrations/2026_07_31_000_video_recordings_owner_session.ts +0 -39
  52. package/dist/src/db/migrations/2026_08_02_000_navigation_provenance.ts +0 -159
  53. package/dist/src/db/migrations/2026_08_17_000_emulator_loss_incidents.ts +0 -39
  54. package/dist/src/db/migrations/2026_08_22_000_provision_device_operations.ts +0 -21
  55. package/dist/src/db/migrations/2026_08_22_001_session_tool_overrides.ts +0 -106
  56. package/dist/src/db/migrations/2026_08_22_002_navigation_observation_eviction_index.ts +0 -67
  57. package/dist/src/db/migrations/2026_08_22_003_device_teardown_operations.ts +0 -19
  58. package/dist/src/db/migrations/2026_08_23_000_device_teardown_operation_owner.ts +0 -19
  59. package/dist/src/db/migrations/2026_08_23_001_default_existing_delete_device_disabled.ts +0 -26
  60. package/dist/src/db/migrations/2026_08_24_000_installed_apps_profile_type.ts +0 -15
  61. package/dist/src/db/migrations/2026_09_06_000_tool_selection_profile_provenance.ts +0 -36
  62. package/dist/src/db/migrations/2026_09_08_000_provision_device_operations_expiry.ts +0 -25
  63. package/dist/src/db/migrations/2026_09_10_000_provision_device_operation_attempt.ts +0 -25
  64. package/dist/src/db/migrations/2026_09_10_001_device_session_retention_index.ts +0 -14
  65. package/dist/src/db/migrations/2026_09_13_000_device_snapshot_vm_reclaim.ts +0 -63
  66. package/dist/src/db/migrations/2026_09_14_000_device_session_stable_identity.ts +0 -9
  67. package/dist/src/db/migrations/2026_09_14_001_device_session_identity_writer_fence.ts +0 -45
  68. package/dist/src/db/migrations/2026_09_16_000_device_session_liveness_owner.ts +0 -9
  69. package/dist/src/db/migrations/2026_09_16_001_device_session_liveness_contract.ts +0 -37
  70. package/dist/src/db/migrations/2026_09_17_000_device_session_liveness_writer_fence.ts +0 -48
  71. package/dist/src/db/migrations/2026_09_17_001_device_session_identity_fence_same_serial.ts +0 -53
  72. package/dist/src/db/migrations/2026_09_20_000_provision_device_lifecycle_outcome.ts +0 -32
  73. package/dist/src/db/migrations/2026_09_29_000_navigation_suggestion_provenance.ts +0 -41
  74. package/dist/src/db/migrations/2026_09_30_000_video_recording_display.ts +0 -17
  75. package/dist/src/db/migrations/2026_09_30_001_network_capture_fields.ts +0 -30
  76. package/dist/src/db/migrations/2026_10_05_000_device_locks_device_identity.ts +0 -28
  77. package/dist/src/db/migrations/2026_10_05_001_storage_events_key_lookup_insertion_order.ts +0 -38
  78. package/dist/src/db/migrations/2026_10_05_002_test_edge_coverage_edge_id_index.ts +0 -19
  79. package/dist/src/db/migrations/2026_10_09_000_device_resource_applications.ts +0 -22
  80. package/dist/src/db/migrations/2026_10_09_001_drop_provision_device_operations.ts +0 -32
  81. package/dist/src/db/migrations/2026_10_09_002_drop_device_teardown_operations.ts +0 -28
  82. package/dist/src/db/migrations/2026_10_12_000_rename_recording_tool_names.ts +0 -44
  83. package/dist/src/db/migrations/2026_10_12_001_video_recording_capture_teardown.ts +0 -17
  84. package/dist/src/db/migrations/2026_10_13_000_video_recording_recorder_daemon.ts +0 -17
  85. package/dist/src/index.js.map +0 -1
  86. package/dist/src/utils/workers/iosBundleExtractWorker.js.map +0 -1
  87. 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
- }