--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -1290,17 +1290,22 @@ Database::InitSchema(bool* aDatabaseMigr
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 49) {
rv = MigrateV49Up();
NS_ENSURE_SUCCESS(rv, rv);
}
- // Firefox 62 uses schema version 49.
+ if (currentSchemaVersion < 50) {
+ rv = MigrateV50Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 62 uses schema version 50.
// Schema Upgrades must add migration code here.
// >>> IMPORTANT! <<<
// NEVER MIX UP SYNC AND ASYNC EXECUTION IN MIGRATORS, YOU MAY LOCK THE
// CONNECTION AND CAUSE FURTHER STEPS TO FAIL.
// In case, set a bool and do the async work in the ScopeExit guard just
// before the migration steps.
}
@@ -2486,16 +2491,173 @@ Database::MigrateV49Up() {
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(NS_LITERAL_CSTRING(
+ "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<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;
+
+ if (!placeURLs.AppendElement(MakePair(placeId, url))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ 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(NS_LITERAL_CSTRING(
+ "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, NS_LITERAL_CSTRING("url"), url);
+ if (NS_FAILED(rv)) return rv;
+ rv = updateStmt->BindInt64ByName(NS_LITERAL_CSTRING("placeId"), 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(NS_LITERAL_CSTRING(
+ "UPDATE moz_bookmarks SET syncChangeCounter = syncChangeCounter + 1 "
+ "WHERE fk = :placeId "
+ ), getter_AddRefs(syncStmt));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = syncStmt->BindInt64ByName(NS_LITERAL_CSTRING("placeId"), placeId);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = syncStmt->Execute();
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ return NS_OK;
+}
+
+
+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")) {
+ if (!newTokens.AppendElement(kvp)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ 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(NS_LITERAL_CSTRING(
+ "SELECT guid FROM moz_bookmarks "
+ "WHERE id = :itemId "
+ ), getter_AddRefs(stmt));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("itemId"), 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 = NS_LITERAL_CSTRING(ROOT_GUID);
+ } else if (kvp.value.EqualsLiteral("BOOKMARKS_MENU")) {
+ guid = NS_LITERAL_CSTRING(MENU_ROOT_GUID);
+ } else if (kvp.value.EqualsLiteral("TAGS")) {
+ guid = NS_LITERAL_CSTRING(TAGS_ROOT_GUID);
+ } else if (kvp.value.EqualsLiteral("UNFILED_BOOKMARKS")) {
+ guid = NS_LITERAL_CSTRING(UNFILED_ROOT_GUID);
+ } else if (kvp.value.EqualsLiteral("TOOLBAR")) {
+ guid = NS_LITERAL_CSTRING(TOOLBAR_ROOT_GUID);
+ } else if (kvp.value.EqualsLiteral("MOBILE_BOOKMARKS")) {
+ guid = NS_LITERAL_CSTRING(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(NS_LITERAL_CSTRING("invalidOldParentId"), kvp.value);
+ invalid = true;
+ } else {
+ newPair = new QueryKeyValuePair(NS_LITERAL_CSTRING("parent"), guid);
+ }
+ if (!newTokens.AppendElement(*newPair)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ delete newPair;
+ }
+
+ if (invalid) {
+ // One or more of the folders don't exist, replace with an empty query.
+ newTokens.AppendElement(QueryKeyValuePair(NS_LITERAL_CSTRING("excludeItems"),
+ NS_LITERAL_CSTRING("1")));
+ }
+
+ TokensToQueryString(newTokens, aURL);
+ return NS_OK;
+}
+
+nsresult
Database::GetItemsWithAnno(const nsACString& aAnnoName, int32_t aItemType,
nsTArray<int64_t>& aItemIds)
{
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
"SELECT b.id FROM moz_items_annos a "
"JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id "
"JOIN moz_bookmarks b ON b.id = a.item_id "
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -14,17 +14,17 @@
#include "mozilla/storage/StatementCache.h"
#include "mozilla/Attributes.h"
#include "nsIEventTarget.h"
#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 49
+#define DATABASE_SCHEMA_VERSION 50
// 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
// initial shutdown work and notifies TOPIC_PLACES_SHUTDOWN to all listeners.
// Any shutdown work that requires the Places APIs should happen here.
#define TOPIC_PROFILE_CHANGE_TEARDOWN "profile-change-teardown"
// Fired when Places is shutting down. Any code should stop accessing Places
@@ -331,24 +331,26 @@ protected:
nsresult MigrateV42Up();
nsresult MigrateV43Up();
nsresult MigrateV44Up();
nsresult MigrateV45Up();
nsresult MigrateV46Up();
nsresult MigrateV47Up();
nsresult MigrateV48Up();
nsresult MigrateV49Up();
+ nsresult MigrateV50Up();
void MigrateV48Frecencies();
nsresult UpdateBookmarkRootTitles();
friend class ConnectionShutdownBlocker;
int64_t CreateMobileRoot();
+ nsresult ConvertOldStyleQuery(nsCString& aURL);
nsresult GetItemsWithAnno(const nsACString& aAnnoName, int32_t aItemType,
nsTArray<int64_t>& aItemIds);
nsresult DeleteBookmarkItem(int32_t aItemId);
private:
~Database();
/**
--- a/toolkit/components/places/Helpers.cpp
+++ b/toolkit/components/places/Helpers.cpp
@@ -355,16 +355,69 @@ bool
GetHiddenState(bool aIsRedirect,
uint32_t aTransitionType)
{
return aTransitionType == nsINavHistoryService::TRANSITION_FRAMED_LINK ||
aTransitionType == nsINavHistoryService::TRANSITION_EMBED ||
aIsRedirect;
}
+nsresult
+TokenizeQueryString(const nsACString& aQuery,
+ nsTArray<QueryKeyValuePair>* aTokens)
+{
+ // Strip off the "place:" prefix
+ const uint32_t prefixlen = 6; // = strlen("place:");
+ nsCString query;
+ if (aQuery.Length() >= prefixlen &&
+ Substring(aQuery, 0, prefixlen).EqualsLiteral("place:"))
+ query = Substring(aQuery, prefixlen);
+ else
+ query = aQuery;
+
+ int32_t keyFirstIndex = 0;
+ int32_t equalsIndex = 0;
+ for (uint32_t i = 0; i < query.Length(); i ++) {
+ if (query[i] == '&') {
+ // new clause, save last one
+ if (i - keyFirstIndex > 1) {
+ if (! aTokens->AppendElement(QueryKeyValuePair(query, keyFirstIndex,
+ equalsIndex, i)))
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ keyFirstIndex = equalsIndex = i + 1;
+ } else if (query[i] == '=') {
+ equalsIndex = i;
+ }
+ }
+
+ // handle last pair, if any
+ if (query.Length() - keyFirstIndex > 1) {
+ if (! aTokens->AppendElement(QueryKeyValuePair(query, keyFirstIndex,
+ equalsIndex, query.Length())))
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+}
+
+void
+TokensToQueryString(const nsTArray<QueryKeyValuePair> &aTokens,
+ nsACString &aQuery)
+{
+ aQuery = NS_LITERAL_CSTRING("place:");
+ for (uint32_t i = 0; i < aTokens.Length(); i++) {
+ if (i > 0) {
+ aQuery.Append("&");
+ }
+ aQuery.Append(aTokens[i].key);
+ aQuery.AppendLiteral("=");
+ aQuery.Append(aTokens[i].value);
+ }
+}
+
////////////////////////////////////////////////////////////////////////////////
//// AsyncStatementCallbackNotifier
NS_IMETHODIMP
AsyncStatementCallbackNotifier::HandleCompletion(uint16_t aReason)
{
if (aReason != mozIStorageStatementCallback::REASON_FINISHED)
return NS_ERROR_UNEXPECTED;
--- a/toolkit/components/places/Helpers.h
+++ b/toolkit/components/places/Helpers.h
@@ -172,16 +172,62 @@ PRTime RoundToMilliseconds(PRTime aTime)
*
* @return @see PR_Now, RoundToMilliseconds.
*/
PRTime RoundedPRNow();
nsresult HashURL(const nsAString& aSpec, const nsACString& aMode,
uint64_t *_hash);
+class QueryKeyValuePair final
+{
+public:
+
+ QueryKeyValuePair(const nsACString &aKey, const nsACString &aValue)
+ {
+ key = aKey;
+ value = aValue;
+ };
+
+ // QueryKeyValuePair
+ //
+ // 01234567890
+ // input : qwerty&key=value&qwerty
+ // ^ ^ ^
+ // aKeyBegin | aPastEnd (may point to null terminator)
+ // aEquals
+ //
+ // Special case: if aKeyBegin == aEquals, then there is only one string
+ // and no equal sign, so we treat the entire thing as a key with no value
+
+ QueryKeyValuePair(const nsACString& aSource, int32_t aKeyBegin,
+ int32_t aEquals, int32_t aPastEnd)
+ {
+ if (aEquals == aKeyBegin)
+ aEquals = aPastEnd;
+ key = Substring(aSource, aKeyBegin, aEquals - aKeyBegin);
+ if (aPastEnd - aEquals > 0)
+ value = Substring(aSource, aEquals + 1, aPastEnd - aEquals - 1);
+ }
+ nsCString key;
+ nsCString value;
+ };
+
+ /**
+ * Tokenizes a QueryString.
+ *
+ * @param aQuery The string to tokenize.
+ * @param aTokens The tokenized result.
+ */
+nsresult TokenizeQueryString(const nsACString& aQuery,
+ nsTArray<QueryKeyValuePair>* aTokens);
+
+void TokensToQueryString(const nsTArray<QueryKeyValuePair> &aTokens,
+ nsACString &aQuery);
+
/**
* Used to finalize a statementCache on a specified thread.
*/
template<typename StatementType>
class FinalizeStatementCacheProxy : public Runnable
{
public:
/**
--- a/toolkit/components/places/nsNavHistory.h
+++ b/toolkit/components/places/nsNavHistory.h
@@ -73,17 +73,16 @@
#define MOBILE_ROOT_GUID "mobile______"
class nsIAutoCompleteController;
class nsIEffectiveTLDService;
class nsIIDNService;
class nsNavHistory;
class PlacesDecayFrecencyCallback;
class PlacesSQLQueryBuilder;
-class QueryKeyValuePair;
// nsNavHistory
class nsNavHistory final : public nsSupportsWeakReference
, public nsINavHistoryService
, public nsIObserver
, public mozIStorageVacuumParticipant
{
@@ -636,17 +635,17 @@ protected:
int32_t mReloadVisitBonus;
void DecayFrecencyCompleted(uint16_t reason);
uint32_t mDecayFrecencyPendingCount;
nsresult RecalculateFrecencyStatsInternal();
// in nsNavHistoryQuery.cpp
- nsresult TokensToQuery(const nsTArray<QueryKeyValuePair>& aTokens,
+ nsresult TokensToQuery(const nsTArray<mozilla::places::QueryKeyValuePair>& aTokens,
nsNavHistoryQuery* aQuery,
nsNavHistoryQueryOptions* aOptions);
int64_t mTagsFolder;
int32_t mDaysOfHistory;
int64_t mLastCachedStartOfDay;
int64_t mLastCachedEndOfDay;
--- a/toolkit/components/places/nsNavHistoryQuery.cpp
+++ b/toolkit/components/places/nsNavHistoryQuery.cpp
@@ -16,47 +16,18 @@
#include "nsEscape.h"
#include "nsCOMArray.h"
#include "nsNetUtil.h"
#include "nsTArray.h"
#include "prprf.h"
#include "nsVariant.h"
using namespace mozilla;
-
-class QueryKeyValuePair
-{
-public:
-
- // QueryKeyValuePair
- //
- // 01234567890
- // input : qwerty&key=value&qwerty
- // ^ ^ ^
- // aKeyBegin | aPastEnd (may point to null terminator)
- // aEquals
- //
- // Special case: if aKeyBegin == aEquals, then there is only one string
- // and no equal sign, so we treat the entire thing as a key with no value
+using namespace mozilla::places;
- QueryKeyValuePair(const nsACString& aSource, int32_t aKeyBegin,
- int32_t aEquals, int32_t aPastEnd)
- {
- if (aEquals == aKeyBegin)
- aEquals = aPastEnd;
- key = Substring(aSource, aKeyBegin, aEquals - aKeyBegin);
- if (aPastEnd - aEquals > 0)
- value = Substring(aSource, aEquals + 1, aPastEnd - aEquals - 1);
- }
- nsCString key;
- nsCString value;
-};
-
-static nsresult TokenizeQueryString(const nsACString& aQuery,
- nsTArray<QueryKeyValuePair>* aTokens);
static nsresult ParseQueryBooleanString(const nsCString& aString,
bool* aValue);
// query getters
typedef decltype(&nsINavHistoryQuery::GetOnlyBookmarked) BoolQueryGetter;
typedef decltype(&nsINavHistoryQuery::GetBeginTimeReference) Uint32QueryGetter;
typedef decltype(&nsINavHistoryQuery::GetBeginTime) Int64QueryGetter;
static void AppendBoolKeyValueIfTrue(nsACString& aString,
@@ -409,56 +380,16 @@ nsNavHistory::QueryToQueryString(nsINavH
}
aQueryString.AssignLiteral("place:");
aQueryString.Append(queryString);
return NS_OK;
}
-// TokenizeQueryString
-
-nsresult
-TokenizeQueryString(const nsACString& aQuery,
- nsTArray<QueryKeyValuePair>* aTokens)
-{
- // Strip off the "place:" prefix
- const uint32_t prefixlen = 6; // = strlen("place:");
- nsCString query;
- if (aQuery.Length() >= prefixlen &&
- Substring(aQuery, 0, prefixlen).EqualsLiteral("place:"))
- query = Substring(aQuery, prefixlen);
- else
- query = aQuery;
-
- int32_t keyFirstIndex = 0;
- int32_t equalsIndex = 0;
- for (uint32_t i = 0; i < query.Length(); i ++) {
- if (query[i] == '&') {
- // new clause, save last one
- if (i - keyFirstIndex > 1) {
- if (! aTokens->AppendElement(QueryKeyValuePair(query, keyFirstIndex,
- equalsIndex, i)))
- return NS_ERROR_OUT_OF_MEMORY;
- }
- keyFirstIndex = equalsIndex = i + 1;
- } else if (query[i] == '=') {
- equalsIndex = i;
- }
- }
-
- // handle last pair, if any
- if (query.Length() - keyFirstIndex > 1) {
- if (! aTokens->AppendElement(QueryKeyValuePair(query, keyFirstIndex,
- equalsIndex, query.Length())))
- return NS_ERROR_OUT_OF_MEMORY;
- }
- return NS_OK;
-}
-
nsresult
nsNavHistory::TokensToQuery(const nsTArray<QueryKeyValuePair>& aTokens,
nsNavHistoryQuery* aQuery,
nsNavHistoryQueryOptions* aOptions)
{
nsresult rv;
if (aTokens.Length() == 0)
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -1,14 +1,14 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-const CURRENT_SCHEMA_VERSION = 49;
+const CURRENT_SCHEMA_VERSION = 50;
const FIRST_UPGRADABLE_SCHEMA_VERSION = 30;
const NS_APP_USER_PROFILE_50_DIR = "ProfD";
// Shortcuts to transitions type.
const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
const TRANSITION_BOOKMARK = Ci.nsINavHistoryService.TRANSITION_BOOKMARK;
--- a/toolkit/components/places/tests/migration/test_current_from_v45.js
+++ b/toolkit/components/places/tests/migration/test_current_from_v45.js
@@ -17,16 +17,17 @@ let gTags = [
{ 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");
@@ -56,17 +57,17 @@ add_task(async function database_is_vali
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.url : "place:tag=" + tag.title;
+ 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) {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/migration/test_current_from_v48.js
@@ -0,0 +1,146 @@
+/* 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 = OS.Path.join(OS.Constants.Path.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}`);
+ }
+});
--- a/toolkit/components/places/tests/migration/xpcshell.ini
+++ b/toolkit/components/places/tests/migration/xpcshell.ini
@@ -21,8 +21,9 @@ support-files =
[test_current_from_v36.js]
[test_current_from_v38.js]
[test_current_from_v41.js]
[test_current_from_v42.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]