зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1720151 - Hook up Snapshot handling to the real database. r=mak
Differential Revision: https://phabricator.services.mozilla.com/D119652
This commit is contained in:
Родитель
0f85cc5da1
Коммит
317fbf055e
|
@ -49,6 +49,10 @@ const Snapshots = new (class Snapshots {
|
|||
/**
|
||||
* Adds a new snapshot.
|
||||
*
|
||||
* If the snapshot already exists, and this is a user-persisted addition,
|
||||
* then the userPersisted flag will be set, and the removed_at flag will be
|
||||
* cleared.
|
||||
*
|
||||
* @param {object} details
|
||||
* @param {string} details.url
|
||||
* The url associated with the snapshot.
|
||||
|
@ -57,53 +61,48 @@ const Snapshots = new (class Snapshots {
|
|||
* false.
|
||||
*/
|
||||
async add({ url, userPersisted = false }) {
|
||||
let now = new Date();
|
||||
|
||||
let db = await PlacesUtils.promiseDBConnection();
|
||||
let rows = await db.executeCached(
|
||||
`
|
||||
WITH places(place_id) AS (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url),
|
||||
inserts(created_at, updated_at, document_type) AS (
|
||||
VALUES(
|
||||
(SELECT min(created_at) FROM moz_places_metadata WHERE place_id in places),
|
||||
(SELECT max(updated_at) FROM moz_places_metadata WHERE place_id in places),
|
||||
(SELECT document_type FROM moz_places_metadata WHERE place_id in places ORDER BY updated_at DESC LIMIT 1)
|
||||
)
|
||||
)
|
||||
SELECT * from inserts WHERE created_at is not null
|
||||
`,
|
||||
{ url }
|
||||
);
|
||||
if (!rows.length) {
|
||||
throw new Error("Could not find existing interactions");
|
||||
if (!url) {
|
||||
throw new Error("Missing url parameter to Snapshots.add()");
|
||||
}
|
||||
|
||||
this.#snapshots.set(url, {
|
||||
url,
|
||||
userPersisted,
|
||||
createdAt: now,
|
||||
removedAt: null,
|
||||
documentType: rows[0].getResultByName("document_type"),
|
||||
firstInteractionAt: new Date(rows[0].getResultByName("created_at")),
|
||||
lastInteractionAt: new Date(rows[0].getResultByName("updated_at")),
|
||||
await PlacesUtils.withConnectionWrapper("Snapshots: add", async db => {
|
||||
await db.executeCached(
|
||||
`
|
||||
INSERT INTO moz_places_metadata_snapshots
|
||||
(place_id, first_interaction_at, last_interaction_at, document_type, created_at, user_persisted)
|
||||
SELECT place_id, min(created_at), max(created_at),
|
||||
first_value(document_type) OVER (PARTITION BY place_id ORDER BY created_at DESC),
|
||||
:createdAt, :userPersisted
|
||||
FROM moz_places_metadata
|
||||
WHERE place_id = (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url)
|
||||
ON CONFLICT DO UPDATE SET user_persisted = :userPersisted, removed_at = NULL WHERE :userPersisted = 1
|
||||
`,
|
||||
{ createdAt: Date.now(), url, userPersisted }
|
||||
);
|
||||
});
|
||||
|
||||
Services.obs.notifyObservers(null, "places-snapshot-added", url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a snapshot, creating a tombstone.
|
||||
* Deletes a snapshot, creating a tombstone. Note, the caller is expected
|
||||
* to take account of the userPersisted value for a Snapshot when appropriate.
|
||||
*
|
||||
* @param {string} url
|
||||
* The url of the snapshot to delete.
|
||||
*/
|
||||
async delete(url) {
|
||||
let snapshot = this.#snapshots.get(url);
|
||||
await PlacesUtils.withConnectionWrapper("Snapshots: delete", async db => {
|
||||
await db.executeCached(
|
||||
`
|
||||
UPDATE moz_places_metadata_snapshots
|
||||
SET removed_at = :removedAt
|
||||
WHERE place_id = (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url)
|
||||
`,
|
||||
{ removedAt: Date.now(), url }
|
||||
);
|
||||
});
|
||||
|
||||
if (snapshot) {
|
||||
snapshot.removedAt = new Date();
|
||||
Services.obs.notifyObservers(null, "places-snapshot-deleted", url);
|
||||
}
|
||||
Services.obs.notifyObservers(null, "places-snapshot-deleted", url);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -116,12 +115,30 @@ const Snapshots = new (class Snapshots {
|
|||
* @returns {?Snapshot}
|
||||
*/
|
||||
async get(url, includeTombstones = false) {
|
||||
let snapshot = this.#snapshots.get(url);
|
||||
if (!snapshot || (snapshot.removedAt && !includeTombstones)) {
|
||||
let db = await PlacesUtils.promiseDBConnection();
|
||||
let extraWhereCondition = "";
|
||||
|
||||
if (!includeTombstones) {
|
||||
extraWhereCondition = " AND removed_at IS NULL";
|
||||
}
|
||||
|
||||
let rows = await db.executeCached(
|
||||
`
|
||||
SELECT h.url AS url, created_at, removed_at, document_type,
|
||||
first_interaction_at, last_interaction_at,
|
||||
user_persisted FROM moz_places_metadata_snapshots s
|
||||
JOIN moz_places h ON h.id = s.place_id
|
||||
WHERE h.url_hash = hash(:url) AND h.url = :url
|
||||
${extraWhereCondition}
|
||||
`,
|
||||
{ url }
|
||||
);
|
||||
|
||||
if (!rows.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
return this.#translateRow(rows[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -136,11 +153,64 @@ const Snapshots = new (class Snapshots {
|
|||
* Returns snapshots in order of descending last interaction time.
|
||||
*/
|
||||
async query({ limit = 100, includeTombstones = false } = {}) {
|
||||
let snapshots = Array.from(this.#snapshots.values());
|
||||
let db = await PlacesUtils.promiseDBConnection();
|
||||
|
||||
let whereStatement = "";
|
||||
|
||||
if (!includeTombstones) {
|
||||
return snapshots.filter(s => !s.removedAt).slice(0, limit);
|
||||
whereStatement = " WHERE removed_at IS NULL";
|
||||
}
|
||||
return snapshots.slice(0, limit);
|
||||
|
||||
let rows = await db.executeCached(
|
||||
`
|
||||
SELECT h.url AS url, created_at, removed_at, document_type,
|
||||
first_interaction_at, last_interaction_at,
|
||||
user_persisted FROM moz_places_metadata_snapshots s
|
||||
JOIN moz_places h ON h.id = s.place_id
|
||||
${whereStatement}
|
||||
ORDER BY last_interaction_at DESC
|
||||
LIMIT :limit
|
||||
`,
|
||||
{ limit }
|
||||
);
|
||||
|
||||
return rows.map(row => this.#translateRow(row));
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a database row to a Snapshot.
|
||||
*
|
||||
* @param {object} row
|
||||
* The database row to translate.
|
||||
* @returns {Snapshot}
|
||||
*/
|
||||
#translateRow(row) {
|
||||
return {
|
||||
url: row.getResultByName("url"),
|
||||
createdAt: this.#toDate(row.getResultByName("created_at")),
|
||||
removedAt: this.#toDate(row.getResultByName("removed_at")),
|
||||
firstInteractionAt: this.#toDate(
|
||||
row.getResultByName("first_interaction_at")
|
||||
),
|
||||
lastInteractionAt: this.#toDate(
|
||||
row.getResultByName("last_interaction_at")
|
||||
),
|
||||
documentType: row.getResultByName("document_type"),
|
||||
userPersisted: !!row.getResultByName("user_persisted"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a date value from the database.
|
||||
*
|
||||
* @param {number} value
|
||||
* The date in milliseconds from the epoch.
|
||||
* @returns {Date?}
|
||||
*/
|
||||
#toDate(value) {
|
||||
if (value) {
|
||||
return new Date(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -7,15 +7,27 @@
|
|||
|
||||
const TEST_URL1 = "https://example.com/";
|
||||
const TEST_URL2 = "https://example.com/12345";
|
||||
const TEST_URL3 = "https://example.com/14235";
|
||||
const TOPIC_ADDED = "places-snapshot-added";
|
||||
const TOPIC_DELETED = "places-snapshot-deleted";
|
||||
|
||||
add_task(async function test_add_simple_snapshot() {
|
||||
add_task(async function setup() {
|
||||
let now = Date.now();
|
||||
await addInteractions([
|
||||
{ url: TEST_URL1, documentType: Interactions.DOCUMENT_TYPE.MEDIA },
|
||||
{ url: TEST_URL2 },
|
||||
// The updated_at values are force to ensure unique times so that we can
|
||||
// retrieve the snapshots in the expected order.
|
||||
{
|
||||
url: TEST_URL1,
|
||||
documentType: Interactions.DOCUMENT_TYPE.MEDIA,
|
||||
created_at: now - 20000,
|
||||
updated_at: now - 20000,
|
||||
},
|
||||
{ url: TEST_URL2, created_at: now - 10000, updated_at: now - 10000 },
|
||||
{ url: TEST_URL3, created_at: now, updated_at: now },
|
||||
]);
|
||||
});
|
||||
|
||||
add_task(async function test_add_simple_snapshot() {
|
||||
let promise = TestUtils.topicObserved(
|
||||
TOPIC_ADDED,
|
||||
(subject, data) => !subject && data == TEST_URL1
|
||||
|
@ -31,8 +43,8 @@ add_task(async function test_add_simple_snapshot() {
|
|||
await promise;
|
||||
|
||||
await assertSnapshots([
|
||||
{ url: TEST_URL1, documentType: Interactions.DOCUMENT_TYPE.MEDIA },
|
||||
{ url: TEST_URL2, userPersisted: true },
|
||||
{ url: TEST_URL1, documentType: Interactions.DOCUMENT_TYPE.MEDIA },
|
||||
]);
|
||||
|
||||
let snapshot = await Snapshots.get(TEST_URL2);
|
||||
|
@ -42,11 +54,50 @@ add_task(async function test_add_simple_snapshot() {
|
|||
add_task(async function test_add_snapshot_without_interaction() {
|
||||
await Assert.rejects(
|
||||
Snapshots.add({ url: "https://invalid.com/" }),
|
||||
/Could not find existing interactions/,
|
||||
/NOT NULL constraint failed/,
|
||||
"Should reject if an interaction is missing"
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_add_duplicate_snapshot() {
|
||||
await Snapshots.add({ url: TEST_URL3 });
|
||||
|
||||
let initialSnapshot = await Snapshots.get(TEST_URL3);
|
||||
|
||||
await Snapshots.add({ url: TEST_URL3 });
|
||||
|
||||
let newSnapshot = await Snapshots.get(TEST_URL3);
|
||||
Assert.deepEqual(
|
||||
initialSnapshot,
|
||||
newSnapshot,
|
||||
"Snapshot should have remained the same"
|
||||
);
|
||||
|
||||
// Check that the other snapshots have not had userPersisted changed.
|
||||
await assertSnapshots([
|
||||
{ url: TEST_URL3 },
|
||||
{ url: TEST_URL2, userPersisted: true },
|
||||
{ url: TEST_URL1, documentType: Interactions.DOCUMENT_TYPE.MEDIA },
|
||||
]);
|
||||
|
||||
info("Re-add existing snapshot to check for userPersisted flag");
|
||||
await Snapshots.add({ url: TEST_URL3, userPersisted: true });
|
||||
|
||||
newSnapshot = await Snapshots.get(TEST_URL3);
|
||||
Assert.deepEqual(
|
||||
{ ...initialSnapshot, userPersisted: true },
|
||||
newSnapshot,
|
||||
"Snapshot should have remained the same apart from the userPersisted value"
|
||||
);
|
||||
|
||||
// Check that the other snapshots have not had userPersisted changed.
|
||||
await assertSnapshots([
|
||||
{ url: TEST_URL3, userPersisted: true },
|
||||
{ url: TEST_URL2, userPersisted: true },
|
||||
{ url: TEST_URL1, documentType: Interactions.DOCUMENT_TYPE.MEDIA },
|
||||
]);
|
||||
});
|
||||
|
||||
add_task(async function test_get_snapshot_not_found() {
|
||||
let snapshot = await Snapshots.get("https://invalid.com/");
|
||||
Assert.ok(
|
||||
|
@ -55,7 +106,7 @@ add_task(async function test_get_snapshot_not_found() {
|
|||
);
|
||||
});
|
||||
|
||||
add_task(async function test_remove_snapshot() {
|
||||
add_task(async function test_delete_snapshot() {
|
||||
let removedAt = new Date();
|
||||
|
||||
let promise = TestUtils.topicObserved(
|
||||
|
@ -74,4 +125,24 @@ add_task(async function test_remove_snapshot() {
|
|||
documentType: Interactions.DOCUMENT_TYPE.MEDIA,
|
||||
removedAt,
|
||||
});
|
||||
|
||||
info("Attempt to re-add non-user persisted snapshot");
|
||||
await Snapshots.add({ url: TEST_URL1 });
|
||||
|
||||
snapshot = await Snapshots.get(TEST_URL1, true);
|
||||
assertSnapshot(snapshot, {
|
||||
url: TEST_URL1,
|
||||
documentType: Interactions.DOCUMENT_TYPE.MEDIA,
|
||||
removedAt,
|
||||
});
|
||||
|
||||
info("Re-add user persisted snapshot");
|
||||
await Snapshots.add({ url: TEST_URL1, userPersisted: true });
|
||||
|
||||
snapshot = await Snapshots.get(TEST_URL1);
|
||||
assertSnapshot(snapshot, {
|
||||
url: TEST_URL1,
|
||||
documentType: Interactions.DOCUMENT_TYPE.MEDIA,
|
||||
userPersisted: true,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,38 +6,52 @@
|
|||
*/
|
||||
|
||||
add_task(async function setup() {
|
||||
// Force 2 seconds between interactions so that we can guarantee the expected
|
||||
// orders.
|
||||
let now = Date.now() - 2000 * 10;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
let url = `https://example.com/${i}/`;
|
||||
await addInteractions([{ url }]);
|
||||
await addInteractions([
|
||||
{ url, created_at: now + i * 2000, updated_at: now + i * 2000 },
|
||||
]);
|
||||
|
||||
await Snapshots.add({ url: `https://example.com/${i}/` });
|
||||
|
||||
// Give a little time between updates to help the ordering.
|
||||
/* eslint-disable-next-line mozilla/no-arbitrary-setTimeout */
|
||||
await new Promise(r => setTimeout(r, 20));
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_query_returns_correct_order() {
|
||||
await assertSnapshots([
|
||||
{ url: "https://example.com/0/" },
|
||||
{ url: "https://example.com/1/" },
|
||||
{ url: "https://example.com/2/" },
|
||||
{ url: "https://example.com/3/" },
|
||||
{ url: "https://example.com/4/" },
|
||||
{ url: "https://example.com/5/" },
|
||||
{ url: "https://example.com/6/" },
|
||||
{ url: "https://example.com/7/" },
|
||||
{ url: "https://example.com/8/" },
|
||||
{ url: "https://example.com/9/" },
|
||||
{ url: "https://example.com/8/" },
|
||||
{ url: "https://example.com/7/" },
|
||||
{ url: "https://example.com/6/" },
|
||||
{ url: "https://example.com/5/" },
|
||||
{ url: "https://example.com/4/" },
|
||||
{ url: "https://example.com/3/" },
|
||||
{ url: "https://example.com/2/" },
|
||||
{ url: "https://example.com/1/" },
|
||||
{ url: "https://example.com/0/" },
|
||||
]);
|
||||
|
||||
// TODO: Update an interaction, and check the new order is returned corectly.
|
||||
await addInteractions([{ url: "https://example.com/0/" }]);
|
||||
|
||||
await assertSnapshots([
|
||||
{ url: "https://example.com/0/" },
|
||||
{ url: "https://example.com/9/" },
|
||||
{ url: "https://example.com/8/" },
|
||||
{ url: "https://example.com/7/" },
|
||||
{ url: "https://example.com/6/" },
|
||||
{ url: "https://example.com/5/" },
|
||||
{ url: "https://example.com/4/" },
|
||||
{ url: "https://example.com/3/" },
|
||||
{ url: "https://example.com/2/" },
|
||||
{ url: "https://example.com/1/" },
|
||||
]);
|
||||
});
|
||||
|
||||
add_task(async function test_query_limit() {
|
||||
await assertSnapshots(
|
||||
[{ url: `https://example.com/0/` }, { url: `https://example.com/1/` }],
|
||||
[{ url: `https://example.com/0/` }, { url: `https://example.com/9/` }],
|
||||
{ limit: 2 }
|
||||
);
|
||||
});
|
||||
|
@ -50,28 +64,28 @@ add_task(async function test_query_handles_tombstones() {
|
|||
info("Deleted snapshots should not appear in the query");
|
||||
await assertSnapshots([
|
||||
{ url: "https://example.com/0/" },
|
||||
{ url: "https://example.com/1/" },
|
||||
{ url: "https://example.com/2/" },
|
||||
{ url: "https://example.com/4/" },
|
||||
{ url: "https://example.com/5/" },
|
||||
{ url: "https://example.com/7/" },
|
||||
{ url: "https://example.com/8/" },
|
||||
{ url: "https://example.com/9/" },
|
||||
{ url: "https://example.com/8/" },
|
||||
{ url: "https://example.com/7/" },
|
||||
{ url: "https://example.com/5/" },
|
||||
{ url: "https://example.com/4/" },
|
||||
{ url: "https://example.com/2/" },
|
||||
{ url: "https://example.com/1/" },
|
||||
]);
|
||||
|
||||
info("Deleted snapshots should appear if tombstones are requested");
|
||||
await assertSnapshots(
|
||||
[
|
||||
{ url: "https://example.com/0/" },
|
||||
{ url: "https://example.com/1/" },
|
||||
{ url: "https://example.com/2/" },
|
||||
{ url: "https://example.com/3/", removedAt },
|
||||
{ url: "https://example.com/4/" },
|
||||
{ url: "https://example.com/5/" },
|
||||
{ url: "https://example.com/6/", removedAt },
|
||||
{ url: "https://example.com/7/" },
|
||||
{ url: "https://example.com/8/" },
|
||||
{ url: "https://example.com/9/" },
|
||||
{ url: "https://example.com/8/" },
|
||||
{ url: "https://example.com/7/" },
|
||||
{ url: "https://example.com/6/", removedAt },
|
||||
{ url: "https://example.com/5/" },
|
||||
{ url: "https://example.com/4/" },
|
||||
{ url: "https://example.com/3/", removedAt },
|
||||
{ url: "https://example.com/2/" },
|
||||
{ url: "https://example.com/1/" },
|
||||
],
|
||||
{ includeTombstones: true }
|
||||
);
|
||||
|
|
Загрузка…
Ссылка в новой задаче