Merge lp:~jamesh/bindwood/migration into lp:bindwood

Proposed by James Henstridge on 2011-03-04
Status: Merged
Approved by: James Henstridge on 2011-03-08
Approved revision: 41
Merged at revision: 40
Proposed branch: lp:~jamesh/bindwood/migration
Merge into: lp:bindwood
Prerequisite: lp:~jamesh/bindwood/switch-to-new-synchroniser
Diff against target: 1134 lines (+736/-112)
9 files modified
modules/bindwood.jsm (+0/-111)
modules/couch.jsm (+2/-1)
modules/migration.jsm (+248/-0)
modules/sync.jsm (+18/-0)
mozmill/shared-modules/bookmarks.js (+7/-0)
mozmill/tests/test_migration.js (+425/-0)
mozmill/tests/test_sync_all.js (+14/-0)
mozmill/tests/test_sync_from_couch.js (+17/-0)
mozmill/tests/test_sync_to_couch.js (+5/-0)
To merge this branch: bzr merge lp:~jamesh/bindwood/migration
Reviewer Review Type Date Requested Status
Eric Casteleijn (community) Approve on 2011-03-08
Manuel de la Peña (community) 2011-03-04 Approve on 2011-03-08
Review via email: mp+52193@code.launchpad.net

Commit message

Migration code to upgrade databases from Bindwood 0.4.x and 1.0.x schemas.

Description of the change

Add code to migrate old Bindwood bookmarks over to the new schema.

We determine the schema version like so:
 * If there is no root_$profile document, assume we're using the
   0.4.x schema (which I call version 0).
 * If there is a root_$profile document but has no record_type_version,
   assume we're using the 1.0.x schema (which I call version 1).
 * If there is a root_$profile document and it has record_type_version,
   assume it is equal to the schema version.

This gives us a way to detect future formats without much trouble.

I don't have code to convert from version 0, but instead rely on the ability to rebuild the database from Firefox's local places database.

I've also added a few attributes that will make it easier to implement a duplicate detection algorithm:
 * parent_title: the title of the parent folder (combine this with
   other bookmark data to detect duplicates).
 * position: the numeric index of a separator (provides a way to
   distinguish different separators in a folder).

To post a comment you must log in.
review: Approve
Eric Casteleijn (thisfred) wrote :

Looks good, tests pass.

review: Approve
Ubuntu One Auto Pilot (otto-pilot) wrote :

The prerequisite lp:~jamesh/bindwood/switch-to-new-synchroniser has not yet been merged into lp:bindwood.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'modules/bindwood.jsm'
2--- modules/bindwood.jsm 2011-03-04 13:02:54 +0000
3+++ modules/bindwood.jsm 2011-03-04 13:02:54 +0000
4@@ -169,115 +169,4 @@
5 Log.exception("Problem setting up repeater.", e);
6 }
7 },
8-
9- migrateOlderBookmarkRecords: function() {
10- Log.debug(
11- "We're an older client. " +
12- "Let's migrate the remote records and re-sync.");
13-
14- var additional = [];
15- var all_docs = Bindwood.couch.view("bookmarks/profile",
16- {
17- startkey: Bindwood.currentProfile,
18- endkey: Bindwood.currentProfile
19- });
20- var rows = all_docs.rows;
21-
22- // Pull all records from Couch, and for each:
23-
24- for (var i = 0; i < rows.length; i++) {
25- var row = rows[i];
26- Log.debug("Got a row: " + row);
27- var id = row.id;
28- Log.debug("Got an id: " + id);
29- var doc = Bindwood.couch.open(id);
30- Log.debug("Got a doc: " + doc);
31-
32- if (doc.record_type_version >= 1) {
33- Log.debug(
34- "Record is already migrated Skipping...");
35- continue;
36- }
37-
38- // get the uuid off the bookmark.
39- var old_uuid = doc.application_annotations.Firefox.uuid;
40- Log.debug(
41- "Got the old uuid from the record: " + old_uuid);
42- // look up itemId by uuid
43- // XXX: This probably needs to be made more robust
44- var itemId = Bindwood.itemIdForUUID(old_uuid);
45- Log.debug(
46- "Found its local itemID from the map: " + itemId);
47- // annotate the itemId with the document's _id
48- Bindwood.annotateItemWithUUID(itemId, id);
49- Log.debug(
50- "Annotating the item's uuid with the document's actual id");
51- // delete the uuid field off the record
52- delete doc.application_annotations.Firefox.uuid;
53- Log.debug("Deleted the document's uuid annotation");
54- // delete the folder field off the record
55- delete doc.application_annotations.Firefox.folder;
56- Log.debug("Deleted the document's folder annotation");
57-
58- if (doc.deleted) {
59- Log.debug(
60- "The document was flagged as deleted, cleaning up.");
61- // swap .deleted for conventional .deleted
62- if (!doc.application_annotations) {
63- doc.application_annotations = {};
64- }
65- if (!doc.application_annotations['Ubuntu One']) {
66- doc.application_annotations['Ubuntu One'] = {};
67- }
68- if (!doc.application_annotations['Ubuntu One'].private_application_annotations) {
69- doc.application_annotations['Ubuntu One'].private_application_annotations = {};
70- }
71- doc.application_annotations['Ubuntu One'].private_application_annotations.deleted = true;
72- Log.debug(
73- "Moved deleted flag into private application annotations.");
74- delete doc.deleted;
75- Log.debug("Deleted the top-level .deleted flag.");
76- }
77-
78- // update the document's record_type_version
79- doc.record_type_version = 1;
80- Log.debug("Set the schema version to 1");
81-
82- // Ensure we're dealing with the proper record type on Migrate.
83- doc = Bindwood.decorateRecordByType(doc, itemId);
84-
85- // add to additional
86- additional.push(doc);
87- Log.debug(
88- "Adding this doc to the stack of addition docs to push back.");
89- }
90-
91- for (var i = 0; i < additional.length; i++) {
92- var doc = additional[i];
93- Log.debug(
94- "Preparing to push back " + doc.title || doc.record_type);
95- // Ensure that any remote folders have their .children
96- // populated, and in particular make sure that we've already
97- // modified their children's annotations/uuids
98- if (doc.record_type == Bindwood.TYPE_FOLDER) {
99- // XXX: This probably needs to be made more robust
100- var itemId = Bindwood.itemIdForUUID(doc._id);
101- doc.children = Bindwood.getUUIDsFromFolder(itemId);
102- Log.debug(
103- "Folder needed updating, calculated children: " +
104- doc.children);
105- }
106-
107- try {
108- var response = Bindwood.couch.save(doc);
109- // We can avoid having to process this revision when we
110- // pull it later
111- Bindwood.seen_revisions[response.rev] = true;
112- } catch(e) {
113- Log.exception(
114- "Problem saving record to CouchDB; record is " +
115- JSON.stringify(doc + ": ", e));
116- }
117- }
118- },
119 };
120
121=== modified file 'modules/couch.jsm'
122--- modules/couch.jsm 2011-02-04 06:03:15 +0000
123+++ modules/couch.jsm 2011-03-04 13:02:54 +0000
124@@ -100,7 +100,7 @@
125 if (docs[i]._id == undefined)
126 newCount++;
127 }
128- var newUuids = CouchDB.newUuids(docs.length);
129+ var newUuids = CouchDB.newUuids(newCount);
130 var newCount = 0;
131 for (var i=0; i<docs.length; i++) {
132 if (docs[i]._id == undefined)
133@@ -112,6 +112,7 @@
134 json[option] = options[option];
135 }
136 this.last_req = this.request("POST", this.uri + "_bulk_docs", {
137+ headers: {"Content-Type": "application/json"},
138 body: JSON.stringify(json)
139 });
140 if (this.last_req.status == 417) {
141
142=== added file 'modules/migration.jsm'
143--- modules/migration.jsm 1970-01-01 00:00:00 +0000
144+++ modules/migration.jsm 2011-03-04 13:02:54 +0000
145@@ -0,0 +1,248 @@
146+/*
147+ * Copyright 2009-2011 Canonical Ltd.
148+ *
149+ * This program is free software: you can redistribute it and/or modify it
150+ * under the terms of the GNU General Public License version 3, as published
151+ * by the Free Software Foundation.
152+ *
153+ * This program is distributed in the hope that it will be useful, but
154+ * WITHOUT ANY WARRANTY; without even the implied warranties of
155+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
156+ * PURPOSE. See the GNU General Public License for more details.
157+ *
158+ * You should have received a copy of the GNU General Public License along
159+ * with this program. If not, see <http://www.gnu.org/licenses/>.
160+ */
161+
162+const EXPORTED_SYMBOLS = ['SchemaMigration'];
163+
164+const Cu = Components.utils;
165+
166+Cu.import("resource://bindwood/logging.jsm");
167+
168+
169+function SchemaMigration(couch, profile) {
170+ this.couch = couch;
171+ this.profile = profile;
172+}
173+
174+SchemaMigration.prototype = {
175+
176+ get_schema_version: function() {
177+ var doc = this.couch.open('root_' + this.profile);
178+
179+ if (!doc) {
180+ // If there is no root document, we're dealing with the
181+ // original Bindwood datbase schema.
182+ return 0;
183+ }
184+ // If the root document does not have a record_type_version,
185+ // assume it is the second schema version.
186+ if (!doc.record_type_version) {
187+ return 1;
188+ }
189+ // Otherwise, use the value of record_type_version.
190+ return doc.record_type_version;
191+ },
192+
193+ upgrade: function() {
194+ var schema_version = this.get_schema_version();
195+ Log.debug("Current schema version is " + schema_version);
196+ switch (schema_version) {
197+ case 0:
198+ // Haven't written migration code for this format, so just
199+ // wipe out the bookmarks and assume that we can rebuild
200+ // from the local places database.
201+ this.wipe_bookmarks();
202+ return true;
203+ case 1:
204+ this.upgrade_1_to_2();
205+ return true;
206+ case 2:
207+ // Nothing to be done: we're at the most recent version.
208+ return false;
209+ default:
210+ Log.error("Unknown schema version " + schema_version);
211+ return false;
212+ }
213+ },
214+
215+ wipe_bookmarks: function() {
216+ Log.debug("Wiping all bookmarks for profile " + this.profile);
217+ var result = this.couch.view("bindwood/bookmarks", {
218+ key: this.profile,
219+ include_docs: true,
220+ });
221+ var changes = [];
222+ for each (var row in result.rows) {
223+ var doc = row.doc;
224+ changes.push({
225+ _id: doc._id,
226+ _rev: doc._rev,
227+ _deleted: true
228+ });
229+ }
230+ // Delete all the bookmarks.
231+ this.couch.bulkSave(changes);
232+ },
233+
234+ /* The format used by Bindwood 0.4.2. Bookmarks were represented as:
235+ *
236+ * {
237+ * _id: '...',
238+ * record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark',
239+ * title: 'Bookmark title',
240+ * uri: 'http://www.example.com/',
241+ * application_annotations: {
242+ * Firefox: {
243+ * uuid: '...', // This is the UUID associated with the local item.
244+ * folder: 'Title of parent folder',
245+ * profile: 'profile name'
246+ * }
247+ * }
248+ * }
249+ *
250+ * Folders and separators had the appropriate record types, and
251+ * had no URI property. Folder child ordering is not stored
252+ * anywhere.
253+ *
254+ * There is no special handling of livemarks. They are stored as
255+ * folders, and their children are also stored.
256+ *
257+ * The special parent names "toolbarFolder", "bookmarksMenuFolder"
258+ * and "unfiledBookmarksFolder" were used to represent items at
259+ * the top levels of each folder hierarchy.
260+ */
261+
262+ /* The format used by Bindwood 1.0.x. Bookmarks look like this:
263+ *
264+ * {
265+ * _id: '...', // This is the UUID associated with the local item.
266+ * record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark',
267+ * record_type_version: 1,
268+ * title: 'Bookmark title',
269+ * uri: 'http://www.example.com/',
270+ * application_annotations: {
271+ * Firefox: {
272+ * profile: 'profile name'
273+ * last_modified: ..., // time in microseconds since epoch
274+ * }
275+ * }
276+ * }
277+ *
278+ * Folders contain a list of the IDs of their children (the
279+ * opposite of the 0.4.2 format), which also acts as a way to
280+ * store the child ordering.
281+ *
282+ * Livemarks are stored with their own record type. They look
283+ * similar to normal bookmarks, but store site_uri and feed_uri
284+ * instead of a single bookmark URI.
285+ */
286+ upgrade_1_to_2: function() {
287+ Log.debug("Upgrading bookmarks for profile " + this.profile +
288+ " from schema v1 to v2");
289+ var result = this.couch.view("bindwood/bookmarks", {
290+ key: this.profile,
291+ include_docs: true,
292+ });
293+
294+ var changes = [];
295+
296+ // Build a map of document IDs to the documents they reflect.
297+ var docs_by_id = {};
298+ for each (var row in result.rows) {
299+ docs_by_id[row.id] = row.doc;
300+ }
301+
302+ // Fix up children of the root document. The children (which
303+ // represent the toolbar, bookmarks menu and unfiled bookmarks
304+ // folders) should be renamed to fixed identifiers.
305+ var root_doc = docs_by_id['root_' + this.profile];
306+ root_doc.record_type = 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/folder';
307+ var expected_children = [
308+ 'toolbar_' + this.profile,
309+ 'menu_' + this.profile,
310+ 'unfiled_' + this.profile];
311+ for (var i = 0; i < expected_children.length; i++) {
312+ var old_id = root_doc.children[i];
313+ var new_id = expected_children[i];
314+ // Existing document has expected name.
315+ if (old_id == new_id) {
316+ continue;
317+ }
318+ var doc = docs_by_id[old_id];
319+ delete docs_by_id[old_id];
320+
321+ // Schedule deletion of old document
322+ changes.push({
323+ _id: doc._id,
324+ _rev: doc._rev,
325+ _deleted: true
326+ });
327+ doc._id = new_id;
328+ delete doc._rev;
329+ docs_by_id[new_id] = doc;
330+ root_doc.children[i] = new_id;
331+ }
332+
333+ // Walk the bookmarks tree from the root node to determine
334+ // which documents are reachable, and build up a parent map.
335+ var reachable = {};
336+ var parents_by_id = {};
337+
338+ var remaining = [root_doc];
339+ reachable[root_doc._id] = true;
340+ while (remaining.length > 0) {
341+ var doc = remaining.pop();
342+ for each (var child_id in doc.children) {
343+ parents_by_id[child_id] = doc;
344+ reachable[child_id] = true;
345+
346+ var child_doc = docs_by_id[child_id];
347+ if (child_doc && child_doc.record_type == 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/folder') {
348+ remaining.push(child_doc);
349+ }
350+ }
351+ }
352+
353+ // Now update the documents.
354+ for each (var doc in docs_by_id) {
355+ // If the document can not be reached from the root, then
356+ // it does not belong in the document tree.
357+ if (!reachable[doc._id]) {
358+ Log.debug("Deleting bookmark " + doc._id +
359+ " is not reachable from root.");
360+ changes.push({
361+ _id: doc._id,
362+ _rev: doc._rev,
363+ _deleted: true
364+ });
365+ continue;
366+ }
367+
368+ doc.record_type_version = 2;
369+ var parent_doc = parents_by_id[doc._id]
370+ if (parent_doc) {
371+ doc.parent_guid = parent_doc._id;
372+ doc.parent_title = parent_doc.title;
373+ }
374+
375+ // Bindwood 1.0.x would store a lot of useless data in the
376+ // Firefox section of the document (basically anything
377+ // that triggered an onItemChanged callback). Keep only
378+ // the information we care about.
379+ var old_annotations = doc.application_annotations.Firefox;
380+ var new_annotations = {
381+ profile: old_annotations.profile,
382+ last_modified: old_annotations.last_modified
383+ };
384+ doc.application_annotations.Firefox = new_annotations;
385+
386+ changes.push(doc);
387+ }
388+ // Now push the updates back to CouchDB.
389+ Log.debug("Saving modifications to database.");
390+ this.couch.bulkSave(changes);
391+ Log.debug("Done.");
392+ }
393+};
394
395=== modified file 'modules/sync.jsm'
396--- modules/sync.jsm 2011-03-04 13:02:54 +0000
397+++ modules/sync.jsm 2011-03-04 13:02:54 +0000
398@@ -22,6 +22,7 @@
399 const Cu = Components.utils;
400
401 Cu.import("resource://bindwood/logging.jsm");
402+Cu.import("resource://bindwood/migration.jsm");
403
404 const GUID_ANNOTATION = "bindwood/uuid";
405 const PARENT_ANNOTATION = "bindwood/parent";
406@@ -33,6 +34,8 @@
407 const TYPE_FEED = RECORD_PREFIX + "feed";
408 const TYPE_SEPARATOR = RECORD_PREFIX + "separator";
409
410+const RECORD_TYPE_VERSION = 2;
411+
412 var bookmarksService = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]
413 .getService(Ci.nsINavBookmarksService);
414 var livemarkService = Cc["@mozilla.org/browser/livemark-service;2"]
415@@ -62,6 +65,7 @@
416 Log.debug("Initialising synchroniser.");
417 this.ensureDatabase();
418 this.ensureCouchViews();
419+ this.maybeMigrateData();
420 this.first_push = true;
421 this.observer = new BookmarksObserver(this);
422 bookmarksService.addObserver(this.observer, false);
423@@ -132,6 +136,16 @@
424 }
425 },
426
427+ maybeMigrateData: function() {
428+ migration = new SchemaMigration(this.couch, this.profile);
429+
430+ if (migration.upgrade()) {
431+ Log.debug("Schema was upgraded, so resetting last known " +
432+ "sequence number.");
433+ this.last_seq = 0;
434+ }
435+ },
436+
437 set_guid: function(item_id, guid) {
438 // If the item has already been annotated with a GUID, clear
439 // the old GUID mapping from our cache.
440@@ -543,6 +557,7 @@
441 }
442 }
443
444+ _setattr(doc, "record_type_version", RECORD_TYPE_VERSION);
445 if (!doc.application_annotations)
446 doc.application_annotations = {};
447 if (!doc.application_annotations.Firefox)
448@@ -553,6 +568,8 @@
449 if (parent_id > 0) {
450 var parent_guid = this.guid_from_id(parent_id);
451 _setattr(doc, "parent_guid", parent_guid);
452+ _setattr(
453+ doc, "parent_title", bookmarksService.getItemTitle(parent_id));
454 }
455 switch (item_type) {
456 case bookmarksService.TYPE_BOOKMARK:
457@@ -579,6 +596,7 @@
458 break;
459 case bookmarksService.TYPE_SEPARATOR:
460 _setattr(doc, "record_type", TYPE_SEPARATOR);
461+ _setattr(doc, "position", bookmarksService.getItemIndex(item_id));
462 break;
463 default:
464 Log.error("Can not handle item " + item_id + " of type " +
465
466=== modified file 'mozmill/shared-modules/bookmarks.js'
467--- mozmill/shared-modules/bookmarks.js 2011-02-15 09:33:35 +0000
468+++ mozmill/shared-modules/bookmarks.js 2011-03-04 13:02:54 +0000
469@@ -62,8 +62,15 @@
470 }
471 }
472
473+function setGuid(item_id, guid) {
474+ annotationService.setItemAnnotation(
475+ item_id, GUID_ANNOTATION, guid,
476+ 0, annotationService.EXPIRE_NEVER);
477+}
478+
479 exports.bookmarksService = bookmarksService;
480 exports.livemarkService = livemarkService;
481 exports.annotationService = annotationService;
482 exports.createURI = createURI;
483 exports.clearBookmarks = clearBookmarks;
484+exports.setGuid = setGuid;
485
486=== added file 'mozmill/tests/test_migration.js'
487--- mozmill/tests/test_migration.js 1970-01-01 00:00:00 +0000
488+++ mozmill/tests/test_migration.js 2011-03-04 13:02:54 +0000
489@@ -0,0 +1,425 @@
490+var bm = require("../shared-modules/bookmarks");
491+
492+const TIMEOUT = 5000;
493+
494+const RECORD_PREFIX = (
495+ "http://www.freedesktop.org/wiki/Specifications/desktopcouch/");
496+const TYPE_BOOKMARK = RECORD_PREFIX + "bookmark";
497+const TYPE_FOLDER = RECORD_PREFIX + "folder";
498+const TYPE_FEED = RECORD_PREFIX + "feed";
499+const TYPE_SEPARATOR = RECORD_PREFIX + "separator";
500+
501+
502+var setupModule = function(module) {
503+ module.controller = mozmill.getBrowserController();
504+ module.jum = {};
505+ module.desktopcouch = {};
506+ module.sync = {};
507+ module.migration = {};
508+ Cu.import("resource://mozmill/modules/jum.js", module.jum);
509+ Cu.import("resource://bindwood/desktopcouch.jsm", module.desktopcouch);
510+ Cu.import("resource://bindwood/sync.jsm", module.sync);
511+ Cu.import("resource://bindwood/migration.jsm", module.migration);
512+ module.couch = null;
513+ module.synchroniser = null;
514+ bm.clearBookmarks();
515+};
516+
517+
518+var setupTest = function(test) {
519+ var done = false;
520+ desktopcouch.connect_desktopcouch("test_bookmarks", function(db) {
521+ couch = db;
522+ done = true;
523+ }, function (message) {});
524+ controller.waitFor(
525+ function() { return done; }, "Could not connect to CouchDB", TIMEOUT);
526+ jum.assertNotEquals(couch, null);
527+
528+ try {
529+ couch.createDb();
530+ } catch (e) {
531+ if (e.error != 'file_exists')
532+ throw(e);
533+ }
534+ synchroniser = new sync.Synchroniser(couch, "profile_name");
535+ synchroniser.ensureCouchViews();
536+};
537+
538+
539+var teardownTest = function(test) {
540+ synchroniser.uninit();
541+ couch.deleteDb();
542+ bm.clearBookmarks();
543+};
544+
545+
546+var test_get_schema_version = function() {
547+ var sm = new migration.SchemaMigration(couch, 'profile_name');
548+ // With no root document, the database must be from 0.4.2.
549+ jum.assertEquals(sm.get_schema_version(), 0);
550+
551+ // Create a 1.0.x style root document.
552+ var root_doc = {
553+ _id: 'root_profile_name',
554+ children: ['old_toolbar', 'old_menu', 'old_unfiled'],
555+ application_annotations: {
556+ Firefox: {
557+ profile: 'profile_name',
558+ last_modified: 1
559+ }
560+ }
561+ };
562+ couch.save(root_doc);
563+ jum.assertEquals(sm.get_schema_version(), 1);
564+
565+ // Now modify it to match this version.
566+ root_doc.record_type = TYPE_FOLDER;
567+ root_doc.record_type_version = 2;
568+ root_doc.children = [
569+ 'toolbar_profile_name', 'menu_profile_name', 'unfiled_profile_name'];
570+ couch.save(root_doc);
571+ jum.assertEquals(sm.get_schema_version(), 2);
572+
573+ // Future schema versions will update record_type_version.
574+ root_doc.record_type_version = 42;
575+ couch.save(root_doc);
576+ jum.assertEquals(sm.get_schema_version(), 42);
577+};
578+
579+
580+// Build up bookmarks that match the sample data below.
581+var make_sample_bookmarks = function() {
582+ var folder1_id = bm.bookmarksService.createFolder(
583+ bm.bookmarksService.toolbarFolder, 'Folder 1',
584+ bm.bookmarksService.DEFAULT_INDEX);
585+ bm.setGuid(folder1_id, 'folder1');
586+
587+ var livemark1_id = bm.livemarkService.createLivemark(
588+ folder1_id, 'Livemark 1',
589+ bm.createURI('http://www.example.com/'),
590+ bm.createURI('http://www.example.com/feed'),
591+ bm.bookmarksService.DEFAULT_INDEX);
592+ bm.setGuid(livemark1_id, 'livemark1');
593+
594+ var bookmark1_id = bm.bookmarksService.insertBookmark(
595+ bm.bookmarksService.toolbarFolder,
596+ bm.createURI('http://www.example.com/bookmark1'),
597+ bm.bookmarksService.DEFAULT_INDEX, 'Bookmark 1');
598+ bm.setGuid(bookmark1_id, 'bookmark1');
599+
600+ var bookmark2_id = bm.bookmarksService.insertBookmark(
601+ bm.bookmarksService.bookmarksMenuFolder,
602+ bm.createURI('http://www.example.com/bookmark2'),
603+ bm.bookmarksService.DEFAULT_INDEX, 'Bookmark 2');
604+ bm.setGuid(bookmark2_id, 'bookmark2');
605+
606+ var separator1_id = bm.bookmarksService.insertSeparator(
607+ bm.bookmarksService.bookmarksMenuFolder,
608+ bm.bookmarksService.DEFAULT_INDEX);
609+ bm.setGuid(separator1_id, 'separator1');
610+
611+ var bookmark3_id = bm.bookmarksService.insertBookmark(
612+ bm.bookmarksService.unfiledBookmarksFolder,
613+ bm.createURI('http://www.example.com/bookmark3'),
614+ bm.bookmarksService.DEFAULT_INDEX, 'Bookmark 3');
615+ bm.setGuid(bookmark3_id, 'bookmark3');
616+};
617+
618+
619+// Make Version 0 (Bindwood 0.4.2) sample data
620+var make_sample_data_v0 = function() {
621+ couch.bulkSave([
622+ {
623+ _id: 'couchdb_folder1',
624+ record_type: TYPE_FOLDER,
625+ title: 'Folder 1',
626+ application_annotations: {
627+ uuid: 'folder1',
628+ folder: 'toolbarFolder',
629+ profile: 'profile_name'
630+ }
631+ },
632+ {
633+ _id: 'couchdb_livemark1',
634+ record_type: TYPE_FOLDER,
635+ title: 'Livemark 1',
636+ application_annotations: {
637+ uuid: 'livemark1',
638+ folder: 'Folder 1',
639+ profile: 'profile_name'
640+ }
641+ },
642+ {
643+ _id: 'couchdb_bookmark1',
644+ record_type: TYPE_BOOKMARK,
645+ title: 'Bookmark 1',
646+ uri: 'http://www.example.com/bookmark1',
647+ application_annotations: {
648+ uuid: 'bookmark1',
649+ folder: 'toolbarFolder',
650+ profile: 'profile_name'
651+ }
652+ },
653+ {
654+ _id: 'couchdb_bookmark2',
655+ record_type: TYPE_BOOKMARK,
656+ title: 'Bookmark 2',
657+ uri: 'http://www.example.com/bookmark2',
658+ application_annotations: {
659+ uuid: 'bookmark2',
660+ folder: 'bookmarksMenuFolder',
661+ profile: 'profile_name'
662+ }
663+ },
664+ {
665+ _id: 'couchdb_separator1',
666+ record_type: TYPE_SEPARATOR,
667+ title: null,
668+ application_annotations: {
669+ uuid: 'separator1',
670+ folder: 'bookmarksMenuFolder',
671+ profile: 'profile_name'
672+ }
673+ },
674+ {
675+ _id: 'couchdb_bookmark3',
676+ record_type: TYPE_BOOKMARK,
677+ title: 'Bookmark 3',
678+ uri: 'http://www.example.com/bookmark3',
679+ application_annotations: {
680+ uuid: 'bookmark3',
681+ folder: 'unfiledBookmarksFolder',
682+ profile: 'profile_name'
683+ }
684+ }
685+ ]);
686+};
687+
688+
689+// Make Version 1 (Bindwood 1.0.x) sample data
690+var make_sample_data_v1 = function() {
691+ var common_annotations = {
692+ Firefox: {
693+ profile: 'profile_name',
694+ last_modified: 1,
695+ garbage: ''
696+ }
697+ };
698+ couch.bulkSave([
699+ {
700+ _id: 'root_profile_name',
701+ children: ['obsolete_toolbar', 'obsolete_menu', 'obsolete_unfiled'],
702+ application_annotations: common_annotations
703+ },
704+ {
705+ _id: 'obsolete_toolbar',
706+ record_type: TYPE_FOLDER,
707+ record_type_version: 1,
708+ title: 'Bookmarks Toolbar',
709+ children: ['folder1', 'bookmark1'],
710+ application_annotations: common_annotations
711+ },
712+ {
713+ _id: 'folder1',
714+ record_type: TYPE_FOLDER,
715+ record_type_version: 1,
716+ title: 'Folder 1',
717+ children: ['livemark1'],
718+ application_annotations: common_annotations
719+ },
720+ {
721+ _id: 'livemark1',
722+ record_type: TYPE_FEED,
723+ record_type_version: 1,
724+ title: 'Livemark 1',
725+ feed_uri: 'http://www.example.com/feed',
726+ site_uri: 'http://www.example.com/',
727+ application_annotations: common_annotations
728+ },
729+ {
730+ _id: 'bookmark1',
731+ record_type: TYPE_BOOKMARK,
732+ record_type_version: 1,
733+ title: 'Bookmark 1',
734+ uri: 'http://www.example.com/bookmark1',
735+ application_annotations: common_annotations
736+ },
737+ {
738+ _id: 'obsolete_menu',
739+ record_type: TYPE_FOLDER,
740+ record_type_version: 1,
741+ title: 'Bookmarks Menu',
742+ children: ['bookmark2', 'separator1'],
743+ application_annotations: common_annotations
744+ },
745+ {
746+ _id: 'bookmark2',
747+ record_type: TYPE_BOOKMARK,
748+ record_type_version: 1,
749+ title: 'Bookmark 2',
750+ uri: 'http://www.example.com/bookmark2',
751+ application_annotations: common_annotations
752+ },
753+ {
754+ _id: 'separator1',
755+ record_type: TYPE_SEPARATOR,
756+ record_type_version: 1,
757+ application_annotations: common_annotations
758+ },
759+ {
760+ _id: 'obsolete_unfiled',
761+ record_type: TYPE_FOLDER,
762+ record_type_version: 1,
763+ title: 'Unfiled Bookmarks',
764+ children: ['bookmark3'],
765+ application_annotations: common_annotations
766+ },
767+ {
768+ _id: 'bookmark3',
769+ record_type: TYPE_BOOKMARK,
770+ record_type_version: 1,
771+ title: 'Bookmark 3',
772+ uri: 'http://www.example.com/bookmark3',
773+ application_annotations: common_annotations
774+ },
775+ {
776+ _id: 'orphaned_bookmark',
777+ record_type: TYPE_BOOKMARK,
778+ record_type_version: 1,
779+ title: 'Orphaned Bookmark',
780+ uri: 'http://www.example.com/orphan',
781+ application_annotations: common_annotations
782+ }
783+ ]);
784+};
785+
786+// Verify that the database contains expected v2 sample data.
787+var verify_sample_data_v2 = function() {
788+ var root = couch.open('root_profile_name');
789+ jum.assertNotNull(root);
790+ jum.assertEquals(root.record_type, TYPE_FOLDER);
791+ jum.assertEquals(root.record_type_version, 2);
792+ jum.assertEquals(root.children.length, 3);
793+ jum.assertEquals(root.children[0], 'toolbar_profile_name');
794+ jum.assertEquals(root.children[1], 'menu_profile_name');
795+ jum.assertEquals(root.children[2], 'unfiled_profile_name');
796+
797+ var toolbar = couch.open('toolbar_profile_name');
798+ jum.assertNotNull(toolbar);
799+ jum.assertEquals(toolbar.record_type, TYPE_FOLDER);
800+ jum.assertEquals(toolbar.record_type_version, 2);
801+ jum.assertEquals(toolbar.parent_guid, 'root_profile_name');
802+ jum.assertEquals(toolbar.children.length, 2);
803+ jum.assertEquals(toolbar.children[0], 'folder1');
804+ jum.assertEquals(toolbar.children[1], 'bookmark1');
805+
806+ var folder1 = couch.open('folder1');
807+ jum.assertNotNull(folder1);
808+ jum.assertEquals(folder1.record_type, TYPE_FOLDER);
809+ jum.assertEquals(folder1.record_type_version, 2);
810+ jum.assertEquals(folder1.parent_guid, 'toolbar_profile_name');
811+ jum.assertEquals(folder1.title, 'Folder 1');
812+ jum.assertEquals(folder1.children.length, 1);
813+ jum.assertEquals(folder1.children[0], 'livemark1');
814+
815+ var livemark1 = couch.open('livemark1');
816+ jum.assertNotNull(livemark1);
817+ jum.assertEquals(livemark1.record_type, TYPE_FEED);
818+ jum.assertEquals(livemark1.record_type_version, 2);
819+ jum.assertEquals(livemark1.parent_guid, 'folder1');
820+ jum.assertEquals(livemark1.title, 'Livemark 1');
821+ jum.assertEquals(livemark1.site_uri, 'http://www.example.com/')
822+ jum.assertEquals(livemark1.feed_uri, 'http://www.example.com/feed')
823+
824+ var bookmark1 = couch.open('bookmark1');
825+ jum.assertNotNull(bookmark1);
826+ jum.assertEquals(bookmark1.record_type, TYPE_BOOKMARK);
827+ jum.assertEquals(bookmark1.record_type_version, 2);
828+ jum.assertEquals(bookmark1.parent_guid, 'toolbar_profile_name');
829+ jum.assertEquals(bookmark1.title, 'Bookmark 1');
830+ jum.assertEquals(bookmark1.uri, 'http://www.example.com/bookmark1');
831+
832+ var menu = couch.open('menu_profile_name');
833+ jum.assertNotNull(menu);
834+ jum.assertEquals(menu.record_type, TYPE_FOLDER);
835+ jum.assertEquals(menu.record_type_version, 2);
836+ jum.assertEquals(menu.parent_guid, 'root_profile_name');
837+ jum.assertEquals(menu.children.length, 2);
838+ jum.assertEquals(menu.children[0], 'bookmark2');
839+ jum.assertEquals(menu.children[1], 'separator1');
840+
841+ var bookmark2 = couch.open('bookmark2');
842+ jum.assertNotNull(bookmark2);
843+ jum.assertEquals(bookmark2.record_type, TYPE_BOOKMARK);
844+ jum.assertEquals(bookmark2.record_type_version, 2);
845+ jum.assertEquals(bookmark2.parent_guid, 'menu_profile_name');
846+ jum.assertEquals(bookmark2.title, 'Bookmark 2');
847+ jum.assertEquals(bookmark2.uri, 'http://www.example.com/bookmark2');
848+
849+ var separator1 = couch.open('separator1');
850+ jum.assertNotNull(separator1);
851+ jum.assertEquals(separator1.record_type, TYPE_SEPARATOR);
852+ jum.assertEquals(separator1.record_type_version, 2);
853+ jum.assertEquals(separator1.parent_guid, 'menu_profile_name');
854+
855+ var unfiled = couch.open('unfiled_profile_name');
856+ jum.assertNotNull(unfiled);
857+ jum.assertEquals(unfiled.record_type, TYPE_FOLDER);
858+ jum.assertEquals(unfiled.record_type_version, 2);
859+ jum.assertEquals(unfiled.parent_guid, 'root_profile_name');
860+ jum.assertEquals(unfiled.children.length, 1);
861+ jum.assertEquals(unfiled.children[0], 'bookmark3');
862+
863+ var bookmark3 = couch.open('bookmark3');
864+ jum.assertNotNull(bookmark3);
865+ jum.assertEquals(bookmark3.record_type, TYPE_BOOKMARK);
866+ jum.assertEquals(bookmark3.record_type_version, 2);
867+ jum.assertEquals(bookmark3.parent_guid, 'unfiled_profile_name');
868+ jum.assertEquals(bookmark3.title, 'Bookmark 3');
869+ jum.assertEquals(bookmark3.uri, 'http://www.example.com/bookmark3');
870+};
871+
872+
873+var test_upgrade_1_to_2 = function() {
874+ make_sample_data_v1();
875+ var sm = new migration.SchemaMigration(couch, 'profile_name');
876+ sm.upgrade_1_to_2();
877+
878+ verify_sample_data_v2();
879+
880+ // The old documents for the special folders have been removed.
881+ jum.assertNull(couch.open('obsolete_toolbar'));
882+ jum.assertNull(couch.open('obsolete_menu'));
883+ jum.assertNull(couch.open('obsolete_unfiled'));
884+
885+ // Unneeded attributes removed from annotation.
886+ var bookmark1_doc = couch.open('bookmark1');
887+ jum.assertUndefined(bookmark1_doc.application_annotations.Firefox.garbage);
888+
889+ // Orphaned items have been deleted.
890+ jum.assertNull(couch.open('orphaned_bookmark'));
891+};
892+
893+
894+var test_migrate_v0 = function() {
895+ make_sample_bookmarks();
896+ make_sample_data_v0();
897+
898+ synchroniser.last_seq = couch.info().last_seq;
899+ synchroniser.init();
900+ synchroniser.sync();
901+
902+ verify_sample_data_v2();
903+};
904+
905+var test_migrate_v1 = function() {
906+ make_sample_bookmarks();
907+ make_sample_data_v1();
908+
909+ synchroniser.last_seq = couch.info().last_seq;
910+ synchroniser.init();
911+ synchroniser.sync();
912+
913+ verify_sample_data_v2();
914+};
915
916=== modified file 'mozmill/tests/test_sync_all.js'
917--- mozmill/tests/test_sync_all.js 2011-03-04 13:02:54 +0000
918+++ mozmill/tests/test_sync_all.js 2011-03-04 13:02:54 +0000
919@@ -100,10 +100,23 @@
920 };
921
922 var test_sync_remote = function() {
923+ couch.save({
924+ _id: 'root_profile_name',
925+ record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/folder',
926+ record_type_version: 2,
927+ children: [
928+ 'toolbar_profile_name',
929+ 'menu_profile_name',
930+ 'unfiled_profile_name'
931+ ]
932+ });
933+
934 var item_guid = '12345';
935 var doc = {
936 _id: item_guid,
937 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark',
938+ record_type_version: 2,
939+ parent_title: 'Bookmarks Toolbar',
940 parent_guid: 'toolbar_profile_name',
941 title: 'Bookmark title',
942 uri: LOCAL_TEST_PAGE,
943@@ -173,6 +186,7 @@
944 var doc = {
945 _id: item_guid,
946 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark',
947+ record_type_version: 2,
948 parent_guid: 'toolbar_profile_name',
949 title: 'Bookmark title',
950 uri: LOCAL_TEST_PAGE,
951
952=== modified file 'mozmill/tests/test_sync_from_couch.js'
953--- mozmill/tests/test_sync_from_couch.js 2011-03-04 13:02:54 +0000
954+++ mozmill/tests/test_sync_from_couch.js 2011-03-04 13:02:54 +0000
955@@ -29,6 +29,7 @@
956 synchroniser.processRecord({
957 _id: '12345',
958 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark',
959+ record_type_version: 2,
960 parent_guid: 'toolbar_profile_name',
961 title: 'Bookmark title',
962 uri: LOCAL_TEST_PAGE,
963@@ -54,6 +55,7 @@
964 doc = {
965 _id: '12345',
966 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark',
967+ record_type_version: 2,
968 parent_guid: 'toolbar_profile_name',
969 title: 'Bookmark title',
970 uri: LOCAL_TEST_PAGE,
971@@ -82,6 +84,7 @@
972 synchroniser.processRecord({
973 _id: '12345',
974 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/folder',
975+ record_type_version: 2,
976 parent_guid: 'toolbar_profile_name',
977 title: 'Folder',
978 children: [],
979@@ -107,6 +110,7 @@
980 doc = {
981 _id: '12345',
982 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/folder',
983+ record_type_version: 2,
984 parent_guid: 'toolbar_profile_name',
985 title: 'Folder',
986 children: [],
987@@ -134,6 +138,7 @@
988 synchroniser.processRecord({
989 _id: '12345',
990 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/feed',
991+ record_type_version: 2,
992 parent_guid: 'toolbar_profile_name',
993 title: 'Livemark',
994 site_uri: "http://www.example.com/",
995@@ -161,6 +166,7 @@
996 doc = {
997 _id: '12345',
998 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/feed',
999+ record_type_version: 2,
1000 parent_guid: 'toolbar_profile_name',
1001 title: 'Livemark',
1002 site_uri: "http://www.example.com/",
1003@@ -190,6 +196,7 @@
1004 synchroniser.processRecord({
1005 _id: '12345',
1006 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/separator',
1007+ record_type_version: 2,
1008 parent_guid: 'toolbar_profile_name',
1009 application_annotations: {
1010 Firefox: {
1011@@ -209,6 +216,7 @@
1012 doc = {
1013 _id: '12345',
1014 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/separator',
1015+ record_type_version: 2,
1016 parent_guid: 'toolbar_profile_name',
1017 application_annotations: {
1018 Firefox: {
1019@@ -233,6 +241,7 @@
1020 doc = {
1021 _id: '12345',
1022 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark',
1023+ record_type_version: 2,
1024 parent_guid: 'toolbar_profile_name',
1025 title: 'Bookmark title',
1026 uri: LOCAL_TEST_PAGE,
1027@@ -262,6 +271,7 @@
1028 synchroniser.processRecord({
1029 _id: '12345',
1030 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/folder',
1031+ record_type_version: 2,
1032 parent_guid: 'toolbar_profile_name',
1033 title: 'Folder',
1034 children: ['67890'],
1035@@ -281,6 +291,7 @@
1036 synchroniser.processRecord({
1037 _id: '67890',
1038 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark',
1039+ record_type_version: 2,
1040 parent_guid: '12345',
1041 title: 'Bookmark title',
1042 uri: LOCAL_TEST_PAGE,
1043@@ -300,6 +311,7 @@
1044 synchroniser.processRecord({
1045 _id: '67890',
1046 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark',
1047+ record_type_version: 2,
1048 parent_guid: '12345',
1049 title: 'Bookmark title',
1050 uri: LOCAL_TEST_PAGE,
1051@@ -318,6 +330,7 @@
1052 synchroniser.processRecord({
1053 _id: '12345',
1054 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/folder',
1055+ record_type_version: 2,
1056 parent_guid: 'toolbar_profile_name',
1057 title: 'Folder',
1058 children: ['67890'],
1059@@ -342,6 +355,7 @@
1060 synchroniser.processRecord({
1061 _id: 'parent',
1062 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/folder',
1063+ record_type_version: 2,
1064 parent_guid: 'toolbar_profile_name',
1065 title: 'Folder',
1066 children: ['child1', 'child2', 'child3', 'missing_child'],
1067@@ -363,6 +377,7 @@
1068 synchroniser.processRecord({
1069 _id: 'child1',
1070 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark',
1071+ record_type_version: 2,
1072 parent_guid: 'parent',
1073 title: 'Child 1',
1074 uri: LOCAL_TEST_PAGE + "#child1",
1075@@ -376,6 +391,7 @@
1076 synchroniser.processRecord({
1077 _id: 'child3',
1078 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark',
1079+ record_type_version: 2,
1080 parent_guid: 'parent',
1081 title: 'Child 3',
1082 uri: LOCAL_TEST_PAGE + "#child1",
1083@@ -389,6 +405,7 @@
1084 synchroniser.processRecord({
1085 _id: 'child2',
1086 record_type: 'http://www.freedesktop.org/wiki/Specifications/desktopcouch/bookmark',
1087+ record_type_version: 2,
1088 parent_guid: 'parent',
1089 title: 'Child 2',
1090 uri: LOCAL_TEST_PAGE + "#child1",
1091
1092=== modified file 'mozmill/tests/test_sync_to_couch.js'
1093--- mozmill/tests/test_sync_to_couch.js 2011-03-04 13:02:54 +0000
1094+++ mozmill/tests/test_sync_to_couch.js 2011-03-04 13:02:54 +0000
1095@@ -56,6 +56,7 @@
1096 var doc = couch.open(item_guid);
1097 jum.assertEquals(doc.record_type, "http://www.freedesktop.org/wiki/" +
1098 "Specifications/desktopcouch/bookmark");
1099+ jum.assertEquals(doc.record_type_version, 2);
1100 jum.assertEquals(doc.application_annotations.Firefox.profile,
1101 "profile_name");
1102 jum.assertEquals(doc.parent_guid, "toolbar_profile_name");
1103@@ -91,6 +92,7 @@
1104 var doc = couch.open(folder_guid);
1105 jum.assertEquals(doc.record_type, "http://www.freedesktop.org/wiki/" +
1106 "Specifications/desktopcouch/folder");
1107+ jum.assertEquals(doc.record_type_version, 2);
1108 jum.assertEquals(doc.application_annotations.Firefox.profile,
1109 "profile_name");
1110 jum.assertEquals(doc.parent_guid, "toolbar_profile_name");
1111@@ -128,6 +130,7 @@
1112 var doc = couch.open(item_guid);
1113 jum.assertEquals(doc.record_type, "http://www.freedesktop.org/wiki/" +
1114 "Specifications/desktopcouch/feed");
1115+ jum.assertEquals(doc.record_type_version, 2);
1116 jum.assertEquals(doc.application_annotations.Firefox.profile,
1117 "profile_name");
1118 jum.assertEquals(doc.parent_guid, "toolbar_profile_name");
1119@@ -157,6 +160,7 @@
1120 var doc = couch.open(item_guid);
1121 jum.assertEquals(doc.record_type, "http://www.freedesktop.org/wiki/" +
1122 "Specifications/desktopcouch/separator");
1123+ jum.assertEquals(doc.record_type_version, 2);
1124 jum.assertEquals(doc.application_annotations.Firefox.profile,
1125 "profile_name");
1126 jum.assertEquals(doc.parent_guid, "toolbar_profile_name");
1127@@ -191,6 +195,7 @@
1128 var doc = couch.open(root_guid);
1129 jum.assertEquals(doc.record_type, "http://www.freedesktop.org/wiki/" +
1130 "Specifications/desktopcouch/folder");
1131+ jum.assertEquals(doc.record_type_version, 2);
1132 jum.assertUndefined(doc.parent_guid);
1133 jum.assertEquals(doc.children.length, 3);
1134 jum.assertEquals(doc.children[0], synchroniser.guid_from_id(

Subscribers

People subscribed via source and target branches