@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,278 +0,0 @@
|
|
|
1
|
-
import { type Kysely, sql } from "kysely";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* #2895 — repair the string-literal timestamp-default bug on already-migrated
|
|
5
|
-
* databases.
|
|
6
|
-
*
|
|
7
|
-
* Every migration column that passed a plain SQL time-expression string to
|
|
8
|
-
* `col.defaultTo(...)` — `"datetime('now')"` or `"CURRENT_TIMESTAMP"` — bound the
|
|
9
|
-
* argument as a VALUE, not raw SQL, so its DDL emitted `DEFAULT 'datetime(''now'')'`
|
|
10
|
-
* / `DEFAULT 'CURRENT_TIMESTAMP'`. Any row inserted without an explicit value for
|
|
11
|
-
* such a column stored the useless literal text instead of a timestamp.
|
|
12
|
-
*
|
|
13
|
-
* The DDL is fixed FORWARD (all columns now use `defaultTo(sql`(datetime('now'))`)`),
|
|
14
|
-
* so fresh databases are already correct. But a database that already ran the
|
|
15
|
-
* buggy migrations keeps the broken `DEFAULT` in its stored schema — editing a
|
|
16
|
-
* historical migration does NOT re-run it (Kysely tracks migrations by name, no
|
|
17
|
-
* content checksum). So on an upgraded DB this migration must do TWO things:
|
|
18
|
-
*
|
|
19
|
-
* 1. REBUILD each column whose live default is the broken string literal, so
|
|
20
|
-
* FUTURE inserts that omit the column (e.g. `getOrCreateApp`, which omits
|
|
21
|
-
* `created_at`) stop re-poisoning it. SQLite cannot alter a column default
|
|
22
|
-
* in place, and bun:sqlite forbids editing `sqlite_master` directly, so we
|
|
23
|
-
* use the standard table-rebuild: create a twin table with the corrected
|
|
24
|
-
* default, copy the rows, drop the original, rename the twin back, recreate
|
|
25
|
-
* its indexes/triggers, and restore its `sqlite_sequence` AUTOINCREMENT
|
|
26
|
-
* high-water mark (so a rebuilt table cannot reuse a deleted top id). FK
|
|
27
|
-
* enforcement is toggled OFF around the
|
|
28
|
-
* rebuild (see `up`) so dropping a referenced parent cannot cascade-delete
|
|
29
|
-
* its children, then `PRAGMA foreign_key_check` verifies integrity before it
|
|
30
|
-
* is restored.
|
|
31
|
-
* 2. REPAIR the existing poisoned rows in those columns to an evaluated
|
|
32
|
-
* timestamp.
|
|
33
|
-
*
|
|
34
|
-
* TARGETING — the fix keys off `pragma_table_info(...).dflt_value`, which reports
|
|
35
|
-
* a column's stored default. Only the two broken string-literal forms match
|
|
36
|
-
* ({@link BROKEN_DEFAULTS}); a corrected `(datetime('now'))` default, a bare
|
|
37
|
-
* `CURRENT_TIMESTAMP` keyword, and legitimate value defaults (`'{}'`, `'success'`)
|
|
38
|
-
* all report a different `dflt_value` and are skipped. This is deliberately
|
|
39
|
-
* NARROWER than "rewrite any TEXT cell equal to the literal": columns like
|
|
40
|
-
* `storage_events.value` persist arbitrary app data (an app could legitimately
|
|
41
|
-
* store the string `datetime('now')`), so restricting the row repair to columns
|
|
42
|
-
* that actually carry the broken default is what keeps it from corrupting real
|
|
43
|
-
* data.
|
|
44
|
-
*
|
|
45
|
-
* The evaluated `datetime('now')` written for repaired rows is the best available
|
|
46
|
-
* timestamp for a row that never had a real one (its true creation time is
|
|
47
|
-
* unrecoverable). It is stored in SQLite's `YYYY-MM-DD HH:MM:SS` format — the same
|
|
48
|
-
* format the fixed column defaults now emit and the codebase's other
|
|
49
|
-
* server-evaluated-now writes use (`sql`datetime('now')`` in ThresholdManager /
|
|
50
|
-
* MemoryThresholdManager) — rather than the app's ISO-8601. This divergence is
|
|
51
|
-
* deliberate and harmless: no column is both defaulted AND written explicit ISO at
|
|
52
|
-
* an ordered/compared read site (audited during #2895 review), and every TTL
|
|
53
|
-
* comparison wraps the value in `datetime(...)`, which normalizes both formats.
|
|
54
|
-
*
|
|
55
|
-
* Safe on a fresh DB and idempotent: fresh/replayed schemas carry the corrected
|
|
56
|
-
* default, so no column matches {@link BROKEN_DEFAULTS} and the whole pass is a
|
|
57
|
-
* no-op. A second run finds nothing left to rebuild or repair.
|
|
58
|
-
*/
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* A column's stored default (as reported by `pragma_table_info(...).dflt_value`)
|
|
62
|
-
* mapped to the corrected default expression it should carry and the literal
|
|
63
|
-
* value its poisoned rows hold. The `dflt_value` keys are the exact broken
|
|
64
|
-
* string-literal forms — a correct `datetime('now')` / `CURRENT_TIMESTAMP`
|
|
65
|
-
* default reports differently and never matches.
|
|
66
|
-
*/
|
|
67
|
-
const BROKEN_DEFAULTS: Record<string, { corrected: string; poisonedValue: string }> = {
|
|
68
|
-
"'datetime(''now'')'": { corrected: "(datetime('now'))", poisonedValue: "datetime('now')" },
|
|
69
|
-
"'CURRENT_TIMESTAMP'": { corrected: "CURRENT_TIMESTAMP", poisonedValue: "CURRENT_TIMESTAMP" },
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
interface NamedRow {
|
|
73
|
-
name: string;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
interface ColumnInfoRow {
|
|
77
|
-
name: string;
|
|
78
|
-
dflt_value: string | null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
interface SqlRow {
|
|
82
|
-
sql: string | null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
86
|
-
// Fast path: on a fresh/already-fixed schema no column carries a broken default,
|
|
87
|
-
// so there is nothing to rebuild. Return WITHOUT touching PRAGMA foreign_keys so
|
|
88
|
-
// the common case (every existing DB and test) leaves the connection's FK state
|
|
89
|
-
// exactly as the caller set it. Only the rare upgraded-DB path below toggles it.
|
|
90
|
-
const brokenTables = await findTablesWithBrokenDefaults(db);
|
|
91
|
-
if (brokenTables.length === 0) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Rebuilding a table that OTHER tables reference by foreign key requires FK
|
|
96
|
-
// enforcement OFF: `DROP TABLE` performs an implicit DELETE that would otherwise
|
|
97
|
-
// fire `ON DELETE CASCADE` and wipe the child rows (deferral only delays the
|
|
98
|
-
// constraint *check*, not the cascade *action*). `PRAGMA foreign_keys` is a
|
|
99
|
-
// no-op inside a transaction, so it is toggled on the connection around the
|
|
100
|
-
// transaction — safe because the migrator does not wrap SQLite migrations in a
|
|
101
|
-
// transaction and the run is serialized by the migration lock. It is restored to
|
|
102
|
-
// ON afterwards: this branch only runs on an upgraded production DB, whose
|
|
103
|
-
// connection is configured FK-ON (`configureSqliteDatabase`), which is also the
|
|
104
|
-
// state the rest of the migration chain expects.
|
|
105
|
-
await sql`PRAGMA foreign_keys = OFF`.execute(db);
|
|
106
|
-
try {
|
|
107
|
-
// One transaction so a mid-rebuild failure cannot leave a half-swapped schema.
|
|
108
|
-
await db.transaction().execute(async (trx) => {
|
|
109
|
-
await rebuildAndRepair(trx, brokenTables);
|
|
110
|
-
});
|
|
111
|
-
} finally {
|
|
112
|
-
await sql`PRAGMA foreign_keys = ON`.execute(db);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Return the tables that have at least one column whose stored default is a
|
|
118
|
-
* broken string literal. Uses a `SELECT`-shaped `pragma_table_info(...)` read so
|
|
119
|
-
* the bunSqliteDialect returns rows (a bare `PRAGMA x` value read does not).
|
|
120
|
-
*/
|
|
121
|
-
async function findTablesWithBrokenDefaults(db: Kysely<unknown>): Promise<string[]> {
|
|
122
|
-
const tables = await sql<NamedRow>`
|
|
123
|
-
SELECT name FROM sqlite_master
|
|
124
|
-
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
125
|
-
`.execute(db);
|
|
126
|
-
|
|
127
|
-
const broken: string[] = [];
|
|
128
|
-
for (const { name: table } of tables.rows) {
|
|
129
|
-
// Inline the table name as a literal (sql.lit), not a bound parameter: a
|
|
130
|
-
// parameterized pragma_table_info(?) was observed to intermittently return a
|
|
131
|
-
// null dflt_value under parallel-file load (bun:sqlite quirk, see #2922's
|
|
132
|
-
// test). Here a spurious null on a genuinely-poisoned column would fail the
|
|
133
|
-
// `!== null` guard below, so the column would never be added to the broken set
|
|
134
|
-
// and its poisoned default would be silently skipped and never rebuilt (#3612).
|
|
135
|
-
// Matches 2026_07_05_000_repair_updated_at_defaults.ts.
|
|
136
|
-
const columns = await sql<ColumnInfoRow>`
|
|
137
|
-
SELECT dflt_value FROM pragma_table_info(${sql.lit(table)})
|
|
138
|
-
`.execute(db);
|
|
139
|
-
if (
|
|
140
|
-
columns.rows.some(
|
|
141
|
-
(column) => column.dflt_value !== null && column.dflt_value in BROKEN_DEFAULTS,
|
|
142
|
-
)
|
|
143
|
-
) {
|
|
144
|
-
broken.push(table);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
return broken;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function rebuildAndRepair(trx: Kysely<unknown>, tables: string[]): Promise<void> {
|
|
151
|
-
for (const table of tables) {
|
|
152
|
-
// sql.lit (not a bound param) for the same bun:sqlite null-dflt_value quirk
|
|
153
|
-
// guarded in findTablesWithBrokenDefaults above (#2922 / #3612).
|
|
154
|
-
const columns = await sql<ColumnInfoRow>`
|
|
155
|
-
SELECT name, dflt_value FROM pragma_table_info(${sql.lit(table)})
|
|
156
|
-
`.execute(trx);
|
|
157
|
-
|
|
158
|
-
const brokenColumns = columns.rows.filter(
|
|
159
|
-
(column) => column.dflt_value !== null && column.dflt_value in BROKEN_DEFAULTS,
|
|
160
|
-
);
|
|
161
|
-
if (brokenColumns.length === 0) {
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
await rebuildTableWithCorrectedDefaults(trx, table);
|
|
166
|
-
|
|
167
|
-
// Rows copied verbatim during the rebuild still hold the literal; heal them
|
|
168
|
-
// now, restricted to the columns that actually carried the broken default.
|
|
169
|
-
for (const column of brokenColumns) {
|
|
170
|
-
const { poisonedValue } = BROKEN_DEFAULTS[column.dflt_value as string];
|
|
171
|
-
await sql`
|
|
172
|
-
UPDATE ${sql.ref(table)}
|
|
173
|
-
SET ${sql.ref(column.name)} = datetime('now')
|
|
174
|
-
WHERE ${sql.ref(column.name)} = ${poisonedValue}
|
|
175
|
-
`.execute(trx);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Rebuild `table` so every broken string-literal default becomes its corrected
|
|
182
|
-
* raw-SQL form, preserving data, indexes, and triggers. Only the `DEFAULT`
|
|
183
|
-
* clauses change; column set and order are untouched, so `INSERT ... SELECT *`
|
|
184
|
-
* lines the rows up 1:1.
|
|
185
|
-
*/
|
|
186
|
-
async function rebuildTableWithCorrectedDefaults(
|
|
187
|
-
trx: Kysely<unknown>,
|
|
188
|
-
table: string,
|
|
189
|
-
): Promise<void> {
|
|
190
|
-
const createRow = await sql<SqlRow>`
|
|
191
|
-
SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ${table}
|
|
192
|
-
`.execute(trx);
|
|
193
|
-
const createSql = createRow.rows[0]?.sql;
|
|
194
|
-
if (!createSql) {
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const tempTable = `__rebuild_${table}`;
|
|
199
|
-
let correctedSql = replaceFirstTableName(createSql, table, tempTable);
|
|
200
|
-
for (const brokenDefault of Object.keys(BROKEN_DEFAULTS)) {
|
|
201
|
-
// The broken literal only appears in the schema as a column default, so a
|
|
202
|
-
// global replace cannot touch anything else.
|
|
203
|
-
correctedSql = correctedSql.split(brokenDefault).join(BROKEN_DEFAULTS[brokenDefault].corrected);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Capture the table's own indexes and triggers before the drop — auto-indexes
|
|
207
|
-
// backing PRIMARY KEY / UNIQUE constraints have a NULL `sql` and are recreated
|
|
208
|
-
// by the CREATE TABLE itself, so only the explicit ones need replaying.
|
|
209
|
-
const auxiliary = await sql<SqlRow>`
|
|
210
|
-
SELECT sql FROM sqlite_master
|
|
211
|
-
WHERE tbl_name = ${table} AND type IN ('index', 'trigger') AND sql IS NOT NULL
|
|
212
|
-
`.execute(trx);
|
|
213
|
-
|
|
214
|
-
// Capture the AUTOINCREMENT high-water mark. `sqlite_sequence` records the
|
|
215
|
-
// highest rowid EVER used so AUTOINCREMENT never reuses a deleted id; the
|
|
216
|
-
// copy/drop/rename below would otherwise reset it to `max(current id)` and let a
|
|
217
|
-
// deleted top id be handed out again — aliasing ids that other tables reference
|
|
218
|
-
// as plain integers. Restored after the rename.
|
|
219
|
-
const originalSequence = await captureSequence(trx, table);
|
|
220
|
-
|
|
221
|
-
await sql.raw(correctedSql).execute(trx);
|
|
222
|
-
await sql`INSERT INTO ${sql.ref(tempTable)} SELECT * FROM ${sql.ref(table)}`.execute(trx);
|
|
223
|
-
await sql`DROP TABLE ${sql.ref(table)}`.execute(trx);
|
|
224
|
-
await sql`ALTER TABLE ${sql.ref(tempTable)} RENAME TO ${sql.ref(table)}`.execute(trx);
|
|
225
|
-
for (const { sql: auxSql } of auxiliary.rows) {
|
|
226
|
-
if (auxSql) {
|
|
227
|
-
await sql.raw(auxSql).execute(trx);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (originalSequence !== undefined) {
|
|
232
|
-
await sql`DELETE FROM sqlite_sequence WHERE name = ${table}`.execute(trx);
|
|
233
|
-
await sql`INSERT INTO sqlite_sequence (name, seq) VALUES (${table}, ${originalSequence})`.execute(
|
|
234
|
-
trx,
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Read a table's `sqlite_sequence` high-water mark, or `undefined` when the table
|
|
241
|
-
* is not AUTOINCREMENT / has never been inserted into (no sequence row) or the
|
|
242
|
-
* `sqlite_sequence` table does not exist (no AUTOINCREMENT table in the DB).
|
|
243
|
-
*/
|
|
244
|
-
async function captureSequence(trx: Kysely<unknown>, table: string): Promise<number | undefined> {
|
|
245
|
-
const hasSequenceTable = await sql<NamedRow>`
|
|
246
|
-
SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'
|
|
247
|
-
`.execute(trx);
|
|
248
|
-
if (hasSequenceTable.rows.length === 0) {
|
|
249
|
-
return undefined;
|
|
250
|
-
}
|
|
251
|
-
const seqRow = await sql<{ seq: number }>`
|
|
252
|
-
SELECT seq FROM sqlite_sequence WHERE name = ${table}
|
|
253
|
-
`.execute(trx);
|
|
254
|
-
return seqRow.rows[0]?.seq;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Replace the table name in a `CREATE TABLE` statement, tolerating the double-
|
|
259
|
-
* quoted (Kysely) and bare forms and arbitrary whitespace after the keyword.
|
|
260
|
-
* Only the first occurrence — the table being declared — is rewritten; a later
|
|
261
|
-
* self-reference (e.g. a table-level FK) keeps pointing at the real name so the
|
|
262
|
-
* copy still works.
|
|
263
|
-
*/
|
|
264
|
-
function replaceFirstTableName(createSql: string, from: string, to: string): string {
|
|
265
|
-
// Match the declared table name as either a double-quoted identifier (Kysely's
|
|
266
|
-
// form) or a bare word, and rewrite only when it is exactly `from` so a later
|
|
267
|
-
// self-reference in the body is left untouched.
|
|
268
|
-
return createSql.replace(
|
|
269
|
-
/^(\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?)(?:"([^"]+)"|(\w+))/i,
|
|
270
|
-
(match, prefix: string, quotedName: string | undefined, bareName: string | undefined) =>
|
|
271
|
-
(quotedName ?? bareName) === from ? `${prefix}"${to}"` : match,
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export async function down(): Promise<void> {
|
|
276
|
-
// Irreversible repair: the corrected defaults and healed rows carry no
|
|
277
|
-
// information worth reverting, so the down migration is intentionally a no-op.
|
|
278
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { Kysely } from "kysely";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Seek-reduction index for `recordStorageEvent`'s per-insert previous-value
|
|
5
|
-
* lookup (#2798).
|
|
6
|
-
*
|
|
7
|
-
* On every storage telemetry insert that doesn't already carry `previousValue`,
|
|
8
|
-
* `storageEventRepository.recordStorageEvent` runs:
|
|
9
|
-
*
|
|
10
|
-
* SELECT value FROM storage_events
|
|
11
|
-
* WHERE device_id=? AND file_name=? AND key=?
|
|
12
|
-
* ORDER BY timestamp DESC LIMIT 1
|
|
13
|
-
*
|
|
14
|
-
* #2798 was filed against the pre-#2788 schema, where the only usable index was
|
|
15
|
-
* the standalone `idx_storage_events_device` and the plan showed
|
|
16
|
-
* `USE TEMP B-TREE FOR ORDER BY`. That temp B-tree is ALREADY gone: PR #2890
|
|
17
|
-
* (#2788) added `idx_storage_events_device_timestamp (device_id, timestamp)` and
|
|
18
|
-
* PR #2904 (#2893) dropped the standalone device index, so the composite's
|
|
19
|
-
* `device_id` prefix now supplies the DESC order for free.
|
|
20
|
-
*
|
|
21
|
-
* What remains — and what this index fixes — is the SEEK width. With only the
|
|
22
|
-
* `(device_id, timestamp)` composite the planner seeks on `device_id=?` and then
|
|
23
|
-
* scans every row for that device in timestamp order, applying the `file_name=?`
|
|
24
|
-
* and `key=?` filters row-by-row until the first match. A device with many
|
|
25
|
-
* distinct keys makes that scan arbitrarily long. `(device_id, file_name, key,
|
|
26
|
-
* timestamp)` turns the three equality predicates into a single prefix seek that
|
|
27
|
-
* descends straight to the target key's rows, and the trailing `timestamp` column
|
|
28
|
-
* still yields the newest row without a sort (SQLite walks the ascending index
|
|
29
|
-
* backward for `ORDER BY timestamp DESC`).
|
|
30
|
-
*
|
|
31
|
-
* NOT a covering index (per Mira Kessler's review on #2798): the query selects
|
|
32
|
-
* `value`, which is deliberately NOT in the index — `value` is an arbitrarily
|
|
33
|
-
* large TEXT blob, and widening the index to make the read index-only is a bad
|
|
34
|
-
* trade. The plan is a prefix seek + one rowid lookup for `value`; the name is
|
|
35
|
-
* `_key_lookup` (a seek-reduction index), NOT `_covering`.
|
|
36
|
-
*
|
|
37
|
-
* Distinct from `idx_storage_events_device_timestamp` (#2788), not a
|
|
38
|
-
* planner-redundant near-duplicate: that composite serves the device-scoped
|
|
39
|
-
* getter (`WHERE device_id=? ORDER BY timestamp DESC`) and cannot serve this
|
|
40
|
-
* three-equality predicate as a prefix seek; this index cannot serve the
|
|
41
|
-
* device-only getter's ordering as efficiently. The planner picks each for its
|
|
42
|
-
* own shape.
|
|
43
|
-
*
|
|
44
|
-
* Additive and forward-only, `.ifNotExists()` / `.ifExists()` so the migration
|
|
45
|
-
* survives #2785's destructive-recovery replay (drop-all then replay every
|
|
46
|
-
* migration on an empty DB) in unusual orders. Sorts after
|
|
47
|
-
* `2026_07_03_*`, so `storage_events` exists when it runs during a full replay.
|
|
48
|
-
*/
|
|
49
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
50
|
-
await db.schema
|
|
51
|
-
.createIndex("idx_storage_events_key_lookup")
|
|
52
|
-
.ifNotExists()
|
|
53
|
-
.on("storage_events")
|
|
54
|
-
.columns(["device_id", "file_name", "key", "timestamp"])
|
|
55
|
-
.execute();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
59
|
-
await db.schema.dropIndex("idx_storage_events_key_lookup").ifExists().execute();
|
|
60
|
-
}
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
import { type Kysely, sql } from "kysely";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* #2937 — give eight tables' `updated_at` columns their `(datetime('now'))`
|
|
5
|
-
* default on already-upgraded databases.
|
|
6
|
-
*
|
|
7
|
-
* PR #2922 (issue #2913) reconciled the schema by adding
|
|
8
|
-
* `.defaultTo(sql`(datetime('now'))`)` to these eight tables' `updated_at`
|
|
9
|
-
* migration declarations, matching their `created_at` sibling. But editing a
|
|
10
|
-
* historical migration body does NOT re-run it — Kysely tracks executed
|
|
11
|
-
* migrations by name, with no content checksum — so the added default lands only
|
|
12
|
-
* on FRESH/replayed databases. A database that already ran the pre-#2922
|
|
13
|
-
* migrations keeps the old `updated_at TEXT NOT NULL` column with no default,
|
|
14
|
-
* and a defaulted insert (one that omits `updated_at`) fails its NOT NULL
|
|
15
|
-
* constraint there.
|
|
16
|
-
*
|
|
17
|
-
* This mirrors the `created_at` situation before #2915, minus the live bug:
|
|
18
|
-
* `created_at` had a broken string-literal default actively poisoning new rows,
|
|
19
|
-
* which the {@link file://./2026_07_03_000_repair_datetime_now_defaults.ts}
|
|
20
|
-
* rebuild repaired. `updated_at` has no such poison — every current writer
|
|
21
|
-
* supplies it explicitly — so this migration only needs to REBUILD each column's
|
|
22
|
-
* default; the existing rows already hold real, explicitly-written timestamps and
|
|
23
|
-
* are copied through untouched (no row repair).
|
|
24
|
-
*
|
|
25
|
-
* REBUILD MECHANICS — identical to the #2915 repair: SQLite cannot alter a column
|
|
26
|
-
* default in place, and bun:sqlite forbids editing `sqlite_master` directly, so
|
|
27
|
-
* each table is rebuilt via a twin: create a twin whose `updated_at` carries the
|
|
28
|
-
* corrected default, copy the rows with `INSERT ... SELECT *`, drop the original,
|
|
29
|
-
* rename the twin back, replay the table's explicit indexes/triggers, and restore
|
|
30
|
-
* its `sqlite_sequence` AUTOINCREMENT high-water mark (so a rebuilt table cannot
|
|
31
|
-
* reuse a deleted top id). FK enforcement is toggled OFF around the transaction so
|
|
32
|
-
* dropping a referenced parent (e.g. `navigation_apps`) cannot cascade-delete its
|
|
33
|
-
* children.
|
|
34
|
-
*
|
|
35
|
-
* TARGETING — the eight tables are enumerated in {@link TABLES_WITH_DEFAULTED_UPDATED_AT}
|
|
36
|
-
* (exactly the set #2922 edited), and each is rebuilt only when its live
|
|
37
|
-
* `updated_at` default is absent. Detection reads
|
|
38
|
-
* `pragma_table_info(...).dflt_value`: `NULL` means the no-default upgraded column
|
|
39
|
-
* (rebuild it); the corrected default reports as `datetime('now')` and is skipped.
|
|
40
|
-
* `device_sessions` / `failure_groups` are deliberately NOT listed — their
|
|
41
|
-
* `updated_at` default has existed on every database since #2915 repaired the
|
|
42
|
-
* broken literal they originally shipped, so they need no rebuild.
|
|
43
|
-
*
|
|
44
|
-
* Safe on a fresh DB and idempotent: a fresh/replayed or already-rebuilt schema
|
|
45
|
-
* carries the default, so nothing matches and the whole pass is a no-op that never
|
|
46
|
-
* touches FK state. A second run finds every column already defaulted.
|
|
47
|
-
*/
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* The eight tables whose `updated_at` default was added by PR #2922 (a migration
|
|
51
|
-
* edit that does not replay), so an upgraded DB's copy still lacks it. Ordered
|
|
52
|
-
* arbitrarily; each is independently rebuilt.
|
|
53
|
-
*/
|
|
54
|
-
const TABLES_WITH_DEFAULTED_UPDATED_AT = [
|
|
55
|
-
"device_configs",
|
|
56
|
-
"navigation_apps",
|
|
57
|
-
"prediction_transition_stats",
|
|
58
|
-
"accessibility_baselines",
|
|
59
|
-
"feature_flags",
|
|
60
|
-
"video_recording_configs",
|
|
61
|
-
"device_snapshot_configs",
|
|
62
|
-
"appearance_configs",
|
|
63
|
-
] as const;
|
|
64
|
-
|
|
65
|
-
/** The corrected default expression `updated_at` should carry, as raw SQL. */
|
|
66
|
-
const CORRECTED_DEFAULT = "(datetime('now'))";
|
|
67
|
-
|
|
68
|
-
interface ColumnInfoRow {
|
|
69
|
-
name: string;
|
|
70
|
-
dflt_value: string | null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface SqlRow {
|
|
74
|
-
sql: string | null;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
78
|
-
// Fast path: only the tables whose `updated_at` column exists AND has no
|
|
79
|
-
// default need a rebuild. On a fresh/already-fixed schema this is empty, so we
|
|
80
|
-
// return WITHOUT touching PRAGMA foreign_keys — leaving the connection's FK
|
|
81
|
-
// state exactly as the caller set it, as the common case (every existing DB and
|
|
82
|
-
// test) requires. Only the rare upgraded-DB path below toggles it.
|
|
83
|
-
const tablesToRebuild: string[] = [];
|
|
84
|
-
for (const table of TABLES_WITH_DEFAULTED_UPDATED_AT) {
|
|
85
|
-
if (await updatedAtLacksDefault(db, table)) {
|
|
86
|
-
tablesToRebuild.push(table);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
if (tablesToRebuild.length === 0) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Rebuilding a table that OTHER tables reference by foreign key requires FK
|
|
94
|
-
// enforcement OFF: `DROP TABLE` performs an implicit DELETE that would otherwise
|
|
95
|
-
// fire `ON DELETE CASCADE` and wipe the child rows (deferral only delays the
|
|
96
|
-
// constraint *check*, not the cascade *action*). `PRAGMA foreign_keys` is a
|
|
97
|
-
// no-op inside a transaction, so it is toggled on the connection around the
|
|
98
|
-
// transaction — safe because the migrator does not wrap SQLite migrations in a
|
|
99
|
-
// transaction and the run is serialized by the migration lock. Restored to ON
|
|
100
|
-
// afterwards, the state `configureSqliteDatabase` and the rest of the chain
|
|
101
|
-
// expect on the production connection.
|
|
102
|
-
await sql`PRAGMA foreign_keys = OFF`.execute(db);
|
|
103
|
-
try {
|
|
104
|
-
// One transaction so a mid-rebuild failure cannot leave a half-swapped schema.
|
|
105
|
-
await db.transaction().execute(async (trx) => {
|
|
106
|
-
for (const table of tablesToRebuild) {
|
|
107
|
-
await rebuildTableWithUpdatedAtDefault(trx, table);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
} finally {
|
|
111
|
-
await sql`PRAGMA foreign_keys = ON`.execute(db);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* True when `table` exists and its `updated_at` column is present with NO stored
|
|
117
|
-
* default (the pre-#2922 upgraded-DB shape). A corrected `(datetime('now'))`
|
|
118
|
-
* default reports its `dflt_value` as `datetime('now')` (non-null) and is skipped;
|
|
119
|
-
* a missing table or column yields no matching row and is also skipped.
|
|
120
|
-
*
|
|
121
|
-
* The table name is inlined as a literal (`sql.lit`) rather than a bound
|
|
122
|
-
* parameter: a parameterized `pragma_table_info(?)` read was observed to
|
|
123
|
-
* intermittently return a null `dflt_value` under parallel-file load (a bun:sqlite
|
|
124
|
-
* quirk, see #2922's test), which here — where null is the very signal that
|
|
125
|
-
* triggers a rebuild — would spuriously rebuild a fresh table.
|
|
126
|
-
*/
|
|
127
|
-
async function updatedAtLacksDefault(db: Kysely<unknown>, table: string): Promise<boolean> {
|
|
128
|
-
const columns = await sql<ColumnInfoRow>`
|
|
129
|
-
SELECT name, dflt_value FROM pragma_table_info(${sql.lit(table)})
|
|
130
|
-
`.execute(db);
|
|
131
|
-
const updatedAt = columns.rows.find((column) => column.name === "updated_at");
|
|
132
|
-
return updatedAt !== undefined && updatedAt.dflt_value === null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Rebuild `table` so its `updated_at` column gains the `(datetime('now'))`
|
|
137
|
-
* default, preserving data, indexes, and triggers. Only the `updated_at` column's
|
|
138
|
-
* `DEFAULT` clause changes; the column set and order are untouched, so
|
|
139
|
-
* `INSERT ... SELECT *` lines the rows up 1:1.
|
|
140
|
-
*/
|
|
141
|
-
async function rebuildTableWithUpdatedAtDefault(
|
|
142
|
-
trx: Kysely<unknown>,
|
|
143
|
-
table: string,
|
|
144
|
-
): Promise<void> {
|
|
145
|
-
const createRow = await sql<SqlRow>`
|
|
146
|
-
SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ${table}
|
|
147
|
-
`.execute(trx);
|
|
148
|
-
const createSql = createRow.rows[0]?.sql;
|
|
149
|
-
if (!createSql) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const tempTable = `__rebuild_${table}`;
|
|
154
|
-
const correctedSql = addUpdatedAtDefault(replaceFirstTableName(createSql, table, tempTable));
|
|
155
|
-
|
|
156
|
-
// Capture the table's own indexes and triggers before the drop — auto-indexes
|
|
157
|
-
// backing PRIMARY KEY / UNIQUE constraints have a NULL `sql` and are recreated
|
|
158
|
-
// by the CREATE TABLE itself, so only the explicit ones need replaying.
|
|
159
|
-
const auxiliary = await sql<SqlRow>`
|
|
160
|
-
SELECT sql FROM sqlite_master
|
|
161
|
-
WHERE tbl_name = ${table} AND type IN ('index', 'trigger') AND sql IS NOT NULL
|
|
162
|
-
`.execute(trx);
|
|
163
|
-
|
|
164
|
-
// Capture the AUTOINCREMENT high-water mark. `sqlite_sequence` records the
|
|
165
|
-
// highest rowid EVER used so AUTOINCREMENT never reuses a deleted id; the
|
|
166
|
-
// copy/drop/rename below would otherwise reset it to `max(current id)` and let a
|
|
167
|
-
// deleted top id be handed out again. Restored after the rename.
|
|
168
|
-
const originalSequence = await captureSequence(trx, table);
|
|
169
|
-
|
|
170
|
-
await sql.raw(correctedSql).execute(trx);
|
|
171
|
-
await sql`INSERT INTO ${sql.ref(tempTable)} SELECT * FROM ${sql.ref(table)}`.execute(trx);
|
|
172
|
-
await sql`DROP TABLE ${sql.ref(table)}`.execute(trx);
|
|
173
|
-
await sql`ALTER TABLE ${sql.ref(tempTable)} RENAME TO ${sql.ref(table)}`.execute(trx);
|
|
174
|
-
for (const { sql: auxSql } of auxiliary.rows) {
|
|
175
|
-
if (auxSql) {
|
|
176
|
-
await sql.raw(auxSql).execute(trx);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (originalSequence !== undefined) {
|
|
181
|
-
await sql`DELETE FROM sqlite_sequence WHERE name = ${table}`.execute(trx);
|
|
182
|
-
await sql`INSERT INTO sqlite_sequence (name, seq) VALUES (${table}, ${originalSequence})`.execute(
|
|
183
|
-
trx,
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Read a table's `sqlite_sequence` high-water mark, or `undefined` when the table
|
|
190
|
-
* is not AUTOINCREMENT / has never been inserted into (no sequence row) or the
|
|
191
|
-
* `sqlite_sequence` table does not exist (no AUTOINCREMENT table in the DB).
|
|
192
|
-
*/
|
|
193
|
-
async function captureSequence(trx: Kysely<unknown>, table: string): Promise<number | undefined> {
|
|
194
|
-
const hasSequenceTable = await sql<{ name: string }>`
|
|
195
|
-
SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'sqlite_sequence'
|
|
196
|
-
`.execute(trx);
|
|
197
|
-
if (hasSequenceTable.rows.length === 0) {
|
|
198
|
-
return undefined;
|
|
199
|
-
}
|
|
200
|
-
const seqRow = await sql<{ seq: number }>`
|
|
201
|
-
SELECT seq FROM sqlite_sequence WHERE name = ${table}
|
|
202
|
-
`.execute(trx);
|
|
203
|
-
return seqRow.rows[0]?.seq;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Inject `default (datetime('now'))` into the `updated_at` column definition of a
|
|
208
|
-
* `CREATE TABLE` statement, right after its type keyword. Matches only when the
|
|
209
|
-
* column has no existing default (the type is immediately followed by `not null`),
|
|
210
|
-
* so it is a no-op on a schema that already carries the default and never
|
|
211
|
-
* double-adds. Only the first (and only) `"updated_at"` column definition is
|
|
212
|
-
* rewritten.
|
|
213
|
-
*/
|
|
214
|
-
function addUpdatedAtDefault(createSql: string): string {
|
|
215
|
-
return createSql.replace(
|
|
216
|
-
/("updated_at"\s+\w+)(\s+not\s+null)/i,
|
|
217
|
-
`$1 default ${CORRECTED_DEFAULT}$2`,
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Replace the table name in a `CREATE TABLE` statement, tolerating the double-
|
|
223
|
-
* quoted (Kysely) and bare forms and arbitrary whitespace after the keyword.
|
|
224
|
-
* Only the first occurrence — the table being declared — is rewritten; a later
|
|
225
|
-
* self-reference (e.g. a table-level FK) keeps pointing at the real name so the
|
|
226
|
-
* copy still works.
|
|
227
|
-
*/
|
|
228
|
-
function replaceFirstTableName(createSql: string, from: string, to: string): string {
|
|
229
|
-
return createSql.replace(
|
|
230
|
-
/^(\s*CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?)(?:"([^"]+)"|(\w+))/i,
|
|
231
|
-
(match, prefix: string, quotedName: string | undefined, bareName: string | undefined) =>
|
|
232
|
-
(quotedName ?? bareName) === from ? `${prefix}"${to}"` : match,
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export async function down(): Promise<void> {
|
|
237
|
-
// Irreversible repair: the corrected default carries no information worth
|
|
238
|
-
// reverting, so the down migration is intentionally a no-op.
|
|
239
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { type Kysely, sql } from "kysely";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Remember how to unlock a device, keyed by device (issue #4360).
|
|
5
|
-
*
|
|
6
|
-
* A dedicated table rather than columns on `device_sessions`: a session row only
|
|
7
|
-
* exists when device-pool autolock is enabled, and never during boot (which runs
|
|
8
|
-
* before any session), so session-scoped storage cannot deliver
|
|
9
|
-
* remember-then-reuse in the default config or at boot. `lock_credential` is
|
|
10
|
-
* stored as-is in the local `~/.auto-mobile` DB — a single-user, on-disk
|
|
11
|
-
* automation store — so a locked dev device does not have to be unlocked by hand
|
|
12
|
-
* every session.
|
|
13
|
-
*/
|
|
14
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
15
|
-
await db.schema
|
|
16
|
-
.createTable("device_locks")
|
|
17
|
-
.ifNotExists()
|
|
18
|
-
.addColumn("device_id", "text", (col) => col.primaryKey())
|
|
19
|
-
.addColumn("lock_type", "text", (col) => col.notNull())
|
|
20
|
-
.addColumn("lock_credential", "text")
|
|
21
|
-
.addColumn("updated_at", "text", (col) => col.notNull().defaultTo(sql`(datetime('now'))`))
|
|
22
|
-
.execute();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
26
|
-
await db.schema.dropTable("device_locks").execute();
|
|
27
|
-
}
|
|
@@ -1,17 +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("session_tool_capabilities")
|
|
6
|
-
.ifNotExists()
|
|
7
|
-
.addColumn("session_uuid", "text", (col) => col.notNull())
|
|
8
|
-
.addColumn("capability", "text", (col) => col.notNull())
|
|
9
|
-
.addColumn("enabled", "integer", (col) => col.notNull())
|
|
10
|
-
.addColumn("updated_at", "text", (col) => col.notNull().defaultTo(sql`(datetime('now'))`))
|
|
11
|
-
.addPrimaryKeyConstraint("session_tool_capabilities_pk", ["session_uuid", "capability"])
|
|
12
|
-
.execute();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
16
|
-
await db.schema.dropTable("session_tool_capabilities").ifExists().execute();
|
|
17
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { sql, type Kysely } from "kysely";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Session-scope the video recording archive (issue #4752).
|
|
5
|
-
*
|
|
6
|
-
* Adds a nullable `owner_session_uuid` column recording the daemon session that
|
|
7
|
-
* started a recording. The column is deliberately nullable: rows that predate
|
|
8
|
-
* this migration have no known owner and keep a NULL owner. The read policy
|
|
9
|
-
* (see `VideoRecordingRepository`) treats a NULL-owner row as legacy/unowned and
|
|
10
|
-
* readable by any session, while a row with a non-null owner is only returned to
|
|
11
|
-
* that owner — so this migration hardens new recordings without orphaning the
|
|
12
|
-
* existing archive.
|
|
13
|
-
*/
|
|
14
|
-
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
15
|
-
const existingColumn = await sql<{ name: string }>`
|
|
16
|
-
SELECT name FROM pragma_table_info('video_recordings')
|
|
17
|
-
WHERE name = 'owner_session_uuid'
|
|
18
|
-
`.execute(db);
|
|
19
|
-
|
|
20
|
-
if (existingColumn.rows.length === 0) {
|
|
21
|
-
await db.schema
|
|
22
|
-
.alterTable("video_recordings")
|
|
23
|
-
.addColumn("owner_session_uuid", "text")
|
|
24
|
-
.execute();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
await db.schema
|
|
28
|
-
.createIndex("idx_video_recordings_owner_session")
|
|
29
|
-
.ifNotExists()
|
|
30
|
-
.on("video_recordings")
|
|
31
|
-
.column("owner_session_uuid")
|
|
32
|
-
.execute();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
36
|
-
await db.schema.dropIndex("idx_video_recordings_owner_session").execute();
|
|
37
|
-
|
|
38
|
-
await db.schema.alterTable("video_recordings").dropColumn("owner_session_uuid").execute();
|
|
39
|
-
}
|