Source code

Revision control

Other Tools

1
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
2
* vim: sw=2 ts=2 sts=2 expandtab filetype=javascript
3
* This Source Code Form is subject to the terms of the Mozilla Public
4
* License, v. 2.0. If a copy of the MPL was not distributed with this
5
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7
const BYTES_PER_MEBIBYTE = 1048576;
8
9
const { XPCOMUtils } = ChromeUtils.import(
11
);
12
XPCOMUtils.defineLazyModuleGetters(this, {
17
});
18
19
var EXPORTED_SYMBOLS = ["PlacesDBUtils"];
20
21
var PlacesDBUtils = {
22
_isShuttingDown: false,
23
shutdown() {
24
PlacesDBUtils._isShuttingDown = true;
25
},
26
27
_clearTaskQueue: false,
28
clearPendingTasks() {
29
PlacesDBUtils._clearTaskQueue = true;
30
},
31
32
/**
33
* Executes integrity check and common maintenance tasks.
34
*
35
* @return a Map[taskName(String) -> Object]. The Object has the following properties:
36
* - succeeded: boolean
37
* - logs: an array of strings containing the messages logged by the task.
38
*/
39
async maintenanceOnIdle() {
40
let tasks = [
41
this.checkIntegrity,
42
this.invalidateCaches,
43
this.checkCoherence,
44
this._refreshUI,
45
this.originFrecencyStats,
46
this.incrementalVacuum,
47
];
48
let telemetryStartTime = Date.now();
49
let taskStatusMap = await PlacesDBUtils.runTasks(tasks);
50
51
Services.prefs.setIntPref(
52
"places.database.lastMaintenance",
53
parseInt(Date.now() / 1000)
54
);
55
Services.telemetry
56
.getHistogramById("PLACES_IDLE_MAINTENANCE_TIME_MS")
57
.add(Date.now() - telemetryStartTime);
58
return taskStatusMap;
59
},
60
61
/**
62
* Executes integrity check, common and advanced maintenance tasks (like
63
* expiration and vacuum). Will also collect statistics on the database.
64
*
65
* Note: although this function isn't actually async, we keep it async to
66
* allow us to maintain a simple, consistent API for the tasks within this object.
67
*
68
* @return {Promise}
69
* A promise that resolves with a Map[taskName(String) -> Object].
70
* The Object has the following properties:
71
* - succeeded: boolean
72
* - logs: an array of strings containing the messages logged by the task.
73
*/
74
async checkAndFixDatabase() {
75
let tasks = [
76
this.checkIntegrity,
77
this.invalidateCaches,
78
this.checkCoherence,
79
this.expire,
80
this.originFrecencyStats,
81
this.vacuum,
82
this.stats,
83
this._refreshUI,
84
];
85
return PlacesDBUtils.runTasks(tasks);
86
},
87
88
/**
89
* Forces a full refresh of Places views.
90
*
91
* Note: although this function isn't actually async, we keep it async to
92
* allow us to maintain a simple, consistent API for the tasks within this object.
93
*
94
* @returns {Array} An empty array.
95
*/
96
async _refreshUI() {
97
// Send batch update notifications to update the UI.
98
let observers = PlacesUtils.history.getObservers();
99
for (let observer of observers) {
100
observer.onBeginUpdateBatch();
101
observer.onEndUpdateBatch();
102
}
103
return [];
104
},
105
106
/**
107
* Checks integrity and tries to fix the database through a reindex.
108
*
109
* @return {Promise} resolves if database is sane or is made sane.
110
* @resolves to an array of logs for this task.
111
* @rejects if we're unable to fix corruption or unable to check status.
112
*/
113
async checkIntegrity() {
114
let logs = [];
115
116
async function check(dbName) {
117
try {
118
await integrity(dbName);
119
logs.push(`The ${dbName} database is sane`);
120
} catch (ex) {
121
PlacesDBUtils.clearPendingTasks();
122
if (ex.result == Cr.NS_ERROR_FILE_CORRUPTED) {
123
logs.push(`The ${dbName} database is corrupt`);
124
Services.prefs.setCharPref(
125
"places.database.replaceDatabaseOnStartup",
126
dbName
127
);
128
throw new Error(
129
`Unable to fix corruption, ${dbName} will be replaced on next startup`
130
);
131
}
132
throw new Error(`Unable to check ${dbName} integrity: ${ex}`);
133
}
134
}
135
136
await check("places.sqlite");
137
await check("favicons.sqlite");
138
139
return logs;
140
},
141
142
invalidateCaches() {
143
let logs = [];
144
return PlacesUtils.withConnectionWrapper(
145
"PlacesDBUtils: invalidate caches",
146
async db => {
147
let idsWithStaleGuidsRows = await db.execute(
148
`SELECT id FROM moz_bookmarks
149
WHERE guid IS NULL OR
150
NOT IS_VALID_GUID(guid) OR
151
(type = :bookmark_type AND fk IS NULL) OR
152
(type <> :bookmark_type AND fk NOT NULL) OR
153
type IS NULL`,
154
{ bookmark_type: PlacesUtils.bookmarks.TYPE_BOOKMARK }
155
);
156
for (let row of idsWithStaleGuidsRows) {
157
let id = row.getResultByName("id");
158
PlacesUtils.invalidateCachedGuidFor(id);
159
}
160
logs.push("The caches have been invalidated");
161
return logs;
162
}
163
).catch(ex => {
164
PlacesDBUtils.clearPendingTasks();
165
throw new Error("Unable to invalidate caches");
166
});
167
},
168
169
/**
170
* Checks data coherence and tries to fix most common errors.
171
*
172
* @return {Promise} resolves when coherence is checked.
173
* @resolves to an array of logs for this task.
174
* @rejects if database is not coherent.
175
*/
176
async checkCoherence() {
177
let logs = [];
178
let stmts = await PlacesDBUtils._getCoherenceStatements();
179
let coherenceCheck = true;
180
await PlacesUtils.withConnectionWrapper(
181
"PlacesDBUtils: coherence check:",
182
db =>
183
db.executeTransaction(async () => {
184
for (let { query, params } of stmts) {
185
try {
186
await db.execute(query, params || null);
187
} catch (ex) {
188
Cu.reportError(ex);
189
coherenceCheck = false;
190
}
191
}
192
})
193
);
194
195
if (coherenceCheck) {
196
logs.push("The database is coherent");
197
} else {
198
PlacesDBUtils.clearPendingTasks();
199
throw new Error("Unable to complete the coherence check");
200
}
201
return logs;
202
},
203
204
/**
205
* Runs incremental vacuum on databases supporting it.
206
*
207
* @return {Promise} resolves when done.
208
* @resolves to an array of logs for this task.
209
* @rejects if we were unable to vacuum.
210
*/
211
async incrementalVacuum() {
212
let logs = [];
213
return PlacesUtils.withConnectionWrapper(
214
"PlacesDBUtils: incrementalVacuum",
215
async db => {
216
let count = (await db.execute(
217
"PRAGMA favicons.freelist_count"
218
))[0].getResultByIndex(0);
219
if (count < 10) {
220
logs.push(
221
`The favicons database has only ${count} free pages, not vacuuming.`
222
);
223
} else {
224
logs.push(
225
`The favicons database has ${count} free pages, vacuuming.`
226
);
227
await db.execute("PRAGMA favicons.incremental_vacuum");
228
count = (await db.execute(
229
"PRAGMA favicons.freelist_count"
230
))[0].getResultByIndex(0);
231
logs.push(
232
`The database has been vacuumed and has now ${count} free pages.`
233
);
234
}
235
return logs;
236
}
237
).catch(ex => {
238
PlacesDBUtils.clearPendingTasks();
239
throw new Error(
240
"Unable to incrementally vacuum the favicons database " + ex
241
);
242
});
243
},
244
245
async _getCoherenceStatements() {
246
let cleanupStatements = [
247
// MOZ_PLACES
248
// L.1 remove duplicate URLs.
249
// This task uses a temp table of potential dupes, and a trigger to remove
250
// them. It runs first because it relies on subsequent tasks to clean up
251
// orphaned foreign key references. The task works like this: first, we
252
// insert all rows with the same hash into the temp table. This lets
253
// SQLite use the `url_hash` index for scanning `moz_places`. Hashes
254
// aren't unique, so two different URLs might have the same hash. To find
255
// the actual dupes, we use a unique constraint on the URL in the temp
256
// table. If that fails, we bump the dupe count. Then, we delete all dupes
257
// from the table. This fires the cleanup trigger, which updates all
258
// foreign key references to point to one of the duplicate Places, then
259
// deletes the others.
260
{
261
query: `CREATE TEMP TABLE IF NOT EXISTS moz_places_dupes_temp(
262
id INTEGER PRIMARY KEY
263
, hash INTEGER NOT NULL
264
, url TEXT UNIQUE NOT NULL
265
, count INTEGER NOT NULL DEFAULT 0
266
)`,
267
},
268
{
269
query: `CREATE TEMP TRIGGER IF NOT EXISTS moz_places_remove_dupes_temp_trigger
270
AFTER DELETE ON moz_places_dupes_temp
271
FOR EACH ROW
272
BEGIN
273
/* Reassign history visits. */
274
UPDATE moz_historyvisits SET
275
place_id = OLD.id
276
WHERE place_id IN (SELECT id FROM moz_places
277
WHERE id <> OLD.id AND
278
url_hash = OLD.hash AND
279
url = OLD.url);
280
281
/* Merge autocomplete history entries. */
282
INSERT INTO moz_inputhistory(place_id, input, use_count)
283
SELECT OLD.id, a.input, a.use_count
284
FROM moz_inputhistory a
285
JOIN moz_places h ON h.id = a.place_id
286
WHERE h.id <> OLD.id AND
287
h.url_hash = OLD.hash AND
288
h.url = OLD.url
289
ON CONFLICT(place_id, input) DO UPDATE SET
290
place_id = excluded.place_id,
291
use_count = use_count + excluded.use_count;
292
293
/* Merge page annos, ignoring annos with the same name that are
294
already set on the destination. */
295
INSERT OR IGNORE INTO moz_annos(id, place_id, anno_attribute_id,
296
content, flags, expiration, type,
297
dateAdded, lastModified)
298
SELECT (SELECT k.id FROM moz_annos k
299
WHERE k.place_id = OLD.id AND
300
k.anno_attribute_id = a.anno_attribute_id), OLD.id,
301
a.anno_attribute_id, a.content, a.flags, a.expiration, a.type,
302
a.dateAdded, a.lastModified
303
FROM moz_annos a
304
JOIN moz_places h ON h.id = a.place_id
305
WHERE h.id <> OLD.id AND
306
url_hash = OLD.hash AND
307
url = OLD.url;
308
309
/* Reassign bookmarks, and bump the Sync change counter just in case
310
we have new keywords. */
311
UPDATE moz_bookmarks SET
312
fk = OLD.id,
313
syncChangeCounter = syncChangeCounter + 1
314
WHERE fk IN (SELECT id FROM moz_places
315
WHERE url_hash = OLD.hash AND
316
url = OLD.url);
317
318
/* Reassign keywords. */
319
UPDATE moz_keywords SET
320
place_id = OLD.id
321
WHERE place_id IN (SELECT id FROM moz_places
322
WHERE id <> OLD.id AND
323
url_hash = OLD.hash AND
324
url = OLD.url);
325
326
/* Now that we've updated foreign key references, drop the
327
conflicting source. */
328
DELETE FROM moz_places
329
WHERE id <> OLD.id AND
330
url_hash = OLD.hash AND
331
url = OLD.url;
332
333
/* Recalculate frecency for the destination. */
334
UPDATE moz_places SET
335
frecency = calculate_frecency(id)
336
WHERE id = OLD.id;
337
338
/* Trigger frecency updates for affected origins. */
339
DELETE FROM moz_updateoriginsupdate_temp;
340
END`,
341
},
342
{
343
query: `INSERT INTO moz_places_dupes_temp(id, hash, url, count)
344
SELECT h.id, h.url_hash, h.url, 1
345
FROM moz_places h
346
JOIN (SELECT url_hash FROM moz_places
347
GROUP BY url_hash
348
HAVING count(*) > 1) d ON d.url_hash = h.url_hash
349
ON CONFLICT(url) DO UPDATE SET
350
count = count + 1`,
351
},
352
{ query: `DELETE FROM moz_places_dupes_temp WHERE count > 1` },
353
{ query: `DROP TABLE moz_places_dupes_temp` },
354
355
// MOZ_ANNO_ATTRIBUTES
356
// A.1 remove obsolete annotations from moz_annos.
357
// The 'weave0' idiom exploits character ordering (0 follows /) to
358
// efficiently select all annos with a 'weave/' prefix.
359
{
360
query: `DELETE FROM moz_annos
361
WHERE type = 4 OR anno_attribute_id IN (
362
SELECT id FROM moz_anno_attributes
363
WHERE name = 'downloads/destinationFileName' OR
364
name BETWEEN 'weave/' AND 'weave0'
365
)`,
366
},
367
368
// A.2 remove obsolete annotations from moz_items_annos.
369
{
370
query: `DELETE FROM moz_items_annos
371
WHERE type = 4 OR anno_attribute_id IN (
372
SELECT id FROM moz_anno_attributes
373
WHERE name = 'sync/children'
374
OR name = 'placesInternal/GUID'
375
OR name BETWEEN 'weave/' AND 'weave0'
376
)`,
377
},
378
379
// A.3 remove unused attributes.
380
{
381
query: `DELETE FROM moz_anno_attributes WHERE id IN (
382
SELECT id FROM moz_anno_attributes n
383
WHERE NOT EXISTS
384
(SELECT id FROM moz_annos WHERE anno_attribute_id = n.id LIMIT 1)
385
AND NOT EXISTS
386
(SELECT id FROM moz_items_annos WHERE anno_attribute_id = n.id LIMIT 1)
387
)`,
388
},
389
390
// MOZ_ANNOS
391
// B.1 remove annos with an invalid attribute
392
{
393
query: `DELETE FROM moz_annos WHERE id IN (
394
SELECT id FROM moz_annos a
395
WHERE NOT EXISTS
396
(SELECT id FROM moz_anno_attributes
397
WHERE id = a.anno_attribute_id LIMIT 1)
398
)`,
399
},
400
401
// B.2 remove orphan annos
402
{
403
query: `DELETE FROM moz_annos WHERE id IN (
404
SELECT id FROM moz_annos a
405
WHERE NOT EXISTS
406
(SELECT id FROM moz_places WHERE id = a.place_id LIMIT 1)
407
)`,
408
},
409
410
// D.1 remove items that are not uri bookmarks from tag containers
411
{
412
query: `DELETE FROM moz_bookmarks WHERE guid NOT IN (
413
:rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
414
) AND id IN (
415
SELECT b.id FROM moz_bookmarks b
416
WHERE b.parent IN
417
(SELECT id FROM moz_bookmarks WHERE parent = :tags_folder)
418
AND b.type <> :bookmark_type
419
)`,
420
params: {
421
tags_folder: PlacesUtils.tagsFolderId,
422
bookmark_type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
423
rootGuid: PlacesUtils.bookmarks.rootGuid,
424
menuGuid: PlacesUtils.bookmarks.menuGuid,
425
toolbarGuid: PlacesUtils.bookmarks.toolbarGuid,
426
unfiledGuid: PlacesUtils.bookmarks.unfiledGuid,
427
tagsGuid: PlacesUtils.bookmarks.tagsGuid,
428
},
429
},
430
431
// D.2 remove empty tags
432
{
433
query: `DELETE FROM moz_bookmarks WHERE guid NOT IN (
434
:rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
435
) AND id IN (
436
SELECT b.id FROM moz_bookmarks b
437
WHERE b.id IN
438
(SELECT id FROM moz_bookmarks WHERE parent = :tags_folder)
439
AND NOT EXISTS
440
(SELECT id from moz_bookmarks WHERE parent = b.id LIMIT 1)
441
)`,
442
params: {
443
tags_folder: PlacesUtils.tagsFolderId,
444
rootGuid: PlacesUtils.bookmarks.rootGuid,
445
menuGuid: PlacesUtils.bookmarks.menuGuid,
446
toolbarGuid: PlacesUtils.bookmarks.toolbarGuid,
447
unfiledGuid: PlacesUtils.bookmarks.unfiledGuid,
448
tagsGuid: PlacesUtils.bookmarks.tagsGuid,
449
},
450
},
451
452
// D.3 move orphan items to unsorted folder
453
{
454
query: `UPDATE moz_bookmarks SET
455
parent = (SELECT id FROM moz_bookmarks WHERE guid = :unfiledGuid)
456
WHERE guid NOT IN (
457
:rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
458
) AND id IN (
459
SELECT b.id FROM moz_bookmarks b
460
WHERE NOT EXISTS
461
(SELECT id FROM moz_bookmarks WHERE id = b.parent LIMIT 1)
462
)`,
463
params: {
464
rootGuid: PlacesUtils.bookmarks.rootGuid,
465
menuGuid: PlacesUtils.bookmarks.menuGuid,
466
toolbarGuid: PlacesUtils.bookmarks.toolbarGuid,
467
unfiledGuid: PlacesUtils.bookmarks.unfiledGuid,
468
tagsGuid: PlacesUtils.bookmarks.tagsGuid,
469
},
470
},
471
472
// D.4 Insert tombstones for any synced items with the wrong type.
473
// Sync doesn't support changing the type of an existing item while
474
// keeping its GUID. To avoid confusing other clients, we insert
475
// tombstones for all synced items with the wrong type, so that we
476
// can reupload them with the correct type and a new GUID.
477
{
478
query: `INSERT OR IGNORE INTO moz_bookmarks_deleted(guid, dateRemoved)
479
SELECT guid, :dateRemoved
480
FROM moz_bookmarks
481
WHERE syncStatus <> :syncStatus AND
482
((type IN (:folder_type, :separator_type) AND
483
fk NOTNULL) OR
484
(type = :bookmark_type AND
485
fk IS NULL) OR
486
type IS NULL)`,
487
params: {
488
dateRemoved: PlacesUtils.toPRTime(new Date()),
489
syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NEW,
490
bookmark_type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
491
folder_type: PlacesUtils.bookmarks.TYPE_FOLDER,
492
separator_type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
493
},
494
},
495
496
// D.5 fix wrong item types
497
// Folders and separators should not have an fk.
498
// If they have a valid fk, convert them to bookmarks, and give them new
499
// GUIDs. If the item has children, we'll move them to the unfiled root
500
// in D.8. If the `fk` doesn't exist in `moz_places`, we'll remove the
501
// item in D.9.
502
{
503
query: `UPDATE moz_bookmarks
504
SET guid = GENERATE_GUID(),
505
type = :bookmark_type
506
WHERE guid NOT IN (
507
:rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
508
) AND id IN (
509
SELECT id FROM moz_bookmarks b
510
WHERE type IN (:folder_type, :separator_type)
511
AND fk NOTNULL
512
)`,
513
params: {
514
bookmark_type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
515
folder_type: PlacesUtils.bookmarks.TYPE_FOLDER,
516
separator_type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
517
rootGuid: PlacesUtils.bookmarks.rootGuid,
518
menuGuid: PlacesUtils.bookmarks.menuGuid,
519
toolbarGuid: PlacesUtils.bookmarks.toolbarGuid,
520
unfiledGuid: PlacesUtils.bookmarks.unfiledGuid,
521
tagsGuid: PlacesUtils.bookmarks.tagsGuid,
522
},
523
},
524
525
// D.6 fix wrong item types
526
// Bookmarks should have an fk, if they don't have any, convert them to
527
// folders.
528
{
529
query: `UPDATE moz_bookmarks
530
SET guid = GENERATE_GUID(),
531
type = :folder_type
532
WHERE guid NOT IN (
533
:rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
534
) AND id IN (
535
SELECT id FROM moz_bookmarks b
536
WHERE type = :bookmark_type
537
AND fk IS NULL
538
)`,
539
params: {
540
bookmark_type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
541
folder_type: PlacesUtils.bookmarks.TYPE_FOLDER,
542
rootGuid: PlacesUtils.bookmarks.rootGuid,
543
menuGuid: PlacesUtils.bookmarks.menuGuid,
544
toolbarGuid: PlacesUtils.bookmarks.toolbarGuid,
545
unfiledGuid: PlacesUtils.bookmarks.unfiledGuid,
546
tagsGuid: PlacesUtils.bookmarks.tagsGuid,
547
},
548
},
549
550
// D.7 fix wrong item types
551
// `moz_bookmarks.type` doesn't have a NOT NULL constraint, so it's
552
// possible for an item to not have a type (bug 1586427).
553
{
554
query: `UPDATE moz_bookmarks
555
SET guid = GENERATE_GUID(),
556
type = CASE WHEN fk NOT NULL THEN :bookmark_type ELSE :folder_type END
557
WHERE guid NOT IN (
558
:rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
559
) AND type IS NULL`,
560
params: {
561
bookmark_type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
562
folder_type: PlacesUtils.bookmarks.TYPE_FOLDER,
563
rootGuid: PlacesUtils.bookmarks.rootGuid,
564
menuGuid: PlacesUtils.bookmarks.menuGuid,
565
toolbarGuid: PlacesUtils.bookmarks.toolbarGuid,
566
unfiledGuid: PlacesUtils.bookmarks.unfiledGuid,
567
tagsGuid: PlacesUtils.bookmarks.tagsGuid,
568
},
569
},
570
571
// D.8 fix wrong parents
572
// Items cannot have separators or other bookmarks
573
// as parent, if they have bad parent move them to unsorted bookmarks.
574
{
575
query: `UPDATE moz_bookmarks SET
576
parent = (SELECT id FROM moz_bookmarks WHERE guid = :unfiledGuid)
577
WHERE guid NOT IN (
578
:rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
579
) AND id IN (
580
SELECT id FROM moz_bookmarks b
581
WHERE EXISTS
582
(SELECT id FROM moz_bookmarks WHERE id = b.parent
583
AND type IN (:bookmark_type, :separator_type)
584
LIMIT 1)
585
)`,
586
params: {
587
bookmark_type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
588
separator_type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
589
rootGuid: PlacesUtils.bookmarks.rootGuid,
590
menuGuid: PlacesUtils.bookmarks.menuGuid,
591
toolbarGuid: PlacesUtils.bookmarks.toolbarGuid,
592
unfiledGuid: PlacesUtils.bookmarks.unfiledGuid,
593
tagsGuid: PlacesUtils.bookmarks.tagsGuid,
594
},
595
},
596
597
// D.9 remove items without a valid place
598
// We've already converted folders with an `fk` to bookmarks in D.5,
599
// and bookmarks without an `fk` to folders in D.6. However, the `fk`
600
// might not reference an existing `moz_places.id`, even if it's
601
// NOT NULL. This statement takes care of those.
602
{
603
query: `DELETE FROM moz_bookmarks AS b
604
WHERE b.guid NOT IN (
605
:rootGuid, :menuGuid, :toolbarGuid, :unfiledGuid, :tagsGuid /* skip roots */
606
) AND b.fk NOT NULL
607
AND b.type = :bookmark_type
608
AND NOT EXISTS (SELECT 1 FROM moz_places h WHERE h.id = b.fk)`,
609
params: {
610
bookmark_type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
611
rootGuid: PlacesUtils.bookmarks.rootGuid,
612
menuGuid: PlacesUtils.bookmarks.menuGuid,
613
toolbarGuid: PlacesUtils.bookmarks.toolbarGuid,
614
unfiledGuid: PlacesUtils.bookmarks.unfiledGuid,
615
tagsGuid: PlacesUtils.bookmarks.tagsGuid,
616
},
617
},
618
619
// D.10 recalculate positions
620
// This requires multiple related statements.
621
// We can detect a folder with bad position values comparing the sum of
622
// all distinct position values (+1 since position is 0-based) with the
623
// triangular numbers obtained by the number of children (n).
624
// SUM(DISTINCT position + 1) == (n * (n + 1) / 2).
625
// id is not a PRIMARY KEY on purpose, since we need a rowid that
626
// increments monotonically.
627
{
628
query: `CREATE TEMP TABLE IF NOT EXISTS moz_bm_reindex_temp (
629
id INTEGER
630
, parent INTEGER
631
, position INTEGER
632
)`,
633
},
634
{
635
query: `INSERT INTO moz_bm_reindex_temp
636
SELECT id, parent, 0
637
FROM moz_bookmarks b
638
WHERE parent IN (
639
SELECT parent
640
FROM moz_bookmarks
641
GROUP BY parent
642
HAVING (SUM(DISTINCT position + 1) - (count(*) * (count(*) + 1) / 2)) <> 0
643
)
644
ORDER BY parent ASC, position ASC, ROWID ASC`,
645
},
646
{
647
query: `CREATE INDEX IF NOT EXISTS moz_bm_reindex_temp_index
648
ON moz_bm_reindex_temp(parent)`,
649
},
650
{
651
query: `UPDATE moz_bm_reindex_temp SET position = (
652
ROWID - (SELECT MIN(t.ROWID) FROM moz_bm_reindex_temp t
653
WHERE t.parent = moz_bm_reindex_temp.parent)
654
)`,
655
},
656
{
657
query: `CREATE TEMP TRIGGER IF NOT EXISTS moz_bm_reindex_temp_trigger
658
BEFORE DELETE ON moz_bm_reindex_temp
659
FOR EACH ROW
660
BEGIN
661
UPDATE moz_bookmarks SET position = OLD.position WHERE id = OLD.id;
662
END`,
663
},
664
{ query: `DELETE FROM moz_bm_reindex_temp` },
665
{ query: `DROP INDEX moz_bm_reindex_temp_index` },
666
{ query: `DROP TRIGGER moz_bm_reindex_temp_trigger` },
667
{ query: `DROP TABLE moz_bm_reindex_temp` },
668
669
// D.12 Fix empty-named tags.
670
// Tags were allowed to have empty names due to a UI bug. Fix them by
671
// replacing their title with "(notitle)", and bumping the change counter
672
// for all bookmarks with the fixed tags.
673
{
674
query: `UPDATE moz_bookmarks SET syncChangeCounter = syncChangeCounter + 1
675
WHERE fk IN (SELECT b.fk FROM moz_bookmarks b
676
JOIN moz_bookmarks p ON p.id = b.parent
677
WHERE length(p.title) = 0 AND p.type = :folder_type AND
678
p.parent = :tags_folder)`,
679
params: {
680
folder_type: PlacesUtils.bookmarks.TYPE_FOLDER,
681
tags_folder: PlacesUtils.tagsFolderId,
682
},
683
},
684
{
685
query: `UPDATE moz_bookmarks SET title = :empty_title
686
WHERE length(title) = 0 AND type = :folder_type
687
AND parent = :tags_folder`,
688
params: {
689
empty_title: "(notitle)",
690
folder_type: PlacesUtils.bookmarks.TYPE_FOLDER,
691
tags_folder: PlacesUtils.tagsFolderId,
692
},
693
},
694
695
// MOZ_ICONS
696
// E.1 remove orphan icon entries.
697
{
698
query: `DELETE FROM moz_pages_w_icons WHERE page_url_hash NOT IN (
699
SELECT url_hash FROM moz_places
700
)`,
701
},
702
703
// Remove icons whose origin is not in moz_origins, unless referenced.
704
{
705
query: `DELETE FROM moz_icons WHERE id IN (
706
SELECT id FROM moz_icons WHERE root = 0
707
UNION ALL
708
SELECT id FROM moz_icons
709
WHERE root = 1
710
AND get_host_and_port(icon_url) NOT IN (SELECT host FROM moz_origins)
711
AND fixup_url(get_host_and_port(icon_url)) NOT IN (SELECT host FROM moz_origins)
712
EXCEPT
713
SELECT icon_id FROM moz_icons_to_pages
714
)`,
715
},
716
717
// MOZ_HISTORYVISITS
718
// F.1 remove orphan visits
719
{
720
query: `DELETE FROM moz_historyvisits WHERE id IN (
721
SELECT id FROM moz_historyvisits v
722
WHERE NOT EXISTS
723
(SELECT id FROM moz_places WHERE id = v.place_id LIMIT 1)
724
)`,
725
},
726
727
// MOZ_INPUTHISTORY
728
// G.1 remove orphan input history
729
{
730
query: `DELETE FROM moz_inputhistory WHERE place_id IN (
731
SELECT place_id FROM moz_inputhistory i
732
WHERE NOT EXISTS
733
(SELECT id FROM moz_places WHERE id = i.place_id LIMIT 1)
734
)`,
735
},
736
737
// MOZ_ITEMS_ANNOS
738
// H.1 remove item annos with an invalid attribute
739
{
740
query: `DELETE FROM moz_items_annos WHERE id IN (
741
SELECT id FROM moz_items_annos t
742
WHERE NOT EXISTS
743
(SELECT id FROM moz_anno_attributes
744
WHERE id = t.anno_attribute_id LIMIT 1)
745
)`,
746
},
747
748
// H.2 remove orphan item annos
749
{
750
query: `DELETE FROM moz_items_annos WHERE id IN (
751
SELECT id FROM moz_items_annos t
752
WHERE NOT EXISTS
753
(SELECT id FROM moz_bookmarks WHERE id = t.item_id LIMIT 1)
754
)`,
755
},
756
757
// MOZ_KEYWORDS
758
// I.1 remove unused keywords
759
{
760
query: `DELETE FROM moz_keywords WHERE id IN (
761
SELECT id FROM moz_keywords k
762
WHERE NOT EXISTS
763
(SELECT 1 FROM moz_places h WHERE k.place_id = h.id)
764
)`,
765
},
766
767
// MOZ_PLACES
768
// L.2 recalculate visit_count and last_visit_date
769
{
770
query: `UPDATE moz_places
771
SET visit_count = (SELECT count(*) FROM moz_historyvisits
772
WHERE place_id = moz_places.id AND visit_type NOT IN (0,4,7,8,9)),
773
last_visit_date = (SELECT MAX(visit_date) FROM moz_historyvisits
774
WHERE place_id = moz_places.id)
775
WHERE id IN (
776
SELECT h.id FROM moz_places h
777
WHERE visit_count <> (SELECT count(*) FROM moz_historyvisits v
778
WHERE v.place_id = h.id AND visit_type NOT IN (0,4,7,8,9))
779
OR last_visit_date <> (SELECT MAX(visit_date) FROM moz_historyvisits v
780
WHERE v.place_id = h.id)
781
)`,
782
},
783
784
// L.3 recalculate hidden for redirects.
785
{
786
query: `UPDATE moz_places
787
SET hidden = 1
788
WHERE id IN (
789
SELECT h.id FROM moz_places h
790
JOIN moz_historyvisits src ON src.place_id = h.id
791
JOIN moz_historyvisits dst ON dst.from_visit = src.id AND dst.visit_type IN (5,6)
792
LEFT JOIN moz_bookmarks on fk = h.id AND fk ISNULL
793
GROUP BY src.place_id HAVING count(*) = visit_count
794
)`,
795
},
796
797
// L.4 recalculate foreign_count.
798
{
799
query: `UPDATE moz_places SET foreign_count =
800
(SELECT count(*) FROM moz_bookmarks WHERE fk = moz_places.id ) +
801
(SELECT count(*) FROM moz_keywords WHERE place_id = moz_places.id )`,
802
},
803
804
// L.5 recalculate missing hashes.
805
{
806
query: `UPDATE moz_places SET url_hash = hash(url) WHERE url_hash = 0`,
807
},
808
809
// L.6 fix invalid Place GUIDs.
810
{
811
query: `UPDATE moz_places
812
SET guid = GENERATE_GUID()
813
WHERE guid IS NULL OR
814
NOT IS_VALID_GUID(guid)`,
815
},
816
817
// MOZ_BOOKMARKS
818
// S.1 fix invalid GUIDs for synced bookmarks.
819
// This requires multiple related statements.
820
// First, we insert tombstones for all synced bookmarks with invalid
821
// GUIDs, so that we can delete them on the server. Second, we add a
822
// temporary trigger to bump the change counter for the parents of any
823
// items we update, since Sync stores the list of child GUIDs on the
824
// parent. Finally, we assign new GUIDs for all items with missing and
825
// invalid GUIDs, bump their change counters, and reset their sync
826
// statuses to NEW so that they're considered for deduping.
827
{
828
query: `INSERT OR IGNORE INTO moz_bookmarks_deleted(guid, dateRemoved)
829
SELECT guid, :dateRemoved
830
FROM moz_bookmarks
831
WHERE syncStatus <> :syncStatus AND
832
guid NOT NULL AND
833
NOT IS_VALID_GUID(guid)`,
834
params: {
835
dateRemoved: PlacesUtils.toPRTime(new Date()),
836
syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NEW,
837
},
838
},
839
{
840
query: `UPDATE moz_bookmarks
841
SET guid = GENERATE_GUID(),
842
syncStatus = :syncStatus
843
WHERE guid IS NULL OR
844
NOT IS_VALID_GUID(guid)`,
845
params: {
846
syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NEW,
847
},
848
},
849
850
// S.2 drop tombstones for bookmarks that aren't deleted.
851
{
852
query: `DELETE FROM moz_bookmarks_deleted
853
WHERE guid IN (SELECT guid FROM moz_bookmarks)`,
854
},
855
856
// S.3 set missing added and last modified dates.
857
{
858
query: `UPDATE moz_bookmarks
859
SET dateAdded = COALESCE(NULLIF(dateAdded, 0), NULLIF(lastModified, 0), NULLIF((
860
SELECT MIN(visit_date) FROM moz_historyvisits
861
WHERE place_id = fk
862
), 0), STRFTIME('%s', 'now', 'localtime', 'utc') * 1000000),
863
lastModified = COALESCE(NULLIF(lastModified, 0), NULLIF(dateAdded, 0), NULLIF((
864
SELECT MAX(visit_date) FROM moz_historyvisits
865
WHERE place_id = fk
866
), 0), STRFTIME('%s', 'now', 'localtime', 'utc') * 1000000)
867
WHERE NULLIF(dateAdded, 0) IS NULL OR
868
NULLIF(lastModified, 0) IS NULL`,
869
},
870
871
// S.4 reset added dates that are ahead of last modified dates.
872
{
873
query: `UPDATE moz_bookmarks
874
SET dateAdded = lastModified
875
WHERE dateAdded > lastModified`,
876
},
877
];
878
879
// Create triggers for updating Sync metadata. The "sync change" trigger
880
// bumps the parent's change counter when we update a GUID or move an item
881
// to a different folder, since Sync stores the list of child GUIDs on the
882
// parent. The "sync tombstone" trigger inserts tombstones for deleted
883
// synced bookmarks.
884
cleanupStatements.unshift({
885
query: `CREATE TEMP TRIGGER IF NOT EXISTS moz_bm_sync_change_temp_trigger
886
AFTER UPDATE OF guid, parent, position ON moz_bookmarks
887
FOR EACH ROW
888
BEGIN
889
UPDATE moz_bookmarks
890
SET syncChangeCounter = syncChangeCounter + 1
891
WHERE id IN (OLD.parent, NEW.parent, NEW.id);
892
END`,
893
});
894
cleanupStatements.unshift({
895
query: `CREATE TEMP TRIGGER IF NOT EXISTS moz_bm_sync_tombstone_temp_trigger
896
AFTER DELETE ON moz_bookmarks
897
FOR EACH ROW WHEN OLD.guid NOT NULL AND
898
OLD.syncStatus <> 1
899
BEGIN
900
UPDATE moz_bookmarks
901
SET syncChangeCounter = syncChangeCounter + 1
902
WHERE id = OLD.parent;
903
904
INSERT INTO moz_bookmarks_deleted(guid, dateRemoved)
905
VALUES(OLD.guid, STRFTIME('%s', 'now', 'localtime', 'utc') * 1000000);
906
END`,
907
});
908
cleanupStatements.push({
909
query: `DROP TRIGGER moz_bm_sync_change_temp_trigger`,
910
});
911
cleanupStatements.push({
912
query: `DROP TRIGGER moz_bm_sync_tombstone_temp_trigger`,
913
});
914
915
return cleanupStatements;
916
},
917
918
/**
919
* Tries to vacuum the database.
920
*
921
* Note: although this function isn't actually async, we keep it async to
922
* allow us to maintain a simple, consistent API for the tasks within this object.
923
*
924
* @return {Promise} resolves when database is vacuumed.
925
* @resolves to an array of logs for this task.
926
* @rejects if we are unable to vacuum database.
927
*/
928
async vacuum() {
929
let logs = [];
930
let placesDbPath = OS.Path.join(
931
OS.Constants.Path.profileDir,
932
"places.sqlite"
933
);
934
let info = await OS.File.stat(placesDbPath);
935
logs.push(`Initial database size is ${parseInt(info.size / 1024)}KiB`);
936
return PlacesUtils.withConnectionWrapper(
937
"PlacesDBUtils: vacuum",
938
async db => {
939
await db.execute("VACUUM");
940
logs.push("The database has been vacuumed");
941
info = await OS.File.stat(placesDbPath);
942
logs.push(`Final database size is ${parseInt(info.size / 1024)}KiB`);
943
return logs;
944
}
945
).catch(() => {
946
PlacesDBUtils.clearPendingTasks();
947
throw new Error("Unable to vacuum database");
948
});
949
},
950
951
/**
952
* Forces a full expiration on the database.
953
*
954
* Note: although this function isn't actually async, we keep it async to
955
* allow us to maintain a simple, consistent API for the tasks within this object.
956
*
957
* @return {Promise} resolves when the database in cleaned up.
958
* @resolves to an array of logs for this task.
959
*/
960
async expire() {
961
let logs = [];
962
963
let expiration = Cc["@mozilla.org/places/expiration;1"].getService(
964
Ci.nsIObserver
965
);
966
967
let returnPromise = new Promise(res => {
968
let observer = (subject, topic, data) => {
969
Services.obs.removeObserver(observer, topic);
970
logs.push("Database cleaned up");
971
res(logs);
972
};
973
Services.obs.addObserver(observer, PlacesUtils.TOPIC_EXPIRATION_FINISHED);
974
});
975
976
// Force an orphans expiration step.
977
expiration.observe(null, "places-debug-start-expiration", 0);
978
return returnPromise;
979
},
980
981
/**
982
* Collects statistical data on the database.
983
*
984
* @return {Promise} resolves when statistics are collected.
985
* @resolves to an array of logs for this task.
986
* @rejects if we are unable to collect stats for some reason.
987
*/
988
async stats() {
989
let logs = [];
990
let placesDbPath = OS.Path.join(
991
OS.Constants.Path.profileDir,
992
"places.sqlite"
993
);
994
let info = await OS.File.stat(placesDbPath);
995
logs.push(`Places.sqlite size is ${parseInt(info.size / 1024)}KiB`);
996
let faviconsDbPath = OS.Path.join(
997
OS.Constants.Path.profileDir,
998
"favicons.sqlite"
999
);
1000
info = await OS.File.stat(faviconsDbPath);
1001
logs.push(`Favicons.sqlite size is ${parseInt(info.size / 1024)}KiB`);
1002
1003
// Execute each step async.
1004
let pragmas = [
1005
"user_version",
1006
"page_size",
1007
"cache_size",
1008
"journal_mode",
1009
"synchronous",
1010
].map(p => `pragma_${p}`);
1011
let pragmaQuery = `SELECT * FROM ${pragmas.join(", ")}`;
1012
await PlacesUtils.withConnectionWrapper(
1013
"PlacesDBUtils: pragma for stats",
1014
async db => {
1015
let row = (await db.execute(pragmaQuery))[0];
1016
for (let i = 0; i != pragmas.length; i++) {
1017
logs.push(`${pragmas[i]} is ${row.getResultByIndex(i)}`);
1018
}
1019
}
1020
).catch(() => {
1021
logs.push("Could not set pragma for stat collection");
1022
});
1023
1024
// Get maximum number of unique URIs.
1025
try {
1026
let limitURIs = Services.prefs.getIntPref(
1027
"places.history.expiration.transient_current_max_pages"
1028
);
1029
logs.push(
1030
"History can store a maximum of " + limitURIs + " unique pages"
1031
);
1032
} catch (ex) {}
1033
1034
let query = "SELECT name FROM sqlite_master WHERE type = :type";
1035
let params = {};
1036
let _getTableCount = async tableName => {
1037
let db = await PlacesUtils.promiseDBConnection();
1038
let rows = await db.execute(`SELECT count(*) FROM ${tableName}`);
1039
logs.push(
1040
`Table ${tableName} has ${rows[0].getResultByIndex(0)} records`
1041
);
1042
};
1043
1044
try {
1045
params.type = "table";
1046
let db = await PlacesUtils.promiseDBConnection();
1047
await db.execute(query, params, r =>
1048
_getTableCount(r.getResultByIndex(0))
1049
);
1050
1051
params.type = "index";
1052
await db.execute(query, params, r => {
1053
logs.push(`Index ${r.getResultByIndex(0)}`);
1054
});
1055
1056
params.type = "trigger";
1057
await db.execute(query, params, r => {
1058
logs.push(`Trigger ${r.getResultByIndex(0)}`);
1059
});
1060
} catch (ex) {
1061
throw new Error("Unable to collect stats.");
1062
}
1063
1064
return logs;
1065
},
1066
1067
/**
1068
* Recalculates statistical data on the origin frecencies in the database.
1069
*
1070
* @return {Promise} resolves when statistics are collected.
1071
*/
1072
originFrecencyStats() {
1073
return new Promise(resolve => {
1074
PlacesUtils.history.recalculateOriginFrecencyStats(() =>
1075
resolve(["Recalculated origin frecency stats"])
1076
);
1077
});
1078
},
1079
1080
/**
1081
* Collects telemetry data and reports it to Telemetry.
1082
*
1083
* Note: although this function isn't actually async, we keep it async to
1084
* allow us to maintain a simple, consistent API for the tasks within this object.
1085
*
1086
*/
1087
async telemetry() {
1088
// This will be populated with one integer property for each probe result,
1089
// using the histogram name as key.
1090
let probeValues = {};
1091
1092
// The following array contains an ordered list of entries that are
1093
// processed to collect telemetry data. Each entry has these properties:
1094
//
1095
// histogram: Name of the telemetry histogram to update.
1096
// query: This is optional. If present, contains a database command
1097
// that will be executed asynchronously, and whose result will
1098
// be added to the telemetry histogram.
1099
// callback: This is optional. If present, contains a function that must
1100
// return the value that will be added to the telemetry
1101
// histogram. If a query is also present, its result is passed
1102
// as the first argument of the function. If the function
1103
// raises an exception, no data is added to the histogram.
1104
//
1105
// Since all queries are executed in order by the database backend, the
1106
// callbacks can also use the result of previous queries stored in the
1107
// probeValues object.
1108
let probes = [
1109
{
1110
histogram: "PLACES_PAGES_COUNT",
1111
query: "SELECT count(*) FROM moz_places",
1112
},
1113
1114
{
1115
histogram: "PLACES_BOOKMARKS_COUNT",
1116
query: `SELECT count(*) FROM moz_bookmarks b
1117
JOIN moz_bookmarks t ON t.id = b.parent
1118
AND t.parent <> :tags_folder
1119
WHERE b.type = :type_bookmark`,
1120
params: {
1121
tags_folder: PlacesUtils.tagsFolderId,
1122
type_bookmark: PlacesUtils.bookmarks.TYPE_BOOKMARK,
1123
},
1124
},
1125
1126
{
1127
histogram: "PLACES_TAGS_COUNT",
1128
query: `SELECT count(*) FROM moz_bookmarks
1129
WHERE parent = :tags_folder`,
1130
params: {
1131
tags_folder: PlacesUtils.tagsFolderId,
1132
},
1133
},
1134
1135
{
1136
histogram: "PLACES_KEYWORDS_COUNT",
1137
query: "SELECT count(*) FROM moz_keywords",
1138
},
1139
1140
{
1141
histogram: "PLACES_SORTED_BOOKMARKS_PERC",
1142
query: `SELECT IFNULL(ROUND((
1143
SELECT count(*) FROM moz_bookmarks b
1144
JOIN moz_bookmarks t ON t.id = b.parent
1145
AND t.parent <> :tags_folder AND t.parent > :places_root
1146
WHERE b.type = :type_bookmark
1147
) * 100 / (
1148
SELECT count(*) FROM moz_bookmarks b
1149
JOIN moz_bookmarks t ON t.id = b.parent
1150
AND t.parent <> :tags_folder
1151
WHERE b.type = :type_bookmark
1152
)), 0)`,
1153
params: {
1154
places_root: PlacesUtils.placesRootId,
1155
tags_folder: PlacesUtils.tagsFolderId,
1156
type_bookmark: PlacesUtils.bookmarks.TYPE_BOOKMARK,
1157
},
1158
},
1159
1160
{
1161
histogram: "PLACES_TAGGED_BOOKMARKS_PERC",
1162
query: `SELECT IFNULL(ROUND((
1163
SELECT count(*) FROM moz_bookmarks b
1164
JOIN moz_bookmarks t ON t.id = b.parent
1165
AND t.parent = :tags_folder
1166
) * 100 / (
1167
SELECT count(*) FROM moz_bookmarks b
1168
JOIN moz_bookmarks t ON t.id = b.parent
1169
AND t.parent <> :tags_folder
1170
WHERE b.type = :type_bookmark
1171
)), 0)`,
1172
params: {
1173
tags_folder: PlacesUtils.tagsFolderId,
1174
type_bookmark: PlacesUtils.bookmarks.TYPE_BOOKMARK,
1175
},
1176
},
1177
1178
{
1179
histogram: "PLACES_DATABASE_FILESIZE_MB",
1180
async callback() {
1181
let placesDbPath = OS.Path.join(
1182
OS.Constants.Path.profileDir,
1183
"places.sqlite"
1184
);
1185
let info = await OS.File.stat(placesDbPath);
1186
return parseInt(info.size / BYTES_PER_MEBIBYTE);
1187
},
1188
},
1189
1190
{
1191
histogram: "PLACES_DATABASE_PAGESIZE_B",
1192
query: "PRAGMA page_size /* PlacesDBUtils.jsm PAGESIZE_B */",
1193
},
1194
1195
{
1196
histogram: "PLACES_DATABASE_SIZE_PER_PAGE_B",
1197
query: "PRAGMA page_count",
1198
callback(aDbPageCount) {
1199
// Note that the database file size would not be meaningful for this
1200
// calculation, because the file grows in fixed-size chunks.
1201
let dbPageSize = probeValues.PLACES_DATABASE_PAGESIZE_B;
1202
let placesPageCount = probeValues.PLACES_PAGES_COUNT;
1203
return Math.round((dbPageSize * aDbPageCount) / placesPageCount);
1204
},
1205
},
1206
1207
{
1208
histogram: "PLACES_DATABASE_FAVICONS_FILESIZE_MB",
1209
async callback() {
1210
let faviconsDbPath = OS.Path.join(
1211
OS.Constants.Path.profileDir,
1212
"favicons.sqlite"
1213
);
1214
let info = await OS.File.stat(faviconsDbPath);
1215
return parseInt(info.size / BYTES_PER_MEBIBYTE);
1216
},
1217
},
1218
1219
{
1220
histogram: "PLACES_ANNOS_BOOKMARKS_COUNT",
1221
query: "SELECT count(*) FROM moz_items_annos",
1222
},
1223
1224
{
1225
histogram: "PLACES_ANNOS_PAGES_COUNT",
1226
query: "SELECT count(*) FROM moz_annos",
1227
},
1228
1229
{
1230
histogram: "PLACES_MAINTENANCE_DAYSFROMLAST",
1231
callback() {
1232
try {
1233
let lastMaintenance = Services.prefs.getIntPref(
1234
"places.database.lastMaintenance"
1235
);
1236
let nowSeconds = parseInt(Date.now() / 1000);
1237
return parseInt((nowSeconds - lastMaintenance) / 86400);
1238
} catch (ex) {
1239
return 60;
1240
}
1241
},
1242
},
1243
];
1244
1245
for (let probe of probes) {
1246
let val;
1247
if ("query" in probe) {
1248
let db = await PlacesUtils.promiseDBConnection();
1249
val = (await db.execute(
1250
probe.query,
1251
probe.params || {}
1252
))[0].getResultByIndex(0);
1253
}
1254
// Report the result of the probe through Telemetry.
1255
// The resulting promise cannot reject.
1256
if ("callback" in probe) {
1257
val = await probe.callback(val);
1258
}
1259
probeValues[probe.histogram] = val;
1260
Services.telemetry.getHistogramById(probe.histogram).add(val);
1261
}
1262
},
1263
1264
/**
1265
* Runs a list of tasks, returning a Map when done.
1266
*
1267
* @param tasks
1268
* Array of tasks to be executed, in form of pointers to methods in
1269
* this module.
1270
* @return {Promise}
1271
* A promise that resolves with a Map[taskName(String) -> Object].
1272
* The Object has the following properties:
1273
* - succeeded: boolean
1274
* - logs: an array of strings containing the messages logged by the task
1275
*/
1276
async runTasks(tasks) {
1277
PlacesDBUtils._clearTaskQueue = false;
1278
let tasksMap = new Map();
1279
for (let task of tasks) {
1280
if (PlacesDBUtils._isShuttingDown) {
1281
tasksMap.set(task.name, {
1282
succeeded: false,
1283
logs: ["Shutting down, will not schedule the task."],
1284
});
1285
continue;
1286
}
1287
1288
if (PlacesDBUtils._clearTaskQueue) {
1289
tasksMap.set(task.name, {
1290
succeeded: false,
1291
logs: ["The task queue was cleared by an error in another task."],
1292
});
1293
continue;
1294
}
1295
1296
let result = await task()
1297
.then((logs = [`${task.name} complete`]) => ({ succeeded: true, logs }))
1298
.catch(err => ({ succeeded: false, logs: [err.message] }));
1299
tasksMap.set(task.name, result);
1300
}
1301
return tasksMap;
1302
},
1303
};
1304
1305
async function integrity(dbName) {
1306
async function check(db) {
1307
let row;
1308
await db.execute("PRAGMA integrity_check", null, (r, cancel) => {
1309
row = r;
1310
cancel();
1311
});
1312
return row.getResultByIndex(0) === "ok";
1313
}
1314
1315
// Create a new connection for this check, so we can operate independently
1316
// from a broken Places service.
1317
// openConnection returns an exception with .result == Cr.NS_ERROR_FILE_CORRUPTED,
1318
// we should do the same everywhere we want maintenance to try replacing the
1319
// database on next startup.
1320
let path = OS.Path.join(OS.Constants.Path.profileDir, dbName);
1321
let db = await Sqlite.openConnection({ path });
1322
try {
1323
if (await check(db)) {
1324
return;
1325
}
1326
1327
// We stopped due to an integrity corruption, try to fix it if possible.
1328
// First, try to reindex, this often fixes simple indices problems.
1329
try {
1330
await db.execute("REINDEX");
1331
} catch (ex) {
1332
throw new Components.Exception(
1333
"Impossible to reindex database",
1334
Cr.NS_ERROR_FILE_CORRUPTED
1335
);
1336
}
1337
1338
// Check again.
1339
if (!(await check(db))) {
1340
throw new Components.Exception(
1341
"The database is still corrupt",
1342
Cr.NS_ERROR_FILE_CORRUPTED
1343
);
1344
}
1345
} finally {
1346
await db.close();
1347
}
1348
}