Bug 1847592 - Remove some old Places migrations and move schema version to the history service interface. r=places-reviewers,lina

Since Firefox 72 was a watershed release, thus any upgrade of previous versions
must go through it, we can remove some old migrations.
We stop removing at Firefox 68, that was the last ESR before the watershed.

Also while there move the schema version to the history service so we don't have
to manually keep the migration test header in sync.

Differential Revision: https://phabricator.services.mozilla.com/D185576
This commit is contained in:
Marco Bonardo 2023-08-08 17:12:12 +00:00
parent f4db08de2f
commit 2e1e0e2641
17 changed files with 22 additions and 1613 deletions

View File

@ -7,7 +7,6 @@
#include "mozilla/DebugOnly.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/JSONStringWriteFuncs.h"
#include "mozilla/StaticPrefs_places.h"
#include "Database.h"
@ -71,9 +70,6 @@
#define ENV_ALLOW_CORRUPTION \
"ALLOW_PLACES_DATABASE_TO_LOSE_DATA_AND_BECOME_CORRUPT"
#define PREF_MIGRATE_V52_ORIGIN_FRECENCIES \
"places.database.migrateV52OriginFrecencies"
// Maximum size for the WAL file.
// For performance reasons this should be as large as possible, so that more
// transactions can fit into it, and the checkpoint cost is paid less often.
@ -1076,17 +1072,12 @@ nsresult Database::InitSchema(bool* aDatabaseMigrated) {
NS_ENSURE_SUCCESS(rv, rv);
bool databaseInitialized = currentSchemaVersion > 0;
if (databaseInitialized && currentSchemaVersion == DATABASE_SCHEMA_VERSION) {
if (databaseInitialized &&
currentSchemaVersion == nsINavHistoryService::DATABASE_SCHEMA_VERSION) {
// The database is up to date and ready to go.
return NS_OK;
}
auto guard = MakeScopeExit([&]() {
// These run at the end of the migration, out of the transaction,
// regardless of its success.
MigrateV52OriginFrecencies();
});
// We are going to update the database, so everything from now on should be in
// a transaction for performances.
mozStorageTransaction transaction(mMainConn, false);
@ -1106,64 +1097,15 @@ nsresult Database::InitSchema(bool* aDatabaseMigrated) {
// The only thing we will do for downgrades is setting back the schema
// version, so that next upgrades will run again the migration step.
if (currentSchemaVersion < DATABASE_SCHEMA_VERSION) {
if (currentSchemaVersion < nsINavHistoryService::DATABASE_SCHEMA_VERSION) {
*aDatabaseMigrated = true;
if (currentSchemaVersion < 43) {
// These are versions older than Firefox 60 ESR that are not supported
if (currentSchemaVersion < 52) {
// These are versions older than Firefox 68 ESR that are not supported
// anymore. In this case it's safer to just replace the database.
return NS_ERROR_FILE_CORRUPTED;
}
// Firefox 60 uses schema version 43. - This is an ESR.
if (currentSchemaVersion < 44) {
rv = MigrateV44Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 45) {
rv = MigrateV45Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 46) {
rv = MigrateV46Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 47) {
rv = MigrateV47Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 61 uses schema version 47.
if (currentSchemaVersion < 48) {
rv = MigrateV48Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 49) {
rv = MigrateV49Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 50) {
rv = MigrateV50Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 51) {
rv = MigrateV51Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 52) {
rv = MigrateV52Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 62 uses schema version 52.
// Firefox 68 uses schema version 52. - This is an ESR.
@ -1173,7 +1115,7 @@ nsresult Database::InitSchema(bool* aDatabaseMigrated) {
}
// Firefox 69 uses schema version 53
// Firefox 78 uses schema version 53 - This is an ESR.
// Firefox 72 is a watershed release.
if (currentSchemaVersion < 54) {
rv = MigrateV54Up();
@ -1409,7 +1351,8 @@ nsresult Database::InitSchema(bool* aDatabaseMigrated) {
}
// Set the schema version to the current one.
rv = mMainConn->SetSchemaVersion(DATABASE_SCHEMA_VERSION);
rv = mMainConn->SetSchemaVersion(
nsINavHistoryService::DATABASE_SCHEMA_VERSION);
NS_ENSURE_SUCCESS(rv, rv);
rv = transaction.Commit();
@ -1742,518 +1685,6 @@ nsresult Database::InitTempEntities() {
return NS_OK;
}
nsresult Database::MigrateV44Up() {
// We need to remove any non-builtin roots and their descendants.
// Install a temp trigger to clean up linked tables when the main
// bookmarks are deleted.
nsresult rv = mMainConn->ExecuteSimpleSQL(nsLiteralCString(
"CREATE TEMP TRIGGER moz_migrate_bookmarks_trigger "
"AFTER DELETE ON moz_bookmarks FOR EACH ROW "
"BEGIN "
// Insert tombstones.
"INSERT OR IGNORE INTO moz_bookmarks_deleted (guid, dateRemoved) "
"VALUES (OLD.guid, strftime('%s', 'now', 'localtime', 'utc') * 1000000); "
// Remove old annotations for the bookmarks.
"DELETE FROM moz_items_annos "
"WHERE item_id = OLD.id; "
// Decrease the foreign_count in moz_places.
"UPDATE moz_places "
"SET foreign_count = foreign_count - 1 "
"WHERE id = OLD.fk; "
"END "));
if (NS_FAILED(rv)) return rv;
// This trigger listens for moz_places deletes, and updates moz_annos and
// moz_keywords accordingly.
rv = mMainConn->ExecuteSimpleSQL(nsLiteralCString(
"CREATE TEMP TRIGGER moz_migrate_annos_trigger "
"AFTER UPDATE ON moz_places FOR EACH ROW "
// Only remove from moz_places if we don't have any remaining keywords
// pointing to this place, and it hasn't been visited. Note: orphan
// keywords are tidied up below.
"WHEN NEW.visit_count = 0 AND "
" NEW.foreign_count = (SELECT COUNT(*) FROM moz_keywords WHERE place_id "
"= NEW.id) "
"BEGIN "
// No more references to the place, so we can delete the place itself.
"DELETE FROM moz_places "
"WHERE id = NEW.id; "
// Delete annotations relating to the place.
"DELETE FROM moz_annos "
"WHERE place_id = NEW.id; "
// Delete keywords relating to the place.
"DELETE FROM moz_keywords "
"WHERE place_id = NEW.id; "
"END "));
if (NS_FAILED(rv)) return rv;
// Listens to moz_keyword deletions, to ensure moz_places gets the
// foreign_count updated corrrectly.
rv = mMainConn->ExecuteSimpleSQL(
nsLiteralCString("CREATE TEMP TRIGGER moz_migrate_keyword_trigger "
"AFTER DELETE ON moz_keywords FOR EACH ROW "
"BEGIN "
// If we remove a keyword, then reduce the foreign_count.
"UPDATE moz_places "
"SET foreign_count = foreign_count - 1 "
"WHERE id = OLD.place_id; "
"END "));
if (NS_FAILED(rv)) return rv;
// First of all, find the non-builtin roots.
nsCOMPtr<mozIStorageStatement> deleteStmt;
rv = mMainConn->CreateStatement(
nsLiteralCString("WITH RECURSIVE "
"itemsToRemove(id, guid) AS ( "
"SELECT b.id, b.guid FROM moz_bookmarks b "
"JOIN moz_bookmarks p ON b.parent = p.id "
"WHERE p.guid = 'root________' AND "
"b.guid NOT IN ('menu________', 'toolbar_____', "
"'tags________', 'unfiled_____', 'mobile______') "
"UNION ALL "
"SELECT b.id, b.guid FROM moz_bookmarks b "
"JOIN itemsToRemove d ON d.id = b.parent "
"WHERE b.guid NOT IN ('menu________', 'toolbar_____', "
"'tags________', 'unfiled_____', 'mobile______') "
") "
"DELETE FROM moz_bookmarks "
"WHERE id IN (SELECT id FROM itemsToRemove) "),
getter_AddRefs(deleteStmt));
if (NS_FAILED(rv)) return rv;
rv = deleteStmt->Execute();
if (NS_FAILED(rv)) return rv;
// Before we remove the triggers, check for keywords attached to places which
// no longer have a bookmark to them. We do this before removing the triggers,
// so that we can make use of the keyword trigger to update the counts in
// moz_places.
rv = mMainConn->ExecuteSimpleSQL(
nsLiteralCString("DELETE FROM moz_keywords WHERE place_id IN ( "
"SELECT h.id FROM moz_keywords k "
"JOIN moz_places h ON h.id = k.place_id "
"GROUP BY place_id HAVING h.foreign_count = count(*) "
")"));
if (NS_FAILED(rv)) return rv;
// Now remove the temp triggers.
rv = mMainConn->ExecuteSimpleSQL(
"DROP TRIGGER moz_migrate_bookmarks_trigger "_ns);
if (NS_FAILED(rv)) return rv;
rv =
mMainConn->ExecuteSimpleSQL("DROP TRIGGER moz_migrate_annos_trigger "_ns);
if (NS_FAILED(rv)) return rv;
rv = mMainConn->ExecuteSimpleSQL(
"DROP TRIGGER moz_migrate_keyword_trigger "_ns);
if (NS_FAILED(rv)) return rv;
// Cleanup any orphan annotation attributes.
rv = mMainConn->ExecuteSimpleSQL(
nsLiteralCString("DELETE FROM moz_anno_attributes WHERE id IN ( "
"SELECT id FROM moz_anno_attributes n "
"EXCEPT "
"SELECT DISTINCT anno_attribute_id FROM moz_annos "
"EXCEPT "
"SELECT DISTINCT anno_attribute_id FROM moz_items_annos "
")"));
if (NS_FAILED(rv)) return rv;
return NS_OK;
}
nsresult Database::MigrateV45Up() {
nsCOMPtr<mozIStorageStatement> metaTableStmt;
nsresult rv = mMainConn->CreateStatement("SELECT 1 FROM moz_meta"_ns,
getter_AddRefs(metaTableStmt));
if (NS_FAILED(rv)) {
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_META);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
nsresult Database::MigrateV46Up() {
// Convert the existing queries. For simplicity we assume the user didn't
// edit these queries, and just do a 1:1 conversion.
nsresult rv = mMainConn->ExecuteSimpleSQL(nsLiteralCString(
"UPDATE moz_places "
"SET url = IFNULL('place:tag=' || ( "
"SELECT title FROM moz_bookmarks "
"WHERE id = CAST(get_query_param(substr(url, 7), 'folder') AS INT) "
"), url) "
"WHERE url_hash BETWEEN hash('place', 'prefix_lo') AND "
"hash('place', 'prefix_hi') "
"AND url LIKE '%type=7%' "
"AND EXISTS(SELECT 1 FROM moz_bookmarks "
"WHERE id = CAST(get_query_param(substr(url, 7), 'folder') AS INT)) "));
// Recalculate hashes for all tag queries.
rv = mMainConn->ExecuteSimpleSQL(
nsLiteralCString("UPDATE moz_places SET url_hash = hash(url) "
"WHERE url_hash BETWEEN hash('place', 'prefix_lo') AND "
"hash('place', 'prefix_hi') "
"AND url LIKE '%tag=%' "));
NS_ENSURE_SUCCESS(rv, rv);
// Update Sync fields for all tag queries.
rv = mMainConn->ExecuteSimpleSQL(nsLiteralCString(
"UPDATE moz_bookmarks SET syncChangeCounter = syncChangeCounter + 1 "
"WHERE fk IN ( "
"SELECT id FROM moz_places "
"WHERE url_hash BETWEEN hash('place', 'prefix_lo') AND "
"hash('place', 'prefix_hi') "
"AND url LIKE '%tag=%' "
") "));
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult Database::MigrateV47Up() {
// v46 may have mistakenly set some url to NULL, we must fix those.
// Since the original url was an invalid query, we replace NULLs with an
// empty query.
nsresult rv = mMainConn->ExecuteSimpleSQL(
nsLiteralCString("UPDATE moz_places "
"SET url = 'place:excludeItems=1', url_hash = "
"hash('place:excludeItems=1') "
"WHERE url ISNULL "));
NS_ENSURE_SUCCESS(rv, rv);
// Update Sync fields for these queries.
rv = mMainConn->ExecuteSimpleSQL(nsLiteralCString(
"UPDATE moz_bookmarks SET syncChangeCounter = syncChangeCounter + 1 "
"WHERE fk IN ( "
"SELECT id FROM moz_places "
"WHERE url_hash = hash('place:excludeItems=1') "
"AND url = 'place:excludeItems=1' "
") "));
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult Database::MigrateV48Up() {
// Create and populate moz_origins.
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = mMainConn->CreateStatement("SELECT * FROM moz_origins; "_ns,
getter_AddRefs(stmt));
if (NS_FAILED(rv)) {
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_ORIGINS);
NS_ENSURE_SUCCESS(rv, rv);
}
rv = mMainConn->ExecuteSimpleSQL(nsLiteralCString(
"INSERT OR IGNORE INTO moz_origins (prefix, host, frecency) "
"SELECT get_prefix(url), get_host_and_port(url), -1 "
"FROM moz_places; "));
NS_ENSURE_SUCCESS(rv, rv);
// Add and populate moz_places.origin_id.
rv = mMainConn->CreateStatement("SELECT origin_id FROM moz_places; "_ns,
getter_AddRefs(stmt));
if (NS_FAILED(rv)) {
rv = mMainConn->ExecuteSimpleSQL(nsLiteralCString(
"ALTER TABLE moz_places "
"ADD COLUMN origin_id INTEGER REFERENCES moz_origins(id); "));
NS_ENSURE_SUCCESS(rv, rv);
}
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_ORIGIN_ID);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(nsLiteralCString(
"UPDATE moz_places "
"SET origin_id = ( "
"SELECT id FROM moz_origins "
"WHERE prefix = get_prefix(url) AND host = get_host_and_port(url) "
"); "));
NS_ENSURE_SUCCESS(rv, rv);
// From this point on, nobody should use moz_hosts again. Empty it so that we
// don't leak the user's history, but don't remove it yet so that the user can
// downgrade.
// This can fail, if moz_hosts doesn't exist anymore, that is what happens in
// case of downgrade+upgrade.
Unused << mMainConn->ExecuteSimpleSQL("DELETE FROM moz_hosts; "_ns);
return NS_OK;
}
nsresult Database::MigrateV49Up() {
// These hidden preferences were added along with the v48 migration as part of
// the frecency stats implementation but are now replaced with entries in the
// moz_meta table.
Unused << Preferences::ClearUser("places.frecency.stats.count");
Unused << Preferences::ClearUser("places.frecency.stats.sum");
Unused << Preferences::ClearUser("places.frecency.stats.sumOfSquares");
return NS_OK;
}
nsresult Database::MigrateV50Up() {
// Convert the existing queries. We don't have REGEX available, so the
// simplest thing to do is to pull the urls out, and process them manually.
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = mMainConn->CreateStatement(
nsLiteralCString("SELECT id, url FROM moz_places "
"WHERE url_hash BETWEEN hash('place', 'prefix_lo') AND "
"hash('place', 'prefix_hi') "
"AND url LIKE '%folder=%' "),
getter_AddRefs(stmt));
if (NS_FAILED(rv)) return rv;
AutoTArray<std::pair<int64_t, nsCString>, 32> placeURLs;
bool hasMore = false;
nsCString url;
while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
int64_t placeId;
rv = stmt->GetInt64(0, &placeId);
if (NS_FAILED(rv)) return rv;
rv = stmt->GetUTF8String(1, url);
if (NS_FAILED(rv)) return rv;
// XXX(Bug 1631371) Check if this should use a fallible operation as it
// pretended earlier.
placeURLs.AppendElement(std::make_pair(placeId, url));
}
if (placeURLs.IsEmpty()) {
return NS_OK;
}
int64_t placeId;
for (uint32_t i = 0; i < placeURLs.Length(); ++i) {
placeId = placeURLs[i].first;
url = placeURLs[i].second;
rv = ConvertOldStyleQuery(url);
// Something bad happened, and we can't convert it, so just continue.
if (NS_WARN_IF(NS_FAILED(rv))) {
continue;
}
nsCOMPtr<mozIStorageStatement> updateStmt;
rv = mMainConn->CreateStatement(
nsLiteralCString("UPDATE moz_places "
"SET url = :url, url_hash = hash(:url) "
"WHERE id = :placeId "),
getter_AddRefs(updateStmt));
if (NS_FAILED(rv)) return rv;
rv = URIBinder::Bind(updateStmt, "url"_ns, url);
if (NS_FAILED(rv)) return rv;
rv = updateStmt->BindInt64ByName("placeId"_ns, placeId);
if (NS_FAILED(rv)) return rv;
rv = updateStmt->Execute();
if (NS_FAILED(rv)) return rv;
// Update Sync fields for these queries.
nsCOMPtr<mozIStorageStatement> syncStmt;
rv = mMainConn->CreateStatement(
nsLiteralCString("UPDATE moz_bookmarks SET syncChangeCounter = "
"syncChangeCounter + 1 "
"WHERE fk = :placeId "),
getter_AddRefs(syncStmt));
if (NS_FAILED(rv)) return rv;
rv = syncStmt->BindInt64ByName("placeId"_ns, placeId);
if (NS_FAILED(rv)) return rv;
rv = syncStmt->Execute();
if (NS_FAILED(rv)) return rv;
}
return NS_OK;
}
nsresult Database::MigrateV51Up() {
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = mMainConn->CreateStatement(
nsLiteralCString("SELECT b.guid FROM moz_anno_attributes n "
"JOIN moz_items_annos a ON n.id = a.anno_attribute_id "
"JOIN moz_bookmarks b ON a.item_id = b.id "
"WHERE n.name = :anno_name ORDER BY a.content DESC"),
getter_AddRefs(stmt));
if (NS_FAILED(rv)) {
MOZ_ASSERT(false,
"Should succeed unless item annotations table has been removed");
return NS_OK;
};
rv = stmt->BindUTF8StringByName("anno_name"_ns, LAST_USED_ANNO);
NS_ENSURE_SUCCESS(rv, rv);
JSONStringWriteFunc<nsAutoCString> json;
JSONWriter jw{json};
jw.StartArrayProperty(nullptr, JSONWriter::SingleLineStyle);
bool hasAtLeastOne = false;
bool hasMore = false;
uint32_t length;
while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
hasAtLeastOne = true;
const char* stmtString = stmt->AsSharedUTF8String(0, &length);
jw.StringElement(Span<const char>(stmtString, length));
}
jw.EndArray();
// If we don't have any, just abort early and save the extra work.
if (!hasAtLeastOne) {
return NS_OK;
}
rv = mMainConn->CreateStatement(
nsLiteralCString("INSERT OR REPLACE INTO moz_meta "
"VALUES (:key, :value) "),
getter_AddRefs(stmt));
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindUTF8StringByName("key"_ns, LAST_USED_FOLDERS_META_KEY);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindUTF8StringByName("value"_ns, json.StringCRef());
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
// Clean up the now redundant annotations.
rv = mMainConn->CreateStatement(
nsLiteralCString(
"DELETE FROM moz_items_annos WHERE anno_attribute_id = "
"(SELECT id FROM moz_anno_attributes WHERE name = :anno_name) "),
getter_AddRefs(stmt));
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindUTF8StringByName("anno_name"_ns, LAST_USED_ANNO);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->CreateStatement(
nsLiteralCString(
"DELETE FROM moz_anno_attributes WHERE name = :anno_name "),
getter_AddRefs(stmt));
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindUTF8StringByName("anno_name"_ns, LAST_USED_ANNO);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
namespace {
class MigrateV52OriginFrecenciesRunnable final : public Runnable {
public:
NS_DECL_NSIRUNNABLE
explicit MigrateV52OriginFrecenciesRunnable(mozIStorageConnection* aDBConn);
private:
nsCOMPtr<mozIStorageConnection> mDBConn;
};
MigrateV52OriginFrecenciesRunnable::MigrateV52OriginFrecenciesRunnable(
mozIStorageConnection* aDBConn)
: Runnable("places::MigrateV52OriginFrecenciesRunnable"),
mDBConn(aDBConn) {}
NS_IMETHODIMP
MigrateV52OriginFrecenciesRunnable::Run() {
if (NS_IsMainThread()) {
// Migration done. Clear the pref.
Unused << Preferences::ClearUser(PREF_MIGRATE_V52_ORIGIN_FRECENCIES);
// Now that frecencies have been migrated, recalculate the origin frecency
// stats.
nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
NS_ENSURE_STATE(navHistory);
nsresult rv = navHistory->RecalculateOriginFrecencyStats(nullptr);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
// We do the work in chunks, or the wal journal may grow too much.
nsresult rv = mDBConn->ExecuteSimpleSQL(nsLiteralCString(
"UPDATE moz_origins "
"SET frecency = ( "
"SELECT CAST(TOTAL(frecency) AS INTEGER) "
"FROM moz_places "
"WHERE frecency > 0 AND moz_places.origin_id = moz_origins.id "
") "
"WHERE id IN ( "
"SELECT id "
"FROM moz_origins "
"WHERE frecency < 0 "
"LIMIT 400 "
") "));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<mozIStorageStatement> selectStmt;
rv = mDBConn->CreateStatement(nsLiteralCString("SELECT 1 "
"FROM moz_origins "
"WHERE frecency < 0 "
"LIMIT 1 "),
getter_AddRefs(selectStmt));
NS_ENSURE_SUCCESS(rv, rv);
bool hasResult = false;
rv = selectStmt->ExecuteStep(&hasResult);
NS_ENSURE_SUCCESS(rv, rv);
if (hasResult) {
// There are more results to handle. Re-dispatch to the same thread for the
// next chunk.
return NS_DispatchToCurrentThread(this);
}
// Re-dispatch to the main-thread to flip the migration pref.
return NS_DispatchToMainThread(this);
}
} // namespace
void Database::MigrateV52OriginFrecencies() {
MOZ_ASSERT(NS_IsMainThread());
if (!Preferences::GetBool(PREF_MIGRATE_V52_ORIGIN_FRECENCIES)) {
// The migration has already been completed.
return;
}
RefPtr<MigrateV52OriginFrecenciesRunnable> runnable(
new MigrateV52OriginFrecenciesRunnable(mMainConn));
nsCOMPtr<nsIEventTarget> target(do_GetInterface(mMainConn));
MOZ_ASSERT(target);
if (target) {
Unused << target->Dispatch(runnable, NS_DISPATCH_NORMAL);
}
}
nsresult Database::MigrateV52Up() {
// Before this migration, moz_origin.frecency is the max frecency of all
// places with the origin. After this migration, it's the sum of frecencies
// of all places with the origin.
//
// Setting this pref will cause InitSchema to begin async migration, via
// MigrateV52OriginFrecencies. When that migration is done, origin frecency
// stats are recalculated (see MigrateV52OriginFrecenciesRunnable::Run).
Unused << Preferences::SetBool(PREF_MIGRATE_V52_ORIGIN_FRECENCIES, true);
// Set all origin frecencies to -1 so that MigrateV52OriginFrecenciesRunnable
// will migrate them.
nsresult rv =
mMainConn->ExecuteSimpleSQL("UPDATE moz_origins SET frecency = -1 "_ns);
NS_ENSURE_SUCCESS(rv, rv);
// This migration also renames these moz_meta keys that keep track of frecency
// stats. (That happens when stats are recalculated.) Delete the old ones.
rv =
mMainConn->ExecuteSimpleSQL(nsLiteralCString("DELETE FROM moz_meta "
"WHERE key IN ( "
"'frecency_count', "
"'frecency_sum', "
"'frecency_sum_of_squares' "
") "));
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult Database::MigrateV53Up() {
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = mMainConn->CreateStatement("SELECT 1 FROM moz_items_annos"_ns,
@ -2599,81 +2030,6 @@ nsresult Database::RecalculateOriginFrecencyStatsInternal() {
") "));
}
nsresult Database::ConvertOldStyleQuery(nsCString& aURL) {
AutoTArray<QueryKeyValuePair, 8> tokens;
nsresult rv = TokenizeQueryString(aURL, &tokens);
NS_ENSURE_SUCCESS(rv, rv);
AutoTArray<QueryKeyValuePair, 8> newTokens;
bool invalid = false;
nsAutoCString guid;
for (uint32_t j = 0; j < tokens.Length(); ++j) {
const QueryKeyValuePair& kvp = tokens[j];
if (!kvp.key.EqualsLiteral("folder")) {
// XXX(Bug 1631371) Check if this should use a fallible operation as it
// pretended earlier.
newTokens.AppendElement(kvp);
continue;
}
int64_t itemId = kvp.value.ToInteger(&rv);
if (NS_SUCCEEDED(rv)) {
// We have the folder's ID, now to find its GUID.
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = mMainConn->CreateStatement(
nsLiteralCString("SELECT guid FROM moz_bookmarks "
"WHERE id = :itemId "),
getter_AddRefs(stmt));
if (NS_FAILED(rv)) return rv;
rv = stmt->BindInt64ByName("itemId"_ns, itemId);
if (NS_FAILED(rv)) return rv;
bool hasMore = false;
if (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
rv = stmt->GetUTF8String(0, guid);
if (NS_FAILED(rv)) return rv;
}
} else if (kvp.value.EqualsLiteral("PLACES_ROOT")) {
guid = nsLiteralCString(ROOT_GUID);
} else if (kvp.value.EqualsLiteral("BOOKMARKS_MENU")) {
guid = nsLiteralCString(MENU_ROOT_GUID);
} else if (kvp.value.EqualsLiteral("TAGS")) {
guid = nsLiteralCString(TAGS_ROOT_GUID);
} else if (kvp.value.EqualsLiteral("UNFILED_BOOKMARKS")) {
guid = nsLiteralCString(UNFILED_ROOT_GUID);
} else if (kvp.value.EqualsLiteral("TOOLBAR")) {
guid = nsLiteralCString(TOOLBAR_ROOT_GUID);
} else if (kvp.value.EqualsLiteral("MOBILE_BOOKMARKS")) {
guid = nsLiteralCString(MOBILE_ROOT_GUID);
}
QueryKeyValuePair* newPair;
if (guid.IsEmpty()) {
// This is invalid, so we'll change this key/value pair to something else
// so that the query remains a valid url.
newPair = new QueryKeyValuePair("invalidOldParentId"_ns, kvp.value);
invalid = true;
} else {
newPair = new QueryKeyValuePair("parent"_ns, guid);
}
// XXX(Bug 1631371) Check if this should use a fallible operation as it
// pretended earlier.
newTokens.AppendElement(*newPair);
delete newPair;
}
if (invalid) {
// One or more of the folders don't exist, replace with an empty query.
newTokens.AppendElement(QueryKeyValuePair("excludeItems"_ns, "1"_ns));
}
TokensToQueryString(newTokens, aURL);
return NS_OK;
}
int64_t Database::CreateMobileRoot() {
MOZ_ASSERT(NS_IsMainThread());

View File

@ -16,10 +16,6 @@
#include "Shutdown.h"
#include "nsCategoryCache.h"
// This is the schema version. Update it at any schema change and add a
// corresponding migrateVxx method below.
#define DATABASE_SCHEMA_VERSION 75
// Fired after Places inited.
#define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
// This topic is received when the profile is about to be lost. Places does
@ -295,16 +291,9 @@ class Database final : public nsIObserver, public nsSupportsWeakReference {
/**
* Helpers used by schema upgrades.
* When adding a new function remember to bump up the schema version in
* nsINavHistoryService.
*/
nsresult MigrateV44Up();
nsresult MigrateV45Up();
nsresult MigrateV46Up();
nsresult MigrateV47Up();
nsresult MigrateV48Up();
nsresult MigrateV49Up();
nsresult MigrateV50Up();
nsresult MigrateV51Up();
nsresult MigrateV52Up();
nsresult MigrateV53Up();
nsresult MigrateV54Up();
nsresult MigrateV55Up();
@ -321,14 +310,11 @@ class Database final : public nsIObserver, public nsSupportsWeakReference {
nsresult MigrateV74Up();
nsresult MigrateV75Up();
void MigrateV52OriginFrecencies();
nsresult UpdateBookmarkRootTitles();
friend class ConnectionShutdownBlocker;
int64_t CreateMobileRoot();
nsresult ConvertOldStyleQuery(nsCString& aURL);
private:
~Database();

View File

@ -925,6 +925,11 @@ interface nsINavHistoryQueryOptions : nsISupports
[scriptable, uuid(20c974ff-ee16-4828-9326-1b7c9e036622)]
interface nsINavHistoryService : nsISupports
{
// The current database schema version.
// To migrate to a new version bump this, add a MigrateVXXUp function to
// Database.cpp/h, and a test into tests/migration/
const unsigned long DATABASE_SCHEMA_VERSION = 75;
/**
* System Notifications:
*

View File

@ -4,7 +4,7 @@
// Tests that history initialization correctly handles a corrupt places schema.
add_task(async function () {
let path = await setupPlacesDatabase(["migration", "places_v43.sqlite"]);
let path = await setupPlacesDatabase(["migration", "places_v52.sqlite"]);
// Ensure the database will go through a migration that depends on moz_places
// and break the schema by dropping that table.

View File

@ -6,7 +6,7 @@
add_task(async function () {
await test_database_replacement(
["migration", "places_v43.sqlite"],
["migration", "places_v52.sqlite"],
"places.sqlite",
false,
PlacesUtils.history.DATABASE_STATUS_CORRUPT

View File

@ -6,7 +6,7 @@
add_task(async function () {
await test_database_replacement(
["migration", "places_v43.sqlite"],
["migration", "places_v52.sqlite"],
"places.sqlite",
true,
PlacesUtils.history.DATABASE_STATUS_UPGRADED

View File

@ -13,8 +13,8 @@
// Put any other stuff relative to this test folder below.
const CURRENT_SCHEMA_VERSION = 75;
const FIRST_UPGRADABLE_SCHEMA_VERSION = 43;
const CURRENT_SCHEMA_VERSION = Ci.nsINavHistoryService.DATABASE_SCHEMA_VERSION;
const FIRST_UPGRADABLE_SCHEMA_VERSION = 52;
async function assertAnnotationsRemoved(db, expectedAnnos) {
for (let anno of expectedAnnos) {

View File

@ -1,253 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const EXPECTED_REMAINING_ROOTS = [
...PlacesUtils.bookmarks.userContentRoots,
PlacesUtils.bookmarks.tagsGuid,
];
const EXPECTED_REMOVED_BOOKMARK_GUIDS = [
// These first ones are the old left-pane folder queries
"SNLmwJH6GtW9", // Root Query
"r0dY_2_y4mlx", // History
"xGGhZK3b6GnW", // Downloads
"EJG6I1nKkQFQ", // Tags
"gSyHo5oNSUJV", // All Bookmarks
// These are simulated add-on injections that we expect to be removed.
"exaddon_____",
"exaddon1____",
"exaddon2____",
"exaddon3____",
"test________",
];
const EXPECTED_REMOVED_ANNOTATIONS = [
"PlacesOrganizer/OrganizerFolder",
"PlacesOrganizer/OrganizerQuery",
];
const EXPECTED_REMOVED_PLACES_ENTRIES = ["exaddonh____", "exaddonh3___"];
const EXPECTED_KEPT_PLACES_ENTRY = "exaddonh2___";
const EXPECTED_REMOVED_KEYWORDS = ["exaddon", "exaddon2"];
async function assertItemIn(db, table, field, expectedItems) {
let rows = await db.execute(`SELECT ${field} from ${table}`);
Assert.ok(
rows.length >= expectedItems.length,
"Should be at least the number of annotations we expect to be removed."
);
let fieldValues = rows.map(row => row.getResultByName(field));
for (let item of expectedItems) {
Assert.ok(
fieldValues.includes(item),
`${table} should have ${expectedItems}`
);
}
}
add_task(async function setup() {
await setupPlacesDatabase("places_v43.sqlite");
// Setup database contents to be migrated.
let path = PathUtils.join(PathUtils.profileDir, DB_FILENAME);
let db = await Sqlite.openConnection({ path });
let rows = await db.execute(`SELECT * FROM moz_bookmarks_deleted`);
Assert.equal(rows.length, 0, "Should be nothing in moz_bookmarks_deleted");
// Break roots parenting, to test for Bug 1472127.
await db.execute(`INSERT INTO moz_bookmarks (title, parent, position, guid)
VALUES ("test", 1, 0, "test________")`);
await db.execute(`UPDATE moz_bookmarks
SET parent = (SELECT id FROM moz_bookmarks WHERE guid = "test________")
WHERE guid = "menu________"`);
await assertItemIn(
db,
"moz_anno_attributes",
"name",
EXPECTED_REMOVED_ANNOTATIONS
);
await assertItemIn(
db,
"moz_bookmarks",
"guid",
EXPECTED_REMOVED_BOOKMARK_GUIDS
);
await assertItemIn(db, "moz_keywords", "keyword", EXPECTED_REMOVED_KEYWORDS);
await assertItemIn(db, "moz_places", "guid", EXPECTED_REMOVED_PLACES_ENTRIES);
await db.close();
});
add_task(async function database_is_valid() {
// Accessing the database for the first time triggers migration.
Assert.equal(
PlacesUtils.history.databaseStatus,
PlacesUtils.history.DATABASE_STATUS_UPGRADED
);
let db = await PlacesUtils.promiseDBConnection();
Assert.equal(await db.getSchemaVersion(), CURRENT_SCHEMA_VERSION);
});
add_task(async function test_roots_removed() {
let db = await PlacesUtils.promiseDBConnection();
let rows = await db.execute(
`
SELECT id FROM moz_bookmarks
WHERE guid = :guid
`,
{ guid: PlacesUtils.bookmarks.rootGuid }
);
Assert.equal(rows.length, 1, "Should have exactly one root row.");
let rootId = rows[0].getResultByName("id");
rows = await db.execute(
`
SELECT guid FROM moz_bookmarks
WHERE parent = :rootId`,
{ rootId }
);
Assert.equal(
rows.length,
EXPECTED_REMAINING_ROOTS.length,
"Should only have the built-in folder roots."
);
for (let row of rows) {
let guid = row.getResultByName("guid");
Assert.ok(
EXPECTED_REMAINING_ROOTS.includes(guid),
`Should have only the expected guids remaining, unexpected guid: ${guid}`
);
}
// Check the reparented menu now.
rows = await db.execute(
`
SELECT id, parent FROM moz_bookmarks
WHERE guid = :guid
`,
{ guid: PlacesUtils.bookmarks.menuGuid }
);
Assert.equal(rows.length, 1, "Should have found the menu root.");
Assert.equal(
rows[0].getResultByName("parent"),
await PlacesTestUtils.promiseItemId(PlacesUtils.bookmarks.rootGuid),
"Should have moved the menu back to the Places root."
);
});
add_task(async function test_tombstones_added() {
let db = await PlacesUtils.promiseDBConnection();
let rows = await db.execute(`
SELECT guid FROM moz_bookmarks_deleted
`);
for (let row of rows) {
let guid = row.getResultByName("guid");
Assert.ok(
EXPECTED_REMOVED_BOOKMARK_GUIDS.includes(guid),
`Should have tombstoned the expected guids, unexpected guid: ${guid}`
);
}
Assert.equal(
rows.length,
EXPECTED_REMOVED_BOOKMARK_GUIDS.length,
"Should have removed all the expected bookmarks."
);
});
add_task(async function test_annotations_removed() {
let db = await PlacesUtils.promiseDBConnection();
await assertAnnotationsRemoved(db, EXPECTED_REMOVED_ANNOTATIONS);
});
add_task(async function test_check_history_entries() {
let db = await PlacesUtils.promiseDBConnection();
for (let entry of EXPECTED_REMOVED_PLACES_ENTRIES) {
let rows = await db.execute(`
SELECT id FROM moz_places
WHERE guid = '${entry}'`);
Assert.equal(
rows.length,
0,
`Should have removed an orphaned history entry ${EXPECTED_REMOVED_PLACES_ENTRIES}.`
);
}
let rows = await db.execute(
`
SELECT foreign_count FROM moz_places
WHERE guid = :guid
`,
{ guid: EXPECTED_KEPT_PLACES_ENTRY }
);
Assert.equal(
rows.length,
1,
`Should have kept visited history entry ${EXPECTED_KEPT_PLACES_ENTRY}`
);
let foreignCount = rows[0].getResultByName("foreign_count");
Assert.equal(
foreignCount,
0,
`Should have updated the foreign_count for ${EXPECTED_KEPT_PLACES_ENTRY}`
);
});
add_task(async function test_check_keyword_removed() {
let db = await PlacesUtils.promiseDBConnection();
for (let keyword of EXPECTED_REMOVED_KEYWORDS) {
let rows = await db.execute(
`
SELECT keyword FROM moz_keywords
WHERE keyword = :keyword
`,
{ keyword }
);
Assert.equal(
rows.length,
0,
`Should have removed the expected keyword: ${keyword}.`
);
}
});
add_task(async function test_no_orphan_annotations() {
let db = await PlacesUtils.promiseDBConnection();
await assertNoOrphanAnnotations(db);
});
add_task(async function test_no_orphan_keywords() {
let db = await PlacesUtils.promiseDBConnection();
let rows = await db.execute(`
SELECT place_id FROM moz_keywords
WHERE place_id NOT IN (SELECT id from moz_places)
`);
Assert.equal(rows.length, 0, `Should have no orphan keywords.`);
});
add_task(async function test_meta_exists() {
let db = await PlacesUtils.promiseDBConnection();
await db.execute(`SELECT 1 FROM moz_meta`);
});

View File

@ -1,100 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let gTags = [
{
folder: 123456,
url: "place:folder=123456&type=7&queryType=1",
title: "tag1",
hash: "268505532566465",
},
{
folder: 234567,
url: "place:folder=234567&type=7&queryType=1&somethingelse",
title: "tag2",
hash: "268506675127932",
},
{
folder: 345678,
url: "place:type=7&folder=345678&queryType=1",
title: "tag3",
hash: "268506471927988",
},
// This will point to an invalid folder id.
{
folder: 456789,
url: "place:type=7&folder=456789&queryType=1",
expectedUrl:
"place:type=7&invalidOldParentId=456789&queryType=1&excludeItems=1",
title: "invalid",
hash: "268505972797836",
},
];
gTags.forEach(t => (t.guid = t.title.padEnd(12, "_")));
add_task(async function setup() {
await setupPlacesDatabase("places_v43.sqlite");
// Setup database contents to be migrated.
let path = PathUtils.join(PathUtils.profileDir, DB_FILENAME);
let db = await Sqlite.openConnection({ path });
for (let tag of gTags) {
// We can reuse the same guid, it doesn't matter for this test.
await db.execute(
`INSERT INTO moz_places (url, guid, url_hash)
VALUES (:url, :guid, :hash)
`,
{ url: tag.url, guid: tag.guid, hash: tag.hash }
);
if (tag.title != "invalid") {
await db.execute(
`INSERT INTO moz_bookmarks (id, fk, guid, title)
VALUES (:id, (SELECT id FROM moz_places WHERE guid = :guid), :guid, :title)
`,
{ id: tag.folder, guid: tag.guid, title: tag.title }
);
}
}
await db.close();
});
add_task(async function database_is_valid() {
// Accessing the database for the first time triggers migration.
Assert.equal(
PlacesUtils.history.databaseStatus,
PlacesUtils.history.DATABASE_STATUS_UPGRADED
);
let db = await PlacesUtils.promiseDBConnection();
Assert.equal(await db.getSchemaVersion(), CURRENT_SCHEMA_VERSION);
});
add_task(async function test_queries_converted() {
for (let tag of gTags) {
let url =
tag.title == "invalid" ? tag.expectedUrl : "place:tag=" + tag.title;
let page = await PlacesUtils.history.fetch(tag.guid);
Assert.equal(page.url.href, url);
}
});
add_task(async function test_sync_fields() {
let db = await PlacesUtils.promiseDBConnection();
for (let tag of gTags) {
if (tag.title != "invalid") {
let rows = await db.execute(
`
SELECT syncChangeCounter
FROM moz_bookmarks
WHERE guid = :guid
`,
{ guid: tag.guid }
);
Assert.equal(rows[0].getResultByIndex(0), 2);
}
}
});

View File

@ -1,52 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let guid = "null".padEnd(12, "_");
add_task(async function setup() {
await setupPlacesDatabase("places_v43.sqlite");
// Setup database contents to be migrated.
let path = PathUtils.join(PathUtils.profileDir, DB_FILENAME);
let db = await Sqlite.openConnection({ path });
// We can reuse the same guid, it doesn't matter for this test.
await db.execute(
`INSERT INTO moz_places (url, guid, url_hash)
VALUES (NULL, :guid, "123456")`,
{ guid }
);
await db.execute(
`INSERT INTO moz_bookmarks (fk, guid)
VALUES ((SELECT id FROM moz_places WHERE guid = :guid), :guid)
`,
{ guid }
);
await db.close();
});
add_task(async function database_is_valid() {
// Accessing the database for the first time triggers migration.
Assert.equal(
PlacesUtils.history.databaseStatus,
PlacesUtils.history.DATABASE_STATUS_UPGRADED
);
let db = await PlacesUtils.promiseDBConnection();
Assert.equal(await db.getSchemaVersion(), CURRENT_SCHEMA_VERSION);
let page = await PlacesUtils.history.fetch(guid);
Assert.equal(page.url.href, "place:excludeItems=1");
let rows = await db.execute(
`
SELECT syncChangeCounter
FROM moz_bookmarks
WHERE guid = :guid
`,
{ guid }
);
Assert.equal(rows[0].getResultByIndex(0), 2);
});

View File

@ -1,128 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
add_task(async function setup() {
await setupPlacesDatabase("places_v43.sqlite");
});
// Accessing the database for the first time should trigger migration, and the
// schema version should be updated.
add_task(async function database_is_valid() {
Assert.equal(
PlacesUtils.history.databaseStatus,
PlacesUtils.history.DATABASE_STATUS_UPGRADED
);
let db = await PlacesUtils.promiseDBConnection();
Assert.equal(await db.getSchemaVersion(), CURRENT_SCHEMA_VERSION);
// Now wait for moz_origins.frecency to be populated before continuing with
// other test tasks.
await TestUtils.waitForCondition(
() => {
return !Services.prefs.getBoolPref(
"places.database.migrateV52OriginFrecencies",
false
);
},
"Waiting for v52 origin frecencies to be migrated",
100,
3000
);
});
// moz_origins should be populated.
add_task(async function test_origins() {
let db = await PlacesUtils.promiseDBConnection();
// Collect origins.
let rows = await db.execute(`
SELECT id, prefix, host, frecency
FROM moz_origins
ORDER BY id ASC;
`);
Assert.notEqual(rows.length, 0);
let origins = rows.map(r => ({
id: r.getResultByName("id"),
prefix: r.getResultByName("prefix"),
host: r.getResultByName("host"),
frecency: r.getResultByName("frecency"),
}));
// Get moz_places.
rows = await db.execute(`
SELECT get_prefix(url) AS prefix, get_host_and_port(url) AS host,
origin_id, frecency
FROM moz_places;
`);
Assert.notEqual(rows.length, 0);
let seenOriginIDs = [];
let frecenciesByOriginID = {};
// Make sure moz_places.origin_id refers to the right origins.
for (let row of rows) {
let originID = row.getResultByName("origin_id");
let origin = origins.find(o => o.id == originID);
Assert.ok(origin);
Assert.equal(origin.prefix, row.getResultByName("prefix"));
Assert.equal(origin.host, row.getResultByName("host"));
seenOriginIDs.push(originID);
let frecency = row.getResultByName("frecency");
frecenciesByOriginID[originID] = frecenciesByOriginID[originID] || 0;
frecenciesByOriginID[originID] += frecency;
}
for (let origin of origins) {
// Make sure each origin corresponds to at least one moz_place.
Assert.ok(seenOriginIDs.includes(origin.id));
// moz_origins.frecency should be the sum of frecencies of all moz_places
// with the origin.
Assert.equal(origin.frecency, frecenciesByOriginID[origin.id]);
}
// Make sure moz_hosts was emptied.
rows = await db.execute(`
SELECT *
FROM moz_hosts;
`);
Assert.equal(rows.length, 0);
});
// Frecency stats should have been collected.
add_task(async function test_frecency_stats() {
let db = await PlacesUtils.promiseDBConnection();
// Collect positive frecencies from moz_origins.
let rows = await db.execute(`
SELECT frecency FROM moz_origins WHERE frecency > 0
`);
Assert.notEqual(rows.length, 0);
let frecencies = rows.map(r => r.getResultByName("frecency"));
// Collect stats.
rows = await db.execute(`
SELECT
(SELECT value FROM moz_meta WHERE key = "origin_frecency_count"),
(SELECT value FROM moz_meta WHERE key = "origin_frecency_sum"),
(SELECT value FROM moz_meta WHERE key = "origin_frecency_sum_of_squares")
`);
let count = rows[0].getResultByIndex(0);
let sum = rows[0].getResultByIndex(1);
let squares = rows[0].getResultByIndex(2);
Assert.equal(count, frecencies.length);
Assert.equal(
sum,
frecencies.reduce((memo, f) => memo + f, 0)
);
Assert.equal(
squares,
frecencies.reduce((memo, f) => memo + f * f, 0)
);
});

View File

@ -1,190 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const gCreatedParentGuid = "m47___FOLDER";
const gTestItems = [
{
// Folder shortcuts to built-in folders.
guid: "m47_____ROOT",
url: "place:folder=PLACES_ROOT",
targetParentGuid: "rootGuid",
},
{
guid: "m47_____MENU",
url: "place:folder=BOOKMARKS_MENU",
targetParentGuid: "menuGuid",
},
{
guid: "m47_____TAGS",
url: "place:folder=TAGS",
targetParentGuid: "tagsGuid",
},
{
guid: "m47____OTHER",
url: "place:folder=UNFILED_BOOKMARKS",
targetParentGuid: "unfiledGuid",
},
{
guid: "m47__TOOLBAR",
url: "place:folder=TOOLBAR",
targetParentGuid: "toolbarGuid",
},
{
guid: "m47___MOBILE",
url: "place:folder=MOBILE_BOOKMARKS",
targetParentGuid: "mobileGuid",
},
{
// Folder shortcut to using id.
guid: "m47_______ID",
url: "place:folder=%id%",
expectedUrl: "place:parent=%guid%",
},
{
// Folder shortcut to multiple folders.
guid: "m47____MULTI",
url: "place:folder=TOOLBAR&folder=%id%&sort=1",
expectedUrl: "place:parent=%toolbarGuid%&parent=%guid%&sort=1",
},
{
// Folder shortcut to non-existent folder.
guid: "m47______NON",
url: "place:folder=454554545",
expectedUrl: "place:invalidOldParentId=454554545&excludeItems=1",
},
];
add_task(async function setup() {
await setupPlacesDatabase("places_v43.sqlite");
// Setup database contents to be migrated.
let path = PathUtils.join(PathUtils.profileDir, DB_FILENAME);
let db = await Sqlite.openConnection({ path });
let rows = await db.execute(
`SELECT id FROM moz_bookmarks
WHERE guid = :guid`,
{ guid: PlacesUtils.bookmarks.unfiledGuid }
);
let unfiledId = rows[0].getResultByName("id");
// Insert a test folder.
await db.execute(
`INSERT INTO moz_bookmarks (guid, title, parent)
VALUES (:guid, "Folder", :parent)`,
{ guid: gCreatedParentGuid, parent: unfiledId }
);
rows = await db.execute(
`SELECT id FROM moz_bookmarks
WHERE guid = :guid`,
{ guid: gCreatedParentGuid }
);
let createdFolderId = rows[0].getResultByName("id");
for (let item of gTestItems) {
item.url = item.url.replace("%id%", createdFolderId);
// We can reuse the same guid, it doesn't matter for this test.
await db.execute(
`INSERT INTO moz_places (url, guid, url_hash)
VALUES (:url, :guid, :hash)
`,
{
url: item.url,
guid: item.guid,
hash: PlacesUtils.history.hashURL(item.url),
}
);
await db.execute(
`INSERT INTO moz_bookmarks (id, fk, guid, title, parent)
VALUES (:id, (SELECT id FROM moz_places WHERE guid = :guid),
:guid, :title,
(SELECT id FROM moz_bookmarks WHERE guid = :parentGuid))
`,
{
id: item.folder,
guid: item.guid,
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
title: item.guid,
}
);
}
await db.close();
});
add_task(async function database_is_valid() {
// Accessing the database for the first time triggers migration.
Assert.equal(
PlacesUtils.history.databaseStatus,
PlacesUtils.history.DATABASE_STATUS_UPGRADED
);
let db = await PlacesUtils.promiseDBConnection();
Assert.equal(await db.getSchemaVersion(), CURRENT_SCHEMA_VERSION);
});
add_task(async function test_correct_folder_queries() {
for (let item of gTestItems) {
let bm = await PlacesUtils.bookmarks.fetch(item.guid);
if (item.targetParentGuid) {
Assert.equal(
bm.url,
`place:parent=${PlacesUtils.bookmarks[item.targetParentGuid]}`,
`Should have updated the URL for ${item.guid}`
);
} else {
let expected = item.expectedUrl
.replace("%guid%", gCreatedParentGuid)
.replace("%toolbarGuid%", PlacesUtils.bookmarks.toolbarGuid);
Assert.equal(
bm.url,
expected,
`Should have updated the URL for ${item.guid}`
);
}
}
});
add_task(async function test_hashes_valid() {
let db = await PlacesUtils.promiseDBConnection();
// Ensure all the hashes in moz_places are valid.
let rows = await db.execute(`SELECT url, url_hash FROM moz_places`);
for (let row of rows) {
let url = row.getResultByName("url");
let url_hash = row.getResultByName("url_hash");
Assert.equal(
url_hash,
PlacesUtils.history.hashURL(url),
`url hash should be correct for ${url}`
);
}
});
add_task(async function test_sync_counters_updated() {
let db = await PlacesUtils.promiseDBConnection();
for (let test of gTestItems) {
let rows = await db.execute(
`SELECT syncChangeCounter FROM moz_bookmarks
WHERE guid = :guid`,
{ guid: test.guid }
);
Assert.equal(rows.length, 1, `Should only be one record for ${test.guid}`);
Assert.equal(
rows[0].getResultByName("syncChangeCounter"),
2,
`Should have bumped the syncChangeCounter for ${test.guid}`
);
}
});

View File

@ -1,209 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const BASE_GUID = "null".padEnd(11, "_");
const LAST_USED_ANNO = "bookmarkPropertiesDialog/folderLastUsed";
const LAST_USED_META_DATA = "places/bookmarks/edit/lastusedfolder";
let expectedGuids = [];
async function adjustIndices(db, itemGuid) {
await db.execute(
`
UPDATE moz_bookmarks SET
position = position - 1
WHERE parent = (SELECT parent FROM moz_bookmarks
WHERE guid = :itemGuid) AND
position >= (SELECT position FROM moz_bookmarks
WHERE guid = :itemGuid)`,
{ itemGuid }
);
}
async function fetchChildInfos(db, parentGuid) {
let rows = await db.execute(
`
SELECT b.guid, b.position, b.syncChangeCounter
FROM moz_bookmarks b
JOIN moz_bookmarks p ON p.id = b.parent
WHERE p.guid = :parentGuid
ORDER BY b.position`,
{ parentGuid }
);
return rows.map(row => ({
guid: row.getResultByName("guid"),
position: row.getResultByName("position"),
syncChangeCounter: row.getResultByName("syncChangeCounter"),
}));
}
add_task(async function setup() {
await setupPlacesDatabase("places_v43.sqlite");
// Setup database contents to be migrated.
let path = PathUtils.join(PathUtils.profileDir, DB_FILENAME);
let db = await Sqlite.openConnection({ path });
// We can reuse the same guid, it doesn't matter for this test.
await db.execute(
`INSERT INTO moz_anno_attributes (name)
VALUES (:last_used_anno)`,
{ last_used_anno: LAST_USED_ANNO }
);
for (let i = 0; i < 3; i++) {
let guid = `${BASE_GUID}${i}`;
await db.execute(
`INSERT INTO moz_bookmarks (guid, type)
VALUES (:guid, :type)
`,
{ guid, type: PlacesUtils.bookmarks.TYPE_FOLDER }
);
await db.execute(
`INSERT INTO moz_items_annos (item_id, anno_attribute_id, content)
VALUES ((SELECT id FROM moz_bookmarks WHERE guid = :guid),
(SELECT id FROM moz_anno_attributes WHERE name = :last_used_anno),
:content)`,
{
guid,
content: new Date(1517318477569) - (3 - i) * 60 * 60 * 1000,
last_used_anno: LAST_USED_ANNO,
}
);
expectedGuids.unshift(guid);
}
info("Move menu into unfiled");
await adjustIndices(db, "menu________");
await db.execute(
`
UPDATE moz_bookmarks SET
parent = (SELECT id FROM moz_bookmarks WHERE guid = :newParentGuid),
position = IFNULL((SELECT MAX(position) + 1 FROM moz_bookmarks
WHERE guid = :newParentGuid), 0)
WHERE guid = :itemGuid`,
{ newParentGuid: "unfiled_____", itemGuid: "menu________" }
);
info("Move toolbar into mobile");
let mobileChildren = [
"bookmarkAAAA",
"bookmarkBBBB",
"toolbar_____",
"bookmarkCCCC",
"bookmarkDDDD",
];
await adjustIndices(db, "toolbar_____");
for (let position = 0; position < mobileChildren.length; position++) {
await db.execute(
`
INSERT INTO moz_bookmarks(guid, parent, position)
VALUES(:guid, (SELECT id FROM moz_bookmarks WHERE guid = 'mobile______'),
:position)
ON CONFLICT(guid) DO UPDATE SET
parent = excluded.parent,
position = excluded.position`,
{ guid: mobileChildren[position], position }
);
}
info("Reset Sync change counters");
await db.execute(`UPDATE moz_bookmarks SET syncChangeCounter = 0`);
await db.close();
});
add_task(async function database_is_valid() {
// Accessing the database for the first time triggers migration.
Assert.equal(
PlacesUtils.history.databaseStatus,
PlacesUtils.history.DATABASE_STATUS_UPGRADED
);
let db = await PlacesUtils.promiseDBConnection();
Assert.equal(await db.getSchemaVersion(), CURRENT_SCHEMA_VERSION);
});
add_task(async function test_folders_migrated() {
let metaData = await PlacesUtils.metadata.get(LAST_USED_META_DATA);
Assert.deepEqual(JSON.parse(metaData), expectedGuids);
});
add_task(async function test_annotations_removed() {
let db = await PlacesUtils.promiseDBConnection();
await assertAnnotationsRemoved(db, [LAST_USED_ANNO]);
});
add_task(async function test_no_orphan_annotations() {
let db = await PlacesUtils.promiseDBConnection();
await assertNoOrphanAnnotations(db);
});
add_task(async function test_roots_fixed() {
let db = await PlacesUtils.promiseDBConnection();
let expectedRootInfos = [
{
guid: PlacesUtils.bookmarks.tagsGuid,
position: 0,
syncChangeCounter: 0,
},
{
guid: PlacesUtils.bookmarks.unfiledGuid,
position: 1,
syncChangeCounter: 1,
},
{
guid: PlacesUtils.bookmarks.mobileGuid,
position: 2,
syncChangeCounter: 1,
},
{
guid: PlacesUtils.bookmarks.menuGuid,
position: 3,
syncChangeCounter: 1,
},
{
guid: PlacesUtils.bookmarks.toolbarGuid,
position: 4,
syncChangeCounter: 1,
},
];
Assert.deepEqual(
expectedRootInfos,
await fetchChildInfos(db, PlacesUtils.bookmarks.rootGuid),
"All roots should be reparented to the Places root"
);
let expectedMobileInfos = [
{
guid: "bookmarkAAAA",
position: 0,
syncChangeCounter: 0,
},
{
guid: "bookmarkBBBB",
position: 1,
syncChangeCounter: 0,
},
{
guid: "bookmarkCCCC",
position: 2,
syncChangeCounter: 0,
},
{
guid: "bookmarkDDDD",
position: 3,
syncChangeCounter: 0,
},
];
Assert.deepEqual(
expectedMobileInfos,
await fetchChildInfos(db, PlacesUtils.bookmarks.mobileGuid),
"Should fix misparented root sibling positions"
);
});

View File

@ -1,6 +1,6 @@
add_task(async function setup() {
// Since this migration doesn't affect places.sqlite, we can reuse v43.
await setupPlacesDatabase("places_v43.sqlite");
await setupPlacesDatabase("places_v52.sqlite");
await setupPlacesDatabase("favicons_v41.sqlite", "favicons.sqlite");
});

View File

@ -6,7 +6,7 @@ skip-if = condprof # Not worth running conditioned profiles on these migration
support-files =
favicons_v41.sqlite
places_outdated.sqlite
places_v43.sqlite
places_v52.sqlite
places_v54.sqlite
places_v66.sqlite
places_v68.sqlite
@ -18,12 +18,6 @@ support-files =
[test_current_from_downgraded.js]
[test_current_from_outdated.js]
[test_current_from_v43.js]
[test_current_from_v45.js]
[test_current_from_v46.js]
[test_current_from_v47.js]
[test_current_from_v48.js]
[test_current_from_v50.js]
[test_current_from_v53.js]
[test_current_from_v54.js]
[test_current_from_v66.js]