Bug 1360872 - Return empty strings for `null` bookmark titles. r=mak draft
authorKit Cambridge <kit@yakshaving.ninja>
Tue, 06 Jun 2017 14:37:22 -0700
changeset 597749 81d0d9b8010d3bdbedc8794ec64a6134127efdab
parent 589150 cad53f061da634a16ea75887558301b77f65745d
child 634308 b92a5567da4aa00d1c1b65394a0e44aaadb54453
push id65016
push userbmo:kit@mozilla.com
push dateTue, 20 Jun 2017 22:33:38 +0000
reviewersmak
bugs1360872
milestone55.0a1
Bug 1360872 - Return empty strings for `null` bookmark titles. r=mak MozReview-Commit-ID: Dd2sEfYvnBt
browser/components/places/PlacesUIUtils.jsm
browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js
services/sync/tests/unit/test_bookmark_store.js
toolkit/components/places/Bookmarks.jsm
toolkit/components/places/Helpers.cpp
toolkit/components/places/PlacesUtils.jsm
toolkit/components/places/nsNavBookmarks.cpp
toolkit/components/places/nsNavHistory.cpp
toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js
toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js
toolkit/components/places/tests/bookmarks/test_bookmarks_insertTree.js
toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
toolkit/components/places/tests/bookmarks/test_bookmarks_update.js
toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
toolkit/components/places/tests/queries/test_onlyBookmarked.js
toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -1126,25 +1126,25 @@ this.PlacesUIUtils = {
     // This is the list of the left pane queries.
     let queries = {
       "PlacesRoot": { title: "" },
       "History": { title: this.getString("OrganizerQueryHistory") },
       "Downloads": { title: this.getString("OrganizerQueryDownloads") },
       "Tags": { title: this.getString("OrganizerQueryTags") },
       "AllBookmarks": { title: this.getString("OrganizerQueryAllBookmarks") },
       "BookmarksToolbar":
-        { title: null,
+        { title: "",
           concreteTitle: PlacesUtils.getString("BookmarksToolbarFolderTitle"),
           concreteId: PlacesUtils.toolbarFolderId },
       "BookmarksMenu":
-        { title: null,
+        { title: "",
           concreteTitle: PlacesUtils.getString("BookmarksMenuFolderTitle"),
           concreteId: PlacesUtils.bookmarksMenuFolderId },
       "UnfiledBookmarks":
-        { title: null,
+        { title: "",
           concreteTitle: PlacesUtils.getString("OtherBookmarksFolderTitle"),
           concreteId: PlacesUtils.unfiledBookmarksFolderId },
     };
     // All queries but PlacesRoot.
     const EXPECTED_QUERY_COUNT = 7;
 
     // Removes an item and associated annotations, ignoring eventual errors.
     function safeRemoveItem(aItemId) {
--- a/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js
@@ -29,14 +29,14 @@ add_task(async function() {
            "Node title is correct");
         // Blur the field and ensure root's name has not been changed.
         namepicker.blur();
         is(namepicker.value,
            PlacesUtils.bookmarks.getItemTitle(PlacesUtils.unfiledBookmarksFolderId),
            "Root title is correct");
         // Check the shortcut's title.
         let bookmark = await PlacesUtils.bookmarks.fetch(tree.selectedNode.bookmarkGuid);
-        is(bookmark.title, null,
+        is(bookmark.title, "",
            "Shortcut title is null");
       }
     );
   });
 });
--- a/services/sync/tests/unit/test_bookmark_store.js
+++ b/services/sync/tests/unit/test_bookmark_store.js
@@ -98,17 +98,17 @@ add_test(function test_bookmark_create()
     store.applyIncoming(tbrecord);
 
     _("Verify it has been created correctly.");
     id = store.idForGUID(tbrecord.id);
     do_check_eq(store.GUIDForId(id), tbrecord.id);
     do_check_eq(PlacesUtils.bookmarks.getItemType(id),
                 PlacesUtils.bookmarks.TYPE_BOOKMARK);
     do_check_true(PlacesUtils.bookmarks.getBookmarkURI(id).equals(tburi));
-    do_check_eq(PlacesUtils.bookmarks.getItemTitle(id), null);
+    do_check_eq(PlacesUtils.bookmarks.getItemTitle(id), "");
     let error;
     try {
       PlacesUtils.annotations.getItemAnnotation(id, "bookmarkProperties/description");
     } catch (ex) {
       error = ex;
     }
     do_check_eq(error.result, Cr.NS_ERROR_NOT_AVAILABLE);
     do_check_eq(PlacesUtils.bookmarks.getFolderIdForItem(id),
@@ -142,17 +142,17 @@ add_test(function test_bookmark_update()
     record.tags = null;
     store.applyIncoming(record);
 
     _("Verify that the values have been cleared.");
     do_check_throws(function() {
       PlacesUtils.annotations.getItemAnnotation(
         bmk1_id, "bookmarkProperties/description");
     }, Cr.NS_ERROR_NOT_AVAILABLE);
-    do_check_eq(PlacesUtils.bookmarks.getItemTitle(bmk1_id), null);
+    do_check_eq(PlacesUtils.bookmarks.getItemTitle(bmk1_id), "");
     do_check_eq(PlacesUtils.bookmarks.getKeywordForBookmark(bmk1_id), null);
   } finally {
     _("Clean up.");
     store.wipe();
     run_next_test();
   }
 });
 
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -30,18 +30,17 @@
  *      The time at which the item was last modified.
  *  - type (number)
  *      The item's type, either TYPE_BOOKMARK, TYPE_FOLDER or TYPE_SEPARATOR.
  *
  *  The following properties are only valid for URLs or folders.
  *
  *  - title (string)
  *      The item's title, if any.  Empty titles and null titles are considered
- *      the same, and the property is unset on retrieval in such a case.
- *      Titles longer than DB_TITLE_LENGTH_MAX will be truncated.
+ *      the same. Titles longer than DB_TITLE_LENGTH_MAX will be truncated.
  *
  *  The following properties are only valid for URLs:
  *
  *  - url (URL, href or nsIURI)
  *      The item's URL.  Note that while input objects can contains either
  *      an URL object, an href string, or an nsIURI, output objects will always
  *      contain an URL object.
  *      An URL cannot be longer than DB_URL_LENGTH_MAX, methods will throw if a
@@ -182,17 +181,18 @@ var Bookmarks = Object.freeze({
       modTime = now;
     }
     let insertInfo = validateBookmarkObject(info,
       { type: { defaultValue: this.TYPE_BOOKMARK }
       , index: { defaultValue: this.DEFAULT_INDEX }
       , url: { requiredIf: b => b.type == this.TYPE_BOOKMARK
              , validIf: b => b.type == this.TYPE_BOOKMARK }
       , parentGuid: { required: true }
-      , title: { validIf: b => [ this.TYPE_BOOKMARK
+      , title: { defaultValue: "",
+                 validIf: b => [ this.TYPE_BOOKMARK
                                , this.TYPE_FOLDER ].includes(b.type) }
       , dateAdded: { defaultValue: addedTime }
       , lastModified: { defaultValue: modTime,
                         validIf: b => b.lastModified >= now || (b.dateAdded && b.lastModified >= b.dateAdded) }
       , source: { defaultValue: this.SOURCES.DEFAULT }
       });
 
     return (async () => {
@@ -215,17 +215,17 @@ var Bookmarks = Object.freeze({
       // complete we may stop using it.
       let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
       let itemId = await PlacesUtils.promiseItemId(item.guid);
 
       // Pass tagging information for the observers to skip over these notifications when needed.
       let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
       let isTagsFolder = parent._id == PlacesUtils.tagsFolderId;
       notify(observers, "onItemAdded", [ itemId, parent._id, item.index,
-                                         item.type, uri, item.title || null,
+                                         item.type, uri, item.title,
                                          PlacesUtils.toPRTime(item.dateAdded), item.guid,
                                          item.parentGuid, item.source ],
                                        { isTagging: isTagging || isTagsFolder });
 
       // If it's a tag, notify OnItemChanged to all bookmarks for this URL.
       if (isTagging) {
         for (let entry of (await fetchBookmarksByURL(item))) {
           notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
@@ -341,17 +341,18 @@ var Bookmarks = Object.freeze({
         // Ensure to use the same date for dateAdded and lastModified, even if
         // dateAdded may be imposed by the caller.
         let time = (info && info.dateAdded) || fallbackLastAdded;
         let insertInfo = validateBookmarkObject(info, {
           type: { defaultValue: TYPE_BOOKMARK }
           , url: { requiredIf: b => b.type == TYPE_BOOKMARK
                  , validIf: b => b.type == TYPE_BOOKMARK }
           , parentGuid: { required: true }
-          , title: { validIf: b => [ TYPE_BOOKMARK
+          , title: { defaultValue: "",
+                     validIf: b => [ TYPE_BOOKMARK
                                    , TYPE_FOLDER ].includes(b.type) }
           , dateAdded: { defaultValue: time
                        , validIf: b => !b.lastModified ||
                                         b.dateAdded <= b.lastModified }
           , lastModified: { defaultValue: time,
                             validIf: b => (!b.dateAdded && b.lastModified >= time) ||
                                           (b.dateAdded && b.lastModified >= b.dateAdded) }
           , index: { replaceWith: indexToUse++ }
@@ -422,17 +423,17 @@ var Bookmarks = Object.freeze({
       let itemIdMap = await PlacesUtils.promiseManyItemIds(insertInfos.map(info => info.guid));
       // Notify onItemAdded to listeners.
       let observers = PlacesUtils.bookmarks.getObservers();
       for (let i = 0; i < insertInfos.length; i++) {
         let item = insertInfos[i];
         let itemId = itemIdMap.get(item.guid);
         let uri = item.hasOwnProperty("url") ? PlacesUtils.toURI(item.url) : null;
         notify(observers, "onItemAdded", [ itemId, parent._id, item.index,
-                                           item.type, uri, item.title || null,
+                                           item.type, uri, item.title,
                                            PlacesUtils.toPRTime(item.dateAdded), item.guid,
                                            item.parentGuid, item.source ],
                                          { isTagging: false });
         // Remove non-enumerable properties.
         delete item.source;
         insertInfos[i] = Object.assign({}, item);
       }
       return insertInfos;
@@ -1073,17 +1074,18 @@ function notify(observers, notification,
 
 function updateBookmark(info, item, newParent) {
   return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: updateBookmark",
     async function(db) {
 
     let tuples = new Map();
     tuples.set("lastModified", { value: PlacesUtils.toPRTime(info.lastModified) });
     if (info.hasOwnProperty("title"))
-      tuples.set("title", { value: info.title });
+      tuples.set("title", { value: info.title,
+                            fragment: `title = NULLIF(:title, "")` });
     if (info.hasOwnProperty("dateAdded"))
       tuples.set("dateAdded", { value: PlacesUtils.toPRTime(info.dateAdded) });
 
     await db.executeTransaction(async function() {
       let isTagging = item._grandParentId == PlacesUtils.tagsFolderId;
       let syncChangeDelta =
         PlacesSyncUtils.bookmarks.determineSyncChangeDelta(info.source);
 
@@ -1213,20 +1215,16 @@ function updateBookmark(info, item, newP
       Object.defineProperty(additionalParentInfo, "_parentId",
                             { value: newParent._id, enumerable: false });
       Object.defineProperty(additionalParentInfo, "_grandParentId",
                             { value: newParent._parentId, enumerable: false });
     }
 
     let updatedItem = mergeIntoNewObject(item, info, additionalParentInfo);
 
-    // Don't return an empty title to the caller.
-    if (updatedItem.hasOwnProperty("title") && updatedItem.title === null)
-      delete updatedItem.title;
-
     return updatedItem;
   });
 }
 
 // Insert implementation.
 
 function insertBookmark(item, parent) {
   return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: insertBookmark",
@@ -1260,18 +1258,18 @@ function insertBookmark(item, parent) {
         PlacesSyncUtils.bookmarks.determineInitialSyncStatus(item.source);
 
       // Insert the bookmark into the database.
       await db.executeCached(
         `INSERT INTO moz_bookmarks (fk, type, parent, position, title,
                                     dateAdded, lastModified, guid,
                                     syncChangeCounter, syncStatus)
          VALUES (CASE WHEN :url ISNULL THEN NULL ELSE (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url) END,
-                 :type, :parent, :index, :title, :date_added, :last_modified,
-                 :guid, :syncChangeCounter, :syncStatus)
+                 :type, :parent, :index, NULLIF(:title, ""), :date_added,
+                 :last_modified, :guid, :syncChangeCounter, :syncStatus)
         `, { url: item.hasOwnProperty("url") ? item.url.href : null,
              type: item.type, parent: parent._id, index: item.index,
              title: item.title, date_added: PlacesUtils.toPRTime(item.dateAdded),
              last_modified: PlacesUtils.toPRTime(item.lastModified), guid: item.guid,
              syncChangeCounter: syncChangeDelta, syncStatus });
 
       // Mark all affected separators as changed
       await adjustSeparatorsSyncCounter(db, parent._id, item.index + 1, syncChangeDelta);
@@ -1295,20 +1293,16 @@ function insertBookmark(item, parent) {
     });
 
     // If not a tag recalculate frecency...
     if (item.type == Bookmarks.TYPE_BOOKMARK && !isTagging) {
       // ...though we don't wait for the calculation.
       updateFrecency(db, [item.url]).catch(Cu.reportError);
     }
 
-    // Don't return an empty title to the caller.
-    if (item.hasOwnProperty("title") && item.title === null)
-      delete item.title;
-
     return item;
   });
 }
 
 function insertBookmarkTree(items, source, parent, urls, lastAddedForParent) {
   return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: insertBookmarkTree", async function(db) {
     await db.executeTransaction(async function transaction() {
       await maybeInsertManyPlaces(db, urls);
@@ -1329,17 +1323,17 @@ function insertBookmarkTree(items, sourc
       }));
       await db.executeCached(
         `INSERT INTO moz_bookmarks (fk, type, parent, position, title,
                                     dateAdded, lastModified, guid,
                                     syncChangeCounter, syncStatus)
          VALUES (CASE WHEN :url ISNULL THEN NULL ELSE (SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url) END, :type,
          (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid),
          IFNULL(:index, (SELECT count(*) FROM moz_bookmarks WHERE parent = :rootId)),
-         :title, :date_added, :last_modified, :guid,
+         NULLIF(:title, ""), :date_added, :last_modified, :guid,
          :syncChangeCounter, :syncStatus)`, items);
 
       await setAncestorsLastModified(db, parent.guid, lastAddedForParent,
                                      syncChangeDelta);
     });
 
     // We don't wait for the frecency calculation.
     updateFrecency(db, urls, true).catch(Cu.reportError);
@@ -1379,18 +1373,18 @@ async function queryBookmarks(info) {
 
   return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: queryBookmarks",
     async function(db) {
       // _id, _childCount, _grandParentId and _parentId fields
       // are required to be in the result by the converting function
       // hence setting them to NULL
       let rows = await db.executeCached(
         `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
-                b.dateAdded, b.lastModified, b.type, b.title,
-                h.url AS url, b.parent, p.parent,
+                b.dateAdded, b.lastModified, b.type,
+                IFNULL(b.title, "") AS title, h.url AS url, b.parent, p.parent,
                 NULL AS _id,
                 NULL AS _childCount,
                 NULL AS _grandParentId,
                 NULL AS _parentId,
                 NULL AS _syncStatus
          FROM moz_bookmarks b
          LEFT JOIN moz_bookmarks p ON p.id = b.parent
          LEFT JOIN moz_places h ON h.id = b.fk
@@ -1404,18 +1398,18 @@ async function queryBookmarks(info) {
 
 
 // Fetch implementation.
 
 async function fetchBookmark(info, concurrent) {
   let query = async function(db) {
     let rows = await db.executeCached(
       `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
-              b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-              b.id AS _id, b.parent AS _parentId,
+              b.dateAdded, b.lastModified, b.type, IFNULL(b.title, "") AS title,
+              h.url AS url, b.id AS _id, b.parent AS _parentId,
               (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
               p.parent AS _grandParentId, b.syncStatus AS _syncStatus
        FROM moz_bookmarks b
        LEFT JOIN moz_bookmarks p ON p.id = b.parent
        LEFT JOIN moz_places h ON h.id = b.fk
        WHERE b.guid = :guid
       `, { guid: info.guid });
 
@@ -1429,18 +1423,18 @@ async function fetchBookmark(info, concu
                                            query);
 }
 
 async function fetchBookmarkByPosition(info, concurrent) {
   let query = async function(db) {
     let index = info.index == Bookmarks.DEFAULT_INDEX ? null : info.index;
     let rows = await db.executeCached(
       `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
-              b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-              b.id AS _id, b.parent AS _parentId,
+              b.dateAdded, b.lastModified, b.type, IFNULL(b.title, "") AS title,
+              h.url AS url, b.id AS _id, b.parent AS _parentId,
               (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
               p.parent AS _grandParentId, b.syncStatus AS _syncStatus
        FROM moz_bookmarks b
        LEFT JOIN moz_bookmarks p ON p.id = b.parent
        LEFT JOIN moz_places h ON h.id = b.fk
        WHERE p.guid = :parentGuid
        AND b.position = IFNULL(:index, (SELECT count(*) - 1
                                         FROM moz_bookmarks
@@ -1458,18 +1452,18 @@ async function fetchBookmarkByPosition(i
 }
 
 async function fetchBookmarksByURL(info, concurrent) {
   let query = async function(db) {
     let tagsFolderId = await promiseTagsFolderId();
     let rows = await db.executeCached(
       `/* do not warn (bug no): not worth to add an index */
       SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
-              b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-              b.id AS _id, b.parent AS _parentId,
+              b.dateAdded, b.lastModified, b.type, IFNULL(b.title, "") AS title,
+              h.url AS url, b.id AS _id, b.parent AS _parentId,
               NULL AS _childCount, /* Unused for now */
               p.parent AS _grandParentId, b.syncStatus AS _syncStatus
       FROM moz_bookmarks b
       JOIN moz_bookmarks p ON p.id = b.parent
       JOIN moz_places h ON h.id = b.fk
       WHERE h.url_hash = hash(:url) AND h.url = :url
       AND _grandParentId <> :tagsFolderId
       ORDER BY b.lastModified DESC
@@ -1487,18 +1481,19 @@ async function fetchBookmarksByURL(info,
 }
 
 function fetchRecentBookmarks(numberOfItems) {
   return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchRecentBookmarks",
     async function(db) {
       let tagsFolderId = await promiseTagsFolderId();
       let rows = await db.executeCached(
         `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
-                b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-                NULL AS _id, NULL AS _parentId, NULL AS _childCount, NULL AS _grandParentId,
+                b.dateAdded, b.lastModified, b.type,
+                IFNULL(b.title, "") AS title, h.url AS url, NULL AS _id,
+                NULL AS _parentId, NULL AS _childCount, NULL AS _grandParentId,
                 NULL AS _syncStatus
         FROM moz_bookmarks b
         JOIN moz_bookmarks p ON p.id = b.parent
         JOIN moz_places h ON h.id = b.fk
         WHERE p.parent <> :tagsFolderId
         AND b.type = :type
         AND url_hash NOT BETWEEN hash("place", "prefix_lo")
                               AND hash("place", "prefix_hi")
@@ -1516,18 +1511,18 @@ function fetchRecentBookmarks(numberOfIt
 }
 
 function fetchBookmarksByParent(info) {
   return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarksByParent",
     async function(db) {
 
     let rows = await db.executeCached(
       `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
-              b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
-              b.id AS _id, b.parent AS _parentId,
+              b.dateAdded, b.lastModified, b.type, IFNULL(b.title, "") AS title,
+              h.url AS url, b.id AS _id, b.parent AS _parentId,
               (SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
               p.parent AS _grandParentId, b.syncStatus AS _syncStatus
        FROM moz_bookmarks b
        LEFT JOIN moz_bookmarks p ON p.id = b.parent
        LEFT JOIN moz_places h ON h.id = b.fk
        WHERE p.guid = :parentGuid
        ORDER BY b.position ASC
       `, { parentGuid: info.parentGuid });
@@ -1796,27 +1791,31 @@ function removeSameValueProperties(dest,
  *
  * @param rows
  *        the array of mozIStorageRow objects.
  * @return an array of bookmark objects.
  */
 function rowsToItemsArray(rows) {
   return rows.map(row => {
     let item = {};
-    for (let prop of ["guid", "index", "type"]) {
+    for (let prop of ["guid", "index", "type", "title"]) {
       item[prop] = row.getResultByName(prop);
     }
     for (let prop of ["dateAdded", "lastModified"]) {
       item[prop] = PlacesUtils.toDate(row.getResultByName(prop));
     }
-    for (let prop of ["title", "parentGuid", "url" ]) {
-      let val = row.getResultByName(prop);
-      if (val)
-        item[prop] = prop === "url" ? new URL(val) : val;
+    let parentGuid = row.getResultByName("parentGuid");
+    if (parentGuid) {
+      item.parentGuid = parentGuid;
     }
+    let url = row.getResultByName("url");
+    if (url) {
+      item.url = new URL(url);
+    }
+
     for (let prop of ["_id", "_parentId", "_childCount", "_grandParentId",
                       "_syncStatus"]) {
       let val = row.getResultByName(prop);
       if (val !== null) {
         // These properties should not be returned to the API consumer, thus
         // they are non-enumerable and removed through Object.assign just before
         // the object is returned.
         // Configurable is set to support mergeIntoNewObject overwrites.
@@ -1969,18 +1968,19 @@ async function(db, folderGuids, options)
          JOIN moz_bookmarks p ON b.parent = p.id
          WHERE p.guid = :folderGuid
          UNION ALL
          SELECT id FROM moz_bookmarks
          JOIN descendants ON parent = did
        )
        SELECT b.id AS _id, b.parent AS _parentId, b.position AS 'index',
               b.type, url, b.guid, p.guid AS parentGuid, b.dateAdded,
-              b.lastModified, b.title, p.parent AS _grandParentId,
-              NULL AS _childCount, b.syncStatus AS _syncStatus
+              b.lastModified, IFNULL(b.title, "") AS title,
+              p.parent AS _grandParentId, NULL AS _childCount,
+              b.syncStatus AS _syncStatus
        FROM descendants
        JOIN moz_bookmarks b ON did = b.id
        JOIN moz_bookmarks p ON p.id = b.parent
        LEFT JOIN moz_places h ON b.fk = h.id`, { folderGuid });
 
     itemsRemoved = itemsRemoved.concat(rowsToItemsArray(rows));
 
     await db.executeCached(
--- a/toolkit/components/places/Helpers.cpp
+++ b/toolkit/components/places/Helpers.cpp
@@ -287,16 +287,19 @@ IsValidGUID(const nsACString& aGUID)
     return false;
   }
   return true;
 }
 
 void
 TruncateTitle(const nsACString& aTitle, nsACString& aTrimmed)
 {
+  if (aTitle.IsVoid()) {
+    return;
+  }
   aTrimmed = aTitle;
   if (aTitle.Length() > TITLE_LENGTH_MAX) {
     aTrimmed = StringHead(aTitle, TITLE_LENGTH_MAX);
   }
 }
 
 PRTime
 RoundToMilliseconds(PRTime aTime) {
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -226,20 +226,23 @@ const BOOKMARK_VALIDATORS = Object.freez
                                  v >= PlacesUtils.bookmarks.DEFAULT_INDEX),
   dateAdded: simpleValidateFunc(v => v.constructor.name == "Date"),
   lastModified: simpleValidateFunc(v => v.constructor.name == "Date"),
   type: simpleValidateFunc(v => Number.isInteger(v) &&
                                 [ PlacesUtils.bookmarks.TYPE_BOOKMARK
                                 , PlacesUtils.bookmarks.TYPE_FOLDER
                                 , PlacesUtils.bookmarks.TYPE_SEPARATOR ].includes(v)),
   title: v => {
-    simpleValidateFunc(val => val === null || typeof(val) == "string").call(this, v);
-    if (!v)
-      return null;
-    return v.slice(0, DB_TITLE_LENGTH_MAX);
+    if (v === null) {
+      return "";
+    }
+    if (typeof(v) == "string") {
+      return v.slice(0, DB_TITLE_LENGTH_MAX);
+    }
+    throw new Error("Invalid title");
   },
   url: v => {
     simpleValidateFunc(val => (typeof(val) == "string" && val.length <= DB_URL_LENGTH_MAX) ||
                               (val instanceof Ci.nsIURI && val.spec.length <= DB_URL_LENGTH_MAX) ||
                               (val instanceof URL && val.href.length <= DB_URL_LENGTH_MAX)
                       ).call(this, v);
     if (typeof(v) === "string")
       return new URL(v);
@@ -1894,18 +1897,18 @@ this.PlacesUtils = {
          FROM moz_bookmarks b1 WHERE b1.guid=:item_guid
          UNION ALL
          SELECT b2.fk, level + 1, b2.type, b2.id, b2.guid, b2.parent,
                 descendants.guid, b2.position, b2.title, b2.dateAdded,
                 b2.lastModified
          FROM moz_bookmarks b2
          JOIN descendants ON b2.parent = descendants.id AND b2.id <> :tags_folder)
        SELECT d.level, d.id, d.guid, d.parent, d.parentGuid, d.type,
-              d.position AS [index], d.title, d.dateAdded, d.lastModified,
-              h.url, (SELECT icon_url FROM moz_icons i
+              d.position AS [index], IFNULL(d.title, "") AS title, d.dateAdded,
+              d.lastModified, h.url, (SELECT icon_url FROM moz_icons i
                       JOIN moz_icons_to_pages ON icon_id = i.id
                       JOIN moz_pages_w_icons pi ON page_id = pi.id
                       WHERE pi.page_url_hash = hash(h.url) AND pi.page_url = h.url
                       ORDER BY width DESC LIMIT 1) AS iconuri,
               (SELECT GROUP_CONCAT(t.title, ',')
                FROM moz_bookmarks b2
                JOIN moz_bookmarks t ON t.id = +b2.parent AND t.parent = :tags_folder
                WHERE b2.fk = h.id
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -466,18 +466,17 @@ nsNavBookmarks::InsertBookmarkInDB(int64
 
   rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_type"), aItemType);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("parent"), aParentId);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_index"), aIndex);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  // Support NULL titles.
-  if (aTitle.IsVoid())
+  if (aTitle.IsEmpty())
     rv = stmt->BindNullByName(NS_LITERAL_CSTRING("item_title"));
   else
     rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"), aTitle);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("date_added"), aDateAdded);
   NS_ENSURE_SUCCESS(rv, rv);
 
@@ -550,20 +549,17 @@ nsNavBookmarks::InsertBookmarkInDB(int64
   // Mark all affected separators as changed
   rv = AdjustSeparatorsSyncCounter(aParentId, aIndex + 1, syncChangeDelta);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Add a cache entry since we know everything about this bookmark.
   BookmarkData bookmark;
   bookmark.id = *_itemId;
   bookmark.guid.Assign(_guid);
-  if (aTitle.IsVoid()) {
-    bookmark.title.SetIsVoid(true);
-  }
-  else {
+  if (!aTitle.IsEmpty()) {
     bookmark.title.Assign(aTitle);
   }
   bookmark.position = aIndex;
   bookmark.placeId = aPlaceId;
   bookmark.parentId = aParentId;
   bookmark.type = aItemType;
   bookmark.dateAdded = aDateAdded;
   if (aLastModified)
@@ -933,31 +929,30 @@ nsNavBookmarks::InsertSeparator(int64_t 
   else {
     index = aIndex;
     // Create space for the insertion.
     rv = AdjustIndices(aParent, index, INT32_MAX, 1);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
   *aNewItemId = -1;
-  // Set a NULL title rather than an empty string.
   nsAutoCString guid(aGUID);
   PRTime dateAdded = RoundedPRNow();
-  rv = InsertBookmarkInDB(-1, SEPARATOR, aParent, index, NullCString(), dateAdded,
+  rv = InsertBookmarkInDB(-1, SEPARATOR, aParent, index, EmptyCString(), dateAdded,
                           0, folderGuid, grandParentId, nullptr, aSource,
                           aNewItemId, guid);
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
   NOTIFY_BOOKMARKS_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                              DontSkip,
                              OnItemAdded(*aNewItemId, aParent, index, TYPE_SEPARATOR,
-                                         nullptr, NullCString(), dateAdded, guid,
+                                         nullptr, EmptyCString(), dateAdded, guid,
                                          folderGuid, aSource));
 
   return NS_OK;
 }
 
 
 nsresult
 nsNavBookmarks::GetLastChildId(int64_t aFolderId, int64_t* aItemId)
@@ -1529,23 +1524,21 @@ nsNavBookmarks::FetchItemInfo(int64_t aI
   NS_ENSURE_SUCCESS(rv, rv);
   if (!hasResult) {
     return NS_ERROR_INVALID_ARG;
   }
 
   _bookmark.id = aItemId;
   rv = stmt->GetUTF8String(1, _bookmark.url);
   NS_ENSURE_SUCCESS(rv, rv);
+
   bool isNull;
   rv = stmt->GetIsNull(2, &isNull);
   NS_ENSURE_SUCCESS(rv, rv);
-  if (isNull) {
-    _bookmark.title.SetIsVoid(true);
-  }
-  else {
+  if (!isNull) {
     rv = stmt->GetUTF8String(2, _bookmark.title);
     NS_ENSURE_SUCCESS(rv, rv);
   }
   rv = stmt->GetInt32(3, &_bookmark.position);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = stmt->GetInt64(4, &_bookmark.placeId);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = stmt->GetInt64(5, &_bookmark.parentId);
@@ -1978,17 +1971,17 @@ nsNavBookmarks::SetItemTitle(int64_t aIt
   TruncateTitle(aTitle, title);
 
   if (isChangingTagFolder) {
     // If we're changing the title of a tag folder, bump the change counter
     // for all tagged bookmarks. We use a separate code path to avoid a
     // transaction for non-tags.
     mozStorageTransaction transaction(mDB->MainConn(), false);
 
-    rv = SetItemTitleInternal(bookmark, aTitle, syncChangeDelta);
+    rv = SetItemTitleInternal(bookmark, title, syncChangeDelta);
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = AddSyncChangesForBookmarksInFolder(bookmark.id, syncChangeDelta);
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = transaction.Commit();
     NS_ENSURE_SUCCESS(rv, rv);
   } else {
@@ -2022,19 +2015,18 @@ nsNavBookmarks::SetItemTitleInternal(Boo
     "UPDATE moz_bookmarks SET "
      "title = :item_title, lastModified = :date, "
      "syncChangeCounter = syncChangeCounter + :delta "
     "WHERE id = :item_id"
   );
   NS_ENSURE_STATE(statement);
   mozStorageStatementScoper scoper(statement);
 
-  // Support setting a null title, we support this in insertBookmark.
   nsresult rv;
-  if (aTitle.IsVoid()) {
+  if (aTitle.IsEmpty()) {
     rv = statement->BindNullByName(NS_LITERAL_CSTRING("item_title"));
   }
   else {
     rv = statement->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"),
                                          aTitle);
   }
   NS_ENSURE_SUCCESS(rv, rv);
   aBookmark.lastModified = RoundToMilliseconds(RoundedPRNow());
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -3793,18 +3793,23 @@ nsNavHistory::RowToResult(mozIStorageVal
 
   // URL
   nsAutoCString url;
   nsresult rv = aRow->GetUTF8String(kGetInfoIndex_URL, url);
   NS_ENSURE_SUCCESS(rv, rv);
 
   // title
   nsAutoCString title;
-  rv = aRow->GetUTF8String(kGetInfoIndex_Title, title);
+  bool isNull;
+  rv = aRow->GetIsNull(kGetInfoIndex_Title, &isNull);
   NS_ENSURE_SUCCESS(rv, rv);
+  if (!isNull) {
+    rv = aRow->GetUTF8String(kGetInfoIndex_Title, title);
+    NS_ENSURE_SUCCESS(rv, rv);
+  }
 
   uint32_t accessCount = aRow->AsInt32(kGetInfoIndex_VisitCount);
   PRTime time = aRow->AsInt64(kGetInfoIndex_VisitDate);
 
   // itemId
   int64_t itemId = aRow->AsInt64(kGetInfoIndex_ItemId);
   int64_t parentId = -1;
   if (itemId == 0) {
@@ -3969,19 +3974,19 @@ nsNavHistory::QueryRowToResult(int64_t i
         // At this point the node is set up like a regular folder node. Here
         // we make the necessary change to make it a folder shortcut.
         resultNode->GetAsFolder()->mTargetFolderItemId = targetFolderId;
         resultNode->mItemId = itemId;
         nsAutoCString targetFolderGuid(resultNode->GetAsFolder()->mBookmarkGuid);
         resultNode->mBookmarkGuid = aBookmarkGuid;
         resultNode->GetAsFolder()->mTargetFolderGuid = targetFolderGuid;
 
-        // Use the query item title, unless it's void (in that case use the
+        // Use the query item title, unless it's empty (in that case use the
         // concrete folder title).
-        if (!aTitle.IsVoid()) {
+        if (!aTitle.IsEmpty()) {
           resultNode->mTitle = aTitle;
         }
       }
     }
     else {
       // This is a regular query.
       resultNode = new nsNavHistoryQueryResultNode(aTitle, aTime, queries, options);
       resultNode->mItemId = itemId;
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js
@@ -117,17 +117,17 @@ add_task(async function fetch_bookmar_em
                                                  title: "" });
   checkBookmarkObject(bm1);
 
   let bm2 = await PlacesUtils.bookmarks.fetch(bm1.guid);
   checkBookmarkObject(bm2);
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.index, 0);
-  Assert.ok(!("title" in bm2));
+  Assert.strictEqual(bm2.title, "");
 
   await PlacesUtils.bookmarks.remove(bm1.guid);
 });
 
 add_task(async function fetch_folder() {
   let bm1 = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                  title: "a folder" });
@@ -153,17 +153,17 @@ add_task(async function fetch_folder_emp
                                                  title: "" });
   checkBookmarkObject(bm1);
 
   let bm2 = await PlacesUtils.bookmarks.fetch(bm1.guid);
   checkBookmarkObject(bm2);
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.index, 0);
-  Assert.ok(!("title" in bm2));
+  Assert.strictEqual(bm2.title, "");
 
   await PlacesUtils.bookmarks.remove(bm1.guid);
 });
 
 add_task(async function fetch_separator() {
   let bm1 = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
   checkBookmarkObject(bm1);
@@ -172,17 +172,17 @@ add_task(async function fetch_separator(
   checkBookmarkObject(bm2);
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm2.index, 0);
   Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
   Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
   Assert.ok(!("url" in bm2));
-  Assert.ok(!("title" in bm2));
+  Assert.strictEqual(bm2.title, "");
 
   await PlacesUtils.bookmarks.remove(bm1.guid);
 });
 
 add_task(async function fetch_byposition_nonexisting_parentGuid() {
   let bm = await PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012",
                                                index: 0 },
                                              gAccumulator.callback);
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js
@@ -112,17 +112,17 @@ add_task(async function create_separator
   let bm = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
                                                 index: PlacesUtils.bookmarks.DEFAULT_INDEX });
   checkBookmarkObject(bm);
   Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm.index, 1);
   Assert.equal(bm.dateAdded, bm.lastModified);
   Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
-  Assert.ok(!("title" in bm), "title should not be set");
+  Assert.strictEqual(bm.title, "");
 });
 
 add_task(async function create_separator_w_title_fail() {
   try {
     await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                          type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
                                          title: "a separator" });
     Assert.ok(false, "Trying to set title for a separator should reject");
@@ -144,17 +144,17 @@ add_task(async function create_separator
                                                 index: PlacesUtils.bookmarks.DEFAULT_INDEX,
                                                 guid: "123456789012" });
   checkBookmarkObject(bm);
   Assert.equal(bm.guid, "123456789012");
   Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm.index, 2);
   Assert.equal(bm.dateAdded, bm.lastModified);
   Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
-  Assert.ok(!("title" in bm), "title should not be set");
+  Assert.strictEqual(bm.title, "");
 });
 
 add_task(async function create_item_given_guid_no_type_fail() {
   try {
     await PlacesUtils.bookmarks.insert({ parentGuid: "123456789012" });
     Assert.ok(false, "Trying to create an item with a given guid but no type should reject");
   } catch (ex) {}
 });
@@ -163,17 +163,17 @@ add_task(async function create_separator
   let bm = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
                                                 index: 9999 });
   checkBookmarkObject(bm);
   Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm.index, 3);
   Assert.equal(bm.dateAdded, bm.lastModified);
   Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
-  Assert.ok(!("title" in bm), "title should not be set");
+  Assert.strictEqual(bm.title, "");
 });
 
 add_task(async function create_separator_given_dateAdded() {
   let time = new Date();
   let past = new Date(time - 86400000);
   let bm = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
                                                 dateAdded: past });
@@ -184,17 +184,17 @@ add_task(async function create_separator
 
 add_task(async function create_folder() {
   let bm = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 type: PlacesUtils.bookmarks.TYPE_FOLDER });
   checkBookmarkObject(bm);
   Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm.dateAdded, bm.lastModified);
   Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_FOLDER);
-  Assert.ok(!("title" in bm), "title should not be set");
+  Assert.strictEqual(bm.title, "");
 
   // And then create a nested folder.
   let parentGuid = bm.guid;
   bm = await PlacesUtils.bookmarks.insert({ parentGuid,
                                             type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                             title: "a folder" });
   checkBookmarkObject(bm);
   Assert.equal(bm.parentGuid, parentGuid);
@@ -226,17 +226,17 @@ add_task(async function create_bookmark(
   bm = await PlacesUtils.bookmarks.insert({ parentGuid,
                                             type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                             url: new URL("http://example.com/") });
   checkBookmarkObject(bm);
   Assert.equal(bm.parentGuid, parentGuid);
   Assert.equal(bm.index, 1);
   Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK);
   Assert.equal(bm.url.href, "http://example.com/");
-  Assert.ok(!("title" in bm), "title should not be set");
+  Assert.strictEqual(bm.title, "");
 });
 
 add_task(async function create_bookmark_frecency() {
   let bm = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 url: "http://example.com/",
                                                 title: "a bookmark" });
   checkBookmarkObject(bm);
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_insertTree.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_insertTree.js
@@ -124,17 +124,17 @@ add_task(async function create_separator
   let [bm] = await PlacesUtils.bookmarks.insertTree({children: [{
     type: PlacesUtils.bookmarks.TYPE_SEPARATOR
   }], guid: PlacesUtils.bookmarks.unfiledGuid});
   checkBookmarkObject(bm);
   Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm.index, 0);
   Assert.equal(bm.dateAdded, bm.lastModified);
   Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
-  Assert.ok(!("title" in bm), "title should not be set");
+  Assert.strictEqual(bm.title, "");
 });
 
 add_task(async function create_plain_bm() {
   let [bm] = await PlacesUtils.bookmarks.insertTree({children: [{
     url: "http://www.example.com/",
     title: "Test"
   }], guid: PlacesUtils.bookmarks.unfiledGuid});
   checkBookmarkObject(bm);
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
@@ -4,17 +4,17 @@
 add_task(async function insert_separator_notification() {
   let observer = expectNotifications();
   let bm = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid});
   let itemId = await PlacesUtils.promiseItemId(bm.guid);
   let parentId = await PlacesUtils.promiseItemId(bm.parentGuid);
   observer.check([ { name: "onItemAdded",
                      arguments: [ itemId, parentId, bm.index, bm.type,
-                                  null, null, bm.dateAdded * 1000,
+                                  null, "", bm.dateAdded * 1000,
                                   bm.guid, bm.parentGuid,
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                  ]);
 });
 
 add_task(async function insert_folder_notification() {
   let observer = expectNotifications();
   let bm = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
@@ -29,21 +29,22 @@ add_task(async function insert_folder_no
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                  ]);
 });
 
 add_task(async function insert_folder_notitle_notification() {
   let observer = expectNotifications();
   let bm = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+  strictEqual(bm.title, "", "Should return empty string for untitled folder");
   let itemId = await PlacesUtils.promiseItemId(bm.guid);
   let parentId = await PlacesUtils.promiseItemId(bm.parentGuid);
   observer.check([ { name: "onItemAdded",
                      arguments: [ itemId, parentId, bm.index, bm.type,
-                                  null, null, bm.dateAdded * 1000,
+                                  null, "", bm.dateAdded * 1000,
                                   bm.guid, bm.parentGuid,
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                  ]);
 });
 
 add_task(async function insert_bookmark_notification() {
   let observer = expectNotifications();
   let bm = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
@@ -60,21 +61,22 @@ add_task(async function insert_bookmark_
                  ]);
 });
 
 add_task(async function insert_bookmark_notitle_notification() {
   let observer = expectNotifications();
   let bm = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 url: new URL("http://example.com/") });
+  strictEqual(bm.title, "", "Should return empty string for untitled bookmark");
   let itemId = await PlacesUtils.promiseItemId(bm.guid);
   let parentId = await PlacesUtils.promiseItemId(bm.parentGuid);
   observer.check([ { name: "onItemAdded",
                      arguments: [ itemId, parentId, bm.index, bm.type,
-                                  bm.url, null, bm.dateAdded * 1000,
+                                  bm.url, "", bm.dateAdded * 1000,
                                   bm.guid, bm.parentGuid,
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                  ]);
 });
 
 add_task(async function insert_bookmark_tag_notification() {
   let bm = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 parentGuid: PlacesUtils.bookmarks.unfiledGuid,
@@ -89,17 +91,17 @@ add_task(async function insert_bookmark_
   let tag = await PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                  parentGuid: tagFolder.guid,
                                                  url: new URL("http://tag.example.com/") });
   let tagId = await PlacesUtils.promiseItemId(tag.guid);
   let tagParentId = await PlacesUtils.promiseItemId(tag.parentGuid);
 
   observer.check([ { name: "onItemAdded",
                      arguments: [ tagId, tagParentId, tag.index, tag.type,
-                                  tag.url, null, tag.dateAdded * 1000,
+                                  tag.url, "", tag.dateAdded * 1000,
                                   tag.guid, tag.parentGuid,
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
                    { name: "onItemChanged",
                      arguments: [ itemId, "tags", false, "",
                                   bm.lastModified * 1000, bm.type, parentId,
                                   bm.guid, bm.parentGuid, "",
                                   Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }
                  ]);
@@ -485,33 +487,79 @@ add_task(async function reorder_notifica
                                               child.parentGuid,
                                               child.parentGuid,
                                               Ci.nsINavBookmarksService.SOURCE_DEFAULT
                                             ] });
   }
   observer.check(expectedNotifications);
 });
 
+add_task(async function update_notitle_notification() {
+  let toolbarBmURI = Services.io.newURI("https://example.com");
+  let toolbarBmId =
+    PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId,
+                                         toolbarBmURI, 0, "Bookmark");
+  let toolbarBmGuid = await PlacesUtils.promiseItemGuid(toolbarBmId);
+
+  let menuFolder = await PlacesUtils.bookmarks.insert({
+    parentGuid: PlacesUtils.bookmarks.menuGuid,
+    type: PlacesUtils.bookmarks.TYPE_FOLDER,
+    index: 0,
+    title: "Folder"
+  });
+  let menuFolderId = await PlacesUtils.promiseItemId(menuFolder.guid);
+
+  let observer = expectNotifications();
+
+  PlacesUtils.bookmarks.setItemTitle(toolbarBmId, null);
+  strictEqual(PlacesUtils.bookmarks.getItemTitle(toolbarBmId), "",
+    "Legacy API should return empty string for untitled bookmark");
+
+  let updatedMenuBm = await PlacesUtils.bookmarks.update({
+    guid: menuFolder.guid,
+    title: null,
+  });
+  strictEqual(updatedMenuBm.title, "",
+    "Async API should return empty string for untitled bookmark");
+
+  let toolbarBmModified =
+    PlacesUtils.toDate(PlacesUtils.bookmarks.getItemLastModified(toolbarBmId));
+  observer.check([{
+    name: "onItemChanged",
+    arguments: [toolbarBmId, "title", false, "", toolbarBmModified * 1000,
+                PlacesUtils.bookmarks.TYPE_BOOKMARK,
+                PlacesUtils.toolbarFolderId, toolbarBmGuid,
+                PlacesUtils.bookmarks.toolbarGuid,
+                "", PlacesUtils.bookmarks.SOURCES.DEFAULT],
+  }, {
+    name: "onItemChanged",
+    arguments: [menuFolderId, "title", false, "",
+                updatedMenuBm.lastModified * 1000,
+                PlacesUtils.bookmarks.TYPE_FOLDER,
+                PlacesUtils.bookmarksMenuFolderId, menuFolder.guid,
+                PlacesUtils.bookmarks.menuGuid,
+                "", PlacesUtils.bookmarks.SOURCES.DEFAULT],
+  }]);
+});
+
 function expectNotifications() {
   let notifications = [];
   let observer = new Proxy(NavBookmarkObserver, {
     get(target, name) {
       if (name == "check") {
         PlacesUtils.bookmarks.removeObserver(observer);
         return expectedNotifications =>
           Assert.deepEqual(notifications, expectedNotifications);
       }
 
       if (name.startsWith("onItem")) {
         return (...origArgs) => {
           let args = Array.from(origArgs, arg => {
             if (arg && arg instanceof Ci.nsIURI)
               return new URL(arg.spec);
-            if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000)
-              return new Date(parseInt(arg / 1000));
             return arg;
           });
           notifications.push({ name, arguments: args });
         }
       }
 
       if (name in target)
         return target[name];
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
@@ -116,17 +116,17 @@ add_task(async function remove_bookmark_
                                                  title: "" });
   checkBookmarkObject(bm1);
 
   let bm2 = await PlacesUtils.bookmarks.remove(bm1.guid);
   checkBookmarkObject(bm2);
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.index, 0);
-  Assert.ok(!("title" in bm2));
+  Assert.strictEqual(bm2.title, "");
 });
 
 add_task(async function remove_folder() {
   let bm1 = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                  title: "a folder" });
   checkBookmarkObject(bm1);
 
@@ -163,34 +163,34 @@ add_task(async function remove_folder_em
                                                  title: "" });
   checkBookmarkObject(bm1);
 
   let bm2 = await PlacesUtils.bookmarks.remove(bm1.guid);
   checkBookmarkObject(bm2);
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.index, 0);
-  Assert.ok(!("title" in bm2));
+  Assert.strictEqual(bm2.title, "");
 });
 
 add_task(async function remove_separator() {
   let bm1 = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
   checkBookmarkObject(bm1);
 
   let bm2 = await PlacesUtils.bookmarks.remove(bm1.guid);
   checkBookmarkObject(bm2);
 
   Assert.deepEqual(bm1, bm2);
   Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
   Assert.equal(bm2.index, 0);
   Assert.deepEqual(bm2.dateAdded, bm2.lastModified);
   Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_SEPARATOR);
   Assert.ok(!("url" in bm2));
-  Assert.ok(!("title" in bm2));
+  Assert.strictEqual(bm2.title, "");
 });
 
 add_task(async function test_nested_content_fails_when_not_allowed() {
   let folder1 = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                      type: PlacesUtils.bookmarks.TYPE_FOLDER,
                                                      title: "a folder" });
   await PlacesUtils.bookmarks.insert({ parentGuid: folder1.guid,
                                        type: PlacesUtils.bookmarks.TYPE_FOLDER,
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_update.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_update.js
@@ -189,20 +189,20 @@ add_task(async function update_lastModif
   Assert.deepEqual(bm.lastModified, yesterday);
 
   bm = await PlacesUtils.bookmarks.update({ guid: bm.guid,
                                             title: "title2" });
   Assert.ok(bm.lastModified >= time);
 
   bm = await PlacesUtils.bookmarks.update({ guid: bm.guid,
                                             title: "" });
-  Assert.ok(!("title" in bm));
+  Assert.strictEqual(bm.title, "");
 
   bm = await PlacesUtils.bookmarks.fetch(bm.guid);
-  Assert.ok(!("title" in bm));
+  Assert.strictEqual(bm.title, "");
 });
 
 add_task(async function update_url() {
   let bm = await PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                 type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
                                                 url: "http://example.com/",
                                                 title: "title" });
   checkBookmarkObject(bm);
--- a/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
+++ b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js
@@ -159,17 +159,17 @@ add_task(async function onItemAdded_sepa
     gBookmarksObserver.setup([
       { name: "onItemAdded",
         args: [
           { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
           { name: "parentId", check: v => v === PlacesUtils.unfiledBookmarksFolderId },
           { name: "index", check: v => v === 1 },
           { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR },
           { name: "uri", check: v => v === null },
-          { name: "title", check: v => v === null },
+          { name: "title", check: v => v === "" },
           { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
           { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
           { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
   ])]);
   PlacesUtils.bookmarks.insertSeparator(PlacesUtils.unfiledBookmarksFolderId,
                                         PlacesUtils.bookmarks.DEFAULT_INDEX);
@@ -254,17 +254,17 @@ add_task(async function onItemChanged_ta
         ] },
       { name: "onItemAdded", // This is the tag.
         args: [
           { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
           { name: "parentId", check: v => typeof(v) == "number" && v > 0 },
           { name: "index", check: v => v === 0 },
           { name: "itemType", check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK },
           { name: "uri", check: v => v instanceof Ci.nsIURI && v.equals(uri) },
-          { name: "title", check: v => v === null },
+          { name: "title", check: v => v === "" },
           { name: "dateAdded", check: v => typeof(v) == "number" && v > 0 },
           { name: "guid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
           { name: "parentGuid", check: v => typeof(v) == "string" && GUID_RE.test(v) },
           { name: "source", check: v => Object.values(PlacesUtils.bookmarks.SOURCES).includes(v) },
         ] },
       { name: "onItemChanged",
         args: [
           { name: "itemId", check: v => typeof(v) == "number" && v > 0 },
--- a/toolkit/components/places/tests/queries/test_onlyBookmarked.js
+++ b/toolkit/components/places/tests/queries/test_onlyBookmarked.js
@@ -15,30 +15,33 @@
  * results that are in the query set at the top of the testData list, and those
  * results MUST be in the same sort order as the items in the resulting query.
  */
 
 var testData = [
   // Add a bookmark that should be in the results
   { isBookmark: true,
     uri: "http://bookmarked.com/",
+    title: "",
     parentGuid: PlacesUtils.bookmarks.toolbarGuid,
     index: PlacesUtils.bookmarks.DEFAULT_INDEX,
     isInQuery: true },
 
   // Add a bookmark that should not be in the results
   { isBookmark: true,
     uri: "http://bookmarked-elsewhere.com/",
+    title: "",
     parentGuid: PlacesUtils.bookmarks.menuGuid,
     index: PlacesUtils.bookmarks.DEFAULT_INDEX,
     isInQuery: false },
 
   // Add an un-bookmarked visit
   { isVisit: true,
     uri: "http://notbookmarked.com/",
+    title: "",
     isInQuery: false }
 ];
 
 
 /**
  * run_test is where the magic happens.  This is automatically run by the test
  * harness.  It is where you do the work of creating the query, running it, and
  * playing with the result set.
@@ -72,23 +75,25 @@ add_task(async function test_onlyBookmar
   compareArrayToResult(testData, root);
   do_print("end first test");
 
   // Test live-update
   var liveUpdateTestData = [
     // Add a bookmark that should show up
     { isBookmark: true,
       uri: "http://bookmarked2.com/",
+      title: "",
       parentGuid: PlacesUtils.bookmarks.toolbarGuid,
       index: PlacesUtils.bookmarks.DEFAULT_INDEX,
       isInQuery: true },
 
     // Add a bookmark that should not show up
     { isBookmark: true,
       uri: "http://bookmarked-elsewhere2.com/",
+      title: "",
       parentGuid: PlacesUtils.bookmarks.menuGuid,
       index: PlacesUtils.bookmarks.DEFAULT_INDEX,
       isInQuery: false }
   ];
 
   await task_populateDB(liveUpdateTestData); // add to the db
 
   // add to the test data
--- a/toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js
+++ b/toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js
@@ -1,16 +1,17 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
 /**
- * Both SetItemtitle and insertBookmark should allow for null titles.
+ * Both SetItemtitle and insertBookmark should default to the empty string
+ * for null titles.
  */
 
 const bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
            getService(Ci.nsINavBookmarksService);
 
 const TEST_URL = "http://www.mozilla.org";
 
 function run_test() {
@@ -18,27 +19,27 @@ function run_test() {
   var itemId = bs.insertBookmark(bs.toolbarFolder,
                                  uri(TEST_URL),
                                  bs.DEFAULT_INDEX,
                                  "");
   // Check returned title is an empty string.
   do_check_eq(bs.getItemTitle(itemId), "");
   // Set title to null.
   bs.setItemTitle(itemId, null);
-  // Check returned title is null.
-  do_check_eq(bs.getItemTitle(itemId), null);
+  // Check returned title defaults to an empty string.
+  do_check_eq(bs.getItemTitle(itemId), "");
   // Cleanup.
   bs.removeItem(itemId);
 
   // Insert a bookmark with a null title.
   itemId = bs.insertBookmark(bs.toolbarFolder,
                              uri(TEST_URL),
                              bs.DEFAULT_INDEX,
                              null);
-  // Check returned title is null.
-  do_check_eq(bs.getItemTitle(itemId), null);
+  // Check returned title defaults to an empty string.
+  do_check_eq(bs.getItemTitle(itemId), "");
   // Set title to an empty string.
   bs.setItemTitle(itemId, "");
   // Check returned title is an empty string.
   do_check_eq(bs.getItemTitle(itemId), "");
   // Cleanup.
   bs.removeItem(itemId);
 }