diff options
Diffstat (limited to 'chrome/browser/sync/syncable')
-rwxr-xr-x | chrome/browser/sync/syncable/directory_backing_store.cc | 309 | ||||
-rw-r--r-- | chrome/browser/sync/syncable/directory_backing_store.h | 38 | ||||
-rwxr-xr-x | chrome/browser/sync/syncable/directory_backing_store_unittest.cc | 523 | ||||
-rw-r--r-- | chrome/browser/sync/syncable/model_type.h | 16 | ||||
-rwxr-xr-x | chrome/browser/sync/syncable/syncable.cc | 87 | ||||
-rwxr-xr-x | chrome/browser/sync/syncable/syncable.h | 62 | ||||
-rwxr-xr-x | chrome/browser/sync/syncable/syncable_columns.h | 10 | ||||
-rwxr-xr-x | chrome/browser/sync/syncable/syncable_unittest.cc | 20 |
8 files changed, 951 insertions, 114 deletions
diff --git a/chrome/browser/sync/syncable/directory_backing_store.cc b/chrome/browser/sync/syncable/directory_backing_store.cc index 6df61be..4cdf7c2 100755 --- a/chrome/browser/sync/syncable/directory_backing_store.cc +++ b/chrome/browser/sync/syncable/directory_backing_store.cc @@ -14,7 +14,9 @@ #include "base/hash_tables.h" #include "base/logging.h" +#include "chrome/browser/sync/protocol/bookmark_specifics.pb.h" #include "chrome/browser/sync/protocol/service_constants.h" +#include "chrome/browser/sync/protocol/sync.pb.h" #include "chrome/browser/sync/syncable/syncable-inl.h" #include "chrome/browser/sync/syncable/syncable_columns.h" #include "chrome/browser/sync/util/crypto_helpers.h" @@ -36,13 +38,7 @@ namespace syncable { static const string::size_type kUpdateStatementBufferSize = 2048; // Increment this version whenever updating DB tables. -static const int32 kCurrentDBVersion = 68; - -static void RegisterPathNameCollate(sqlite3* dbhandle) { - const int collate = SQLITE_UTF8; - CHECK(SQLITE_OK == sqlite3_create_collation(dbhandle, "PATHNAME", collate, - NULL, &ComparePathNames16)); -} +extern const int32 kCurrentDBVersion = 69; // Extern only for our unittest. namespace { @@ -58,12 +54,13 @@ int ExecQuery(sqlite3* dbhandle, const char* query) { return result; } -} // namespace - -static string GenerateCacheGUID() { +string GenerateCacheGUID() { return Generate128BitRandomHexString(); } +} // namespace + + // Iterate over the fields of |entry| and bind each to |statement| for // updating. Returns the number of args bound. int BindFields(const EntryKernel& entry, SQLStatement* statement) { @@ -81,11 +78,10 @@ int BindFields(const EntryKernel& entry, SQLStatement* statement) { for ( ; i < STRING_FIELDS_END; ++i) { statement->bind_string(index++, entry.ref(static_cast<StringField>(i))); } - for ( ; i < BLOB_FIELDS_END; ++i) { - uint8* blob = entry.ref(static_cast<BlobField>(i)).empty() ? - NULL : const_cast<uint8*>(&entry.ref(static_cast<BlobField>(i)).at(0)); - statement->bind_blob(index++, blob, - entry.ref(static_cast<BlobField>(i)).size()); + std::string temp; + for ( ; i < PROTO_FIELDS_END; ++i) { + entry.ref(static_cast<ProtoField>(i)).SerializeToString(&temp); + statement->bind_blob(index++, temp.data(), temp.length()); } return index; } @@ -110,12 +106,12 @@ EntryKernel* UnpackEntry(SQLStatement* statement) { result->put(static_cast<BitField>(i), (0 != statement->column_int(i))); } for ( ; i < STRING_FIELDS_END; ++i) { - result->put(static_cast<StringField>(i), + result->put(static_cast<StringField>(i), statement->column_string(i)); } - for ( ; i < BLOB_FIELDS_END; ++i) { - statement->column_blob_as_vector( - i, &result->mutable_ref(static_cast<BlobField>(i))); + for ( ; i < PROTO_FIELDS_END; ++i) { + result->mutable_ref(static_cast<ProtoField>(i)).ParseFromArray( + statement->column_blob(i), statement->column_bytes(i)); } ZeroFields(result, i); } else { @@ -125,8 +121,11 @@ EntryKernel* UnpackEntry(SQLStatement* statement) { return result; } -static string ComposeCreateTableColumnSpecs(const ColumnSpec* begin, - const ColumnSpec* end) { +namespace { + +string ComposeCreateTableColumnSpecs() { + const ColumnSpec* begin = g_metas_columns; + const ColumnSpec* end = g_metas_columns + arraysize(g_metas_columns); string query; query.reserve(kUpdateStatementBufferSize); char separator = '('; @@ -141,6 +140,18 @@ static string ComposeCreateTableColumnSpecs(const ColumnSpec* begin, return query; } +void AppendColumnList(std::string* output) { + const char* joiner = " "; + // Be explicit in SELECT order to match up with UnpackEntry. + for (int i = BEGIN_FIELDS; i < BEGIN_FIELDS + FIELD_COUNT; ++i) { + output->append(joiner); + output->append(ColumnName(i)); + joiner = ", "; + } +} + +} // namespace + /////////////////////////////////////////////////////////////////////////////// // DirectoryBackingStore implementation. @@ -149,7 +160,8 @@ DirectoryBackingStore::DirectoryBackingStore(const string& dir_name, : load_dbhandle_(NULL), save_dbhandle_(NULL), dir_name_(dir_name), - backing_filepath_(backing_filepath) { + backing_filepath_(backing_filepath), + needs_column_refresh_(false) { } DirectoryBackingStore::~DirectoryBackingStore() { @@ -182,7 +194,6 @@ bool DirectoryBackingStore::OpenAndConfigureHandleHelper( } } sqlite3_busy_timeout(*handle, kDirectoryBackingStoreBusyTimeoutMs); - RegisterPathNameCollate(*handle); #if defined(OS_WIN) // Do not index this file. Scanning can occur every time we close the file, // which causes long delays in SQLite's file locking. @@ -200,8 +211,7 @@ bool DirectoryBackingStore::OpenAndConfigureHandleHelper( DirOpenResult DirectoryBackingStore::Load(MetahandlesIndex* entry_bucket, ExtendedAttributes* xattrs_bucket, Directory::KernelLoadInfo* kernel_load_info) { - DCHECK(load_dbhandle_ == NULL); - if (!OpenAndConfigureHandleHelper(&load_dbhandle_)) + if (!BeginLoad()) return FAILED_OPEN_DATABASE; DirOpenResult result = InitializeTables(); @@ -213,10 +223,18 @@ DirOpenResult DirectoryBackingStore::Load(MetahandlesIndex* entry_bucket, LoadExtendedAttributes(xattrs_bucket); LoadInfo(kernel_load_info); + EndLoad(); + return OPENED; +} + +bool DirectoryBackingStore::BeginLoad() { + DCHECK(load_dbhandle_ == NULL); + return OpenAndConfigureHandleHelper(&load_dbhandle_); +} + +void DirectoryBackingStore::EndLoad() { sqlite3_close(load_dbhandle_); load_dbhandle_ = NULL; // No longer used. - - return OPENED; } bool DirectoryBackingStore::SaveChanges( @@ -273,25 +291,38 @@ DirOpenResult DirectoryBackingStore::InitializeTables() { if (SQLITE_OK != transaction.BeginExclusive()) { return FAILED_DISK_FULL; } - int version_on_disk = 0; + int version_on_disk = GetVersion(); int last_result = SQLITE_OK; - if (DoesSqliteTableExist(load_dbhandle_, "share_version")) { - SQLStatement version_query; - version_query.prepare(load_dbhandle_, "SELECT data from share_version"); - last_result = version_query.step(); - if (SQLITE_ROW == last_result) { - version_on_disk = version_query.column_int(0); - } - last_result = version_query.reset(); + // Upgrade from version 67. Version 67 was widely distributed as the original + // Bookmark Sync release. Version 68 removed unique naming. + if (version_on_disk == 67) { + if (MigrateVersion67To68()) + version_on_disk = 68; + } + // Version 69 introduced additional datatypes. + if (version_on_disk == 68) { + if (MigrateVersion68To69()) + version_on_disk = 69; } + + // If one of the migrations requested it, drop columns that aren't current. + // It's only safe to do this after migrating all the way to the current + // version. + if (version_on_disk == kCurrentDBVersion && needs_column_refresh_) { + if (!RefreshColumns()) + version_on_disk = 0; + } + + // A final, alternative catch-all migration to simply re-sync everything. if (version_on_disk != kCurrentDBVersion) { if (version_on_disk > kCurrentDBVersion) { transaction.Rollback(); return FAILED_NEWER_VERSION; } + // Fallback (re-sync everything) migration path. LOG(INFO) << "Old/null sync database, version " << version_on_disk; - // Delete the existing database (if any), and create a freshone. + // Delete the existing database (if any), and create a fresh one. if (SQLITE_OK == last_result) { DropAllTables(); if (SQLITE_DONE == CreateTables()) { @@ -323,17 +354,40 @@ DirOpenResult DirectoryBackingStore::InitializeTables() { return FAILED_DISK_FULL; } +bool DirectoryBackingStore::RefreshColumns() { + DCHECK(needs_column_refresh_); + + // Create a new table named temp_metas. + SafeDropTable("temp_metas"); + if (CreateMetasTable(true) != SQLITE_DONE) + return false; + + // Populate temp_metas from metas. + std::string query = "INSERT INTO temp_metas ("; + AppendColumnList(&query); + query.append(") SELECT "); + AppendColumnList(&query); + query.append(" FROM metas"); + if (ExecQuery(load_dbhandle_, query.c_str()) != SQLITE_DONE) + return false; + + // Drop metas. + SafeDropTable("metas"); + + // Rename temp_metas -> metas. + int result = ExecQuery(load_dbhandle_, + "ALTER TABLE temp_metas RENAME TO metas"); + if (result != SQLITE_DONE) + return false; + needs_column_refresh_ = false; + return true; +} + void DirectoryBackingStore::LoadEntries(MetahandlesIndex* entry_bucket) { string select; select.reserve(kUpdateStatementBufferSize); - select.append("SELECT"); - const char* joiner = " "; - // Be explicit in SELECT order to match up with UnpackEntry. - for (int i = BEGIN_FIELDS; i < BEGIN_FIELDS + FIELD_COUNT; ++i) { - select.append(joiner); - select.append(ColumnName(i)); - joiner = ", "; - } + select.append("SELECT "); + AppendColumnList(&select); select.append(" FROM metas "); SQLStatement statement; statement.prepare(load_dbhandle_, select.c_str()); @@ -402,7 +456,7 @@ bool DirectoryBackingStore::SaveEntryToDB(const EntryKernel& entry) { values.append("VALUES "); const char* separator = "( "; int i = 0; - for (i = BEGIN_FIELDS; i < BLOB_FIELDS_END; ++i) { + for (i = BEGIN_FIELDS; i < PROTO_FIELDS_END; ++i) { query.append(separator); values.append(separator); separator = ", "; @@ -517,11 +571,162 @@ int DirectoryBackingStore::CreateExtendedAttributeTable() { void DirectoryBackingStore::DropAllTables() { SafeDropTable("metas"); + SafeDropTable("temp_metas"); SafeDropTable("share_info"); SafeDropTable("share_version"); SafeDropTable("extended_attributes"); + needs_column_refresh_ = false; +} + +bool DirectoryBackingStore::MigrateToSpecifics( + const char* old_columns, + const char* specifics_column, + void (*handler_function)(SQLStatement* old_value_query, + int old_value_column, + sync_pb::EntitySpecifics* mutable_new_value)) { + std::string query_sql = StringPrintf("SELECT metahandle, %s, %s FROM metas", + specifics_column, old_columns); + std::string update_sql = StringPrintf( + "UPDATE metas SET %s = ? WHERE metahandle = ?", specifics_column); + SQLStatement query; + query.prepare(load_dbhandle_, query_sql.c_str()); + while (query.step() == SQLITE_ROW) { + int64 metahandle = query.column_int64(0); + std::string new_value_bytes; + query.column_blob_as_string(1, &new_value_bytes); + sync_pb::EntitySpecifics new_value; + new_value.ParseFromString(new_value_bytes); + handler_function(&query, 2, &new_value); + new_value.SerializeToString(&new_value_bytes); + + SQLStatement update; + update.prepare(load_dbhandle_, update_sql.data(), update_sql.length()); + update.bind_blob(0, new_value_bytes.data(), new_value_bytes.length()); + update.bind_int64(1, metahandle); + if (update.step() != SQLITE_DONE) { + NOTREACHED(); + return false; + } + } + return true; +} + +bool DirectoryBackingStore::AddColumn(const ColumnSpec* column) { + SQLStatement add_column; + std::string sql = StringPrintf("ALTER TABLE metas ADD COLUMN %s %s", + column->name, column->spec); + add_column.prepare(load_dbhandle_, sql.c_str()); + return add_column.step() == SQLITE_DONE; +} + +bool DirectoryBackingStore::SetVersion(int version) { + SQLStatement statement; + statement.prepare(load_dbhandle_, "UPDATE share_version SET data = ?"); + statement.bind_int(0, version); + return statement.step() == SQLITE_DONE; +} + +int DirectoryBackingStore::GetVersion() { + if (!DoesSqliteTableExist(load_dbhandle_, "share_version")) + return 0; + SQLStatement version_query; + version_query.prepare(load_dbhandle_, "SELECT data from share_version"); + if (SQLITE_ROW != version_query.step()) + return 0; + int value = version_query.column_int(0); + if (version_query.reset() != SQLITE_OK) + return 0; + return value; +} + +bool DirectoryBackingStore::MigrateVersion67To68() { + // This change simply removed three columns: + // string NAME + // string UNSANITIZED_NAME + // string SERVER_NAME + // No data migration is necessary, but we should do a column refresh. + SetVersion(68); + needs_column_refresh_ = true; + return true; +} + +namespace { + +// Callback passed to MigrateToSpecifics for the v68->v69 migration. See +// MigrateVersion68To69(). +void EncodeBookmarkURLAndFavicon(SQLStatement* old_value_query, + int old_value_column, + sync_pb::EntitySpecifics* mutable_new_value) { + // Extract data from the column trio we expect. + bool old_is_bookmark_object = old_value_query->column_bool(old_value_column); + std::string old_url = old_value_query->column_string(old_value_column + 1); + std::string old_favicon; + old_value_query->column_blob_as_string(old_value_column + 2, &old_favicon); + bool old_is_dir = old_value_query->column_bool(old_value_column + 3); + + if (old_is_bookmark_object) { + sync_pb::BookmarkSpecifics* bookmark_data = + mutable_new_value->MutableExtension(sync_pb::bookmark); + if (!old_is_dir) { + bookmark_data->set_url(old_url); + bookmark_data->set_favicon(old_favicon); + } + } } +} // namespace + +bool DirectoryBackingStore::MigrateVersion68To69() { + // In Version 68, there were columns on table 'metas': + // string BOOKMARK_URL + // string SERVER_BOOKMARK_URL + // blob BOOKMARK_FAVICON + // blob SERVER_BOOKMARK_FAVICON + // In version 69, these columns went away in favor of storing + // a serialized EntrySpecifics protobuf in the columns: + // protobuf blob SPECIFICS + // protobuf blob SERVER_SPECIFICS + // For bookmarks, EntrySpecifics is extended as per + // bookmark_specifics.proto. This migration converts bookmarks from the + // former scheme to the latter scheme. + + // First, add the two new columns to the schema. + if (!AddColumn(&g_metas_columns[SPECIFICS])) + return false; + if (!AddColumn(&g_metas_columns[SERVER_SPECIFICS])) + return false; + + // Next, fold data from the old columns into the new protobuf columns. + if (!MigrateToSpecifics(("is_bookmark_object, bookmark_url, " + "bookmark_favicon, is_dir"), + "specifics", + &EncodeBookmarkURLAndFavicon)) { + return false; + } + if (!MigrateToSpecifics(("server_is_bookmark_object, " + "server_bookmark_url, " + "server_bookmark_favicon, " + "server_is_dir"), + "server_specifics", + &EncodeBookmarkURLAndFavicon)) { + return false; + } + + // Lastly, fix up the "Google Chrome" folder, which is of the TOP_LEVEL_FOLDER + // ModelType: it shouldn't have BookmarkSpecifics. + SQLStatement clear_permanent_items; + clear_permanent_items.prepare(load_dbhandle_, + "UPDATE metas SET specifics = NULL, server_specifics = NULL WHERE " + "singleton_tag IN ('google_chrome')"); + if (clear_permanent_items.step() != SQLITE_DONE) + return false; + + SetVersion(69); + needs_column_refresh_ = true; // Trigger deletion of old columns. + return true; +} + + int DirectoryBackingStore::CreateTables() { LOG(INFO) << "First run, creating tables"; // Create two little tables share_version and share_info @@ -579,9 +784,7 @@ int DirectoryBackingStore::CreateTables() { if (result != SQLITE_DONE) return result; // Create the big metas table. - string query = "CREATE TABLE metas " + ComposeCreateTableColumnSpecs - (g_metas_columns, g_metas_columns + arraysize(g_metas_columns)); - result = ExecQuery(load_dbhandle_, query.c_str()); + result = CreateMetasTable(false); if (result != SQLITE_DONE) return result; { @@ -610,4 +813,12 @@ sqlite3* DirectoryBackingStore::LazyGetSaveHandle() { return save_dbhandle_; } +int DirectoryBackingStore::CreateMetasTable(bool is_temporary) { + const char* name = is_temporary ? "temp_metas" : "metas"; + string query = "CREATE TABLE "; + query.append(name); + query.append(ComposeCreateTableColumnSpecs()); + return ExecQuery(load_dbhandle_, query.c_str()); +} + } // namespace syncable diff --git a/chrome/browser/sync/syncable/directory_backing_store.h b/chrome/browser/sync/syncable/directory_backing_store.h index a3fa5093..a4294ae 100644 --- a/chrome/browser/sync/syncable/directory_backing_store.h +++ b/chrome/browser/sync/syncable/directory_backing_store.h @@ -11,12 +11,19 @@ #include "base/file_path.h" #include "chrome/browser/sync/syncable/dir_open_result.h" #include "chrome/browser/sync/syncable/syncable.h" +#include "testing/gtest/include/gtest/gtest_prod.h" // For FRIEND_TEST extern "C" { struct sqlite3; struct sqlite3_stmt; } +class SQLStatement; + +namespace sync_pb { +class EntitySpecifics; +} + namespace syncable { struct ColumnSpec; @@ -68,10 +75,18 @@ class DirectoryBackingStore { virtual bool SaveChanges(const Directory::SaveChangesSnapshot& snapshot); private: + FRIEND_TEST(DirectoryBackingStoreTest, MigrateVersion67To68); + FRIEND_TEST(DirectoryBackingStoreTest, MigrateVersion68To69); + FRIEND_TEST(MigrationTest, ToCurrentVersion); + // General Directory initialization and load helpers. DirOpenResult InitializeTables(); // Returns an sqlite return code, usually SQLITE_DONE. int CreateTables(); + // Create 'metas' or 'temp_metas' depending on value of is_temporary. + // Returns an sqlite return code, SQLITE_DONE on success. + int CreateMetasTable(bool is_temporary); + int CreateExtendedAttributeTable(); // We don't need to load any synced and applied deleted entries, we can // in fact just purge them forever on startup. @@ -98,6 +113,9 @@ class DirectoryBackingStore { // said handle. Returns true on success, false if the sqlite open operation // did not succeed. bool OpenAndConfigureHandleHelper(sqlite3** handle) const; + // Initialize and destroy load_dbhandle_. Broken out for testing. + bool BeginLoad(); + void EndLoad(); // Lazy creation of save_dbhandle_ for use by SaveChanges code path. sqlite3* LazyGetSaveHandle(); @@ -105,6 +123,22 @@ class DirectoryBackingStore { // Drop all tables in preparation for reinitialization. void DropAllTables(); + // Migration utilities. + bool AddColumn(const ColumnSpec* column); + bool RefreshColumns(); + bool SetVersion(int version); + int GetVersion(); + bool MigrateToSpecifics(const char* old_columns, + const char* specifics_column, + void (*handler_function)( + SQLStatement* old_value_query, + int old_value_column, + sync_pb::EntitySpecifics* mutable_new_value)); + + // Individual version migrations. + bool MigrateVersion67To68(); + bool MigrateVersion68To69(); + // The handle to our sqlite on-disk store for initialization and loading, and // for saving changes periodically via SaveChanges, respectively. // TODO(timsteele): We should only have one handle here. The reason we need @@ -117,6 +151,10 @@ class DirectoryBackingStore { std::string dir_name_; FilePath backing_filepath_; + // Set to true if migration left some old columns around that need to be + // discarded. + bool needs_column_refresh_; + DISALLOW_COPY_AND_ASSIGN(DirectoryBackingStore); }; diff --git a/chrome/browser/sync/syncable/directory_backing_store_unittest.cc b/chrome/browser/sync/syncable/directory_backing_store_unittest.cc new file mode 100755 index 0000000..2bdded7 --- /dev/null +++ b/chrome/browser/sync/syncable/directory_backing_store_unittest.cc @@ -0,0 +1,523 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "testing/gtest/include/gtest/gtest.h" + +#include <string> + +#include "app/sql/connection.h" +#include "app/sql/statement.h" +#include "app/sql/transaction.h" +#include "base/file_path.h" +#include "base/scoped_ptr.h" +#include "base/scoped_temp_dir.h" +#include "base/stl_util-inl.h" +#include "chrome/browser/sync/protocol/bookmark_specifics.pb.h" +#include "chrome/browser/sync/protocol/sync.pb.h" +#include "chrome/browser/sync/syncable/directory_backing_store.h" +#include "chrome/browser/sync/syncable/directory_manager.h" +#include "chrome/browser/sync/syncable/syncable-inl.h" +#include "chrome/browser/sync/syncable/syncable.h" +#include "chrome/common/sqlite_utils.h" +#include "testing/gtest/include/gtest/gtest-param-test.h" + +namespace syncable { + +extern const int32 kCurrentDBVersion; + +class MigrationTest : public testing::TestWithParam<int> { + public: + virtual void SetUp() { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + } + + protected: + std::string GetUsername() { + return "nick@chromium.org"; + } + + FilePath GetDatabasePath() { + return temp_dir_.path().Append( + DirectoryManager::GetSyncDataDatabaseFilename()); + } + void SetUpVersion67Database(); + void SetUpVersion68Database(); + + private: + ScopedTempDir temp_dir_; +}; + +class DirectoryBackingStoreTest : public MigrationTest {}; + +void MigrationTest::SetUpVersion67Database() { + // This is a version 67 database dump whose contents were backformed from + // the contents of the version 68 database dump (the v68 migration was + // actually written first). + sql::Connection connection; + ASSERT_TRUE(connection.Open(GetDatabasePath())); + ASSERT_TRUE(connection.BeginTransaction()); + ASSERT_TRUE(connection.Execute( + "CREATE TABLE extended_attributes(metahandle bigint, key varchar(127), " + "value blob, PRIMARY KEY(metahandle, key) ON CONFLICT REPLACE);" + "CREATE TABLE metas (metahandle bigint primary key ON CONFLICT FAIL," + "base_version bigint default -1,server_version bigint default 0," + "mtime bigint default 0,server_mtime bigint default 0," + "ctime bigint default 0,server_ctime bigint default 0," + "server_position_in_parent bigint default 0," + "local_external_id bigint default 0,id varchar(255) default 'r'," + "parent_id varchar(255) default 'r'," + "server_parent_id varchar(255) default 'r'," + "prev_id varchar(255) default 'r',next_id varchar(255) default 'r'," + "is_unsynced bit default 0,is_unapplied_update bit default 0," + "is_del bit default 0,is_dir bit default 0," + "is_bookmark_object bit default 0,server_is_dir bit default 0," + "server_is_del bit default 0,server_is_bookmark_object bit default 0," + "name varchar(255), " /* COLLATE PATHNAME, */ + "unsanitized_name varchar(255)," /* COLLATE PATHNAME, */ + "non_unique_name varchar," + "server_name varchar(255)," /* COLLATE PATHNAME */ + "server_non_unique_name varchar," + "bookmark_url varchar,server_bookmark_url varchar," + "singleton_tag varchar,bookmark_favicon blob," + "server_bookmark_favicon blob);" + "INSERT INTO metas VALUES(1,-1,0,129079956640320000,0," + "129079956640320000,0,0,0,'r','r','r','r','r',0,0,0,1,0,0,0,0,NULL," + "NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL);" + "INSERT INTO metas VALUES(2,669,669,128976886618480000," + "128976886618480000,128976886618480000,128976886618480000,-2097152," + "4,'s_ID_2','s_ID_9','s_ID_9','s_ID_2','s_ID_2',0,0,1,0,1,0,1,1," + "'Deleted Item',NULL,'Deleted Item','Deleted Item','Deleted Item'," + "'http://www.google.com/','http://www.google.com/2',NULL,'AASGASGA'," + "'ASADGADGADG');" + "INSERT INTO metas VALUES(4,681,681,129002163642690000," + "129002163642690000,129002163642690000,129002163642690000,-3145728," + "3,'s_ID_4','s_ID_9','s_ID_9','s_ID_4','s_ID_4',0,0,1,0,1,0,1,1," + "'Welcome to Chromium',NULL,'Welcome to Chromium'," + "'Welcome to Chromium','Welcome to Chromium'," + "'http://www.google.com/chrome/intl/en/welcome.html'," + "'http://www.google.com/chrome/intl/en/welcome.html',NULL,NULL," + "NULL);" + "INSERT INTO metas VALUES(5,677,677,129001555500000000," + "129001555500000000,129001555500000000,129001555500000000,1048576," + "7,'s_ID_5','s_ID_9','s_ID_9','s_ID_5','s_ID_5',0,0,1,0,1,0,1,1," + "'Google',NULL,'Google','Google','Google','http://www.google.com/'," + "'http://www.google.com/',NULL,'AGASGASG','AGFDGASG');" + "INSERT INTO metas VALUES(6,694,694,129053976170000000," + "129053976170000000,129053976170000000,129053976170000000,-4194304," + "6,'s_ID_6','s_ID_9','s_ID_9','r','r',0,0,0,1,1,1,0,1," + "'The Internet',NULL,'The Internet','The Internet'," + "'The Internet',NULL,NULL,NULL,NULL,NULL);" + "INSERT INTO metas VALUES(7,663,663,128976864758480000," + "128976864758480000,128976864758480000,128976864758480000," + "1048576,0,'s_ID_7','r','r','r','r',0,0,0,1,1,1,0,1," + "'Google Chrome',NULL,'Google Chrome','Google Chrome'," + "'Google Chrome',NULL,NULL,'google_chrome',NULL,NULL);" + "INSERT INTO metas VALUES(8,664,664,128976864758480000," + "128976864758480000,128976864758480000,128976864758480000,1048576," + "0,'s_ID_8','s_ID_7','s_ID_7','r','r',0,0,0,1,1,1,0,1,'Bookmarks'," + "NULL,'Bookmarks','Bookmarks','Bookmarks',NULL,NULL," + "'google_chrome_bookmarks',NULL,NULL);" + "INSERT INTO metas VALUES(9,665,665,128976864758480000," + "128976864758480000,128976864758480000,128976864758480000," + "1048576,1,'s_ID_9','s_ID_8','s_ID_8','r','s_ID_10',0,0,0,1,1,1,0," + "1,'Bookmark Bar',NULL,'Bookmark Bar','Bookmark Bar','Bookmark Bar'," + "NULL,NULL,'bookmark_bar',NULL,NULL);" + "INSERT INTO metas VALUES(10,666,666,128976864758480000," + "128976864758480000,128976864758480000,128976864758480000,2097152," + "2,'s_ID_10','s_ID_8','s_ID_8','s_ID_9','r',0,0,0,1,1,1,0,1," + "'Other Bookmarks',NULL,'Other Bookmarks','Other Bookmarks'," + "'Other Bookmarks',NULL,NULL,'other_bookmarks'," + "NULL,NULL);" + "INSERT INTO metas VALUES(11,683,683,129079956948440000," + "129079956948440000,129079956948440000,129079956948440000,-1048576," + "8,'s_ID_11','s_ID_6','s_ID_6','r','s_ID_13',0,0,0,0,1,0,0,1," + "'Home (The Chromium Projects)',NULL,'Home (The Chromium Projects)'," + "'Home (The Chromium Projects)','Home (The Chromium Projects)'," + "'http://dev.chromium.org/','http://dev.chromium.org/other',NULL," + "'AGATWA','AFAGVASF');" + "INSERT INTO metas VALUES(12,685,685,129079957513650000," + "129079957513650000,129079957513650000,129079957513650000,0,9," + "'s_ID_12','s_ID_6','s_ID_6','s_ID_13','s_ID_14',0,0,0,1,1,1,0,1," + "'Extra Bookmarks',NULL,'Extra Bookmarks','Extra Bookmarks'," + "'Extra Bookmarks',NULL,NULL,NULL,NULL,NULL);" + "INSERT INTO metas VALUES(13,687,687,129079957985300000," + "129079957985300000,129079957985300000,129079957985300000,-917504," + "10,'s_ID_13','s_ID_6','s_ID_6','s_ID_11','s_ID_12',0,0,0,0,1,0,0," + "1,'ICANN | Internet Corporation for Assigned Names and Numbers'," + "'ICANN Internet Corporation for Assigned Names and Numbers'," + "'ICANN | Internet Corporation for Assigned Names and Numbers'," + "'ICANN | Internet Corporation for Assigned Names and Numbers'," + "'ICANN | Internet Corporation for Assigned Names and Numbers'," + "'http://www.icann.com/','http://www.icann.com/',NULL," + "'PNGAXF0AAFF','DAAFASF');" + "INSERT INTO metas VALUES(14,692,692,129079958383000000," + "129079958383000000,129079958383000000,129079958383000000,1048576," + "11,'s_ID_14','s_ID_6','s_ID_6','s_ID_12','r',0,0,0,0,1,0,0,1," + "'The WebKit Open Source Project',NULL," + "'The WebKit Open Source Project','The WebKit Open Source Project'," + "'The WebKit Open Source Project','http://webkit.org/'," + "'http://webkit.org/x',NULL,'PNGX','PNG2Y');" + "CREATE TABLE share_info (id VARCHAR(128) primary key, " + "last_sync_timestamp INT, name VARCHAR(128), " + "initial_sync_ended BIT default 0, store_birthday VARCHAR(256), " + "db_create_version VARCHAR(128), db_create_time int, " + "next_id bigint default -2, cache_guid VARCHAR(32));" + "INSERT INTO share_info VALUES('nick@chromium.org',694," + "'nick@chromium.org',1,'c27e9f59-08ca-46f8-b0cc-f16a2ed778bb'," + "'Unknown',1263522064,-65542," + "'9010788312004066376x-6609234393368420856x');" + "CREATE TABLE share_version (id VARCHAR(128) primary key, data INT);" + "INSERT INTO share_version VALUES('nick@chromium.org',68);")); + ASSERT_TRUE(connection.CommitTransaction()); +} + +void MigrationTest::SetUpVersion68Database() { + // This sets up an actual version 68 database dump. The IDs were + // canonicalized to be less huge, and the favicons were overwritten + // with random junk so that they didn't contain any unprintable + // characters. A few server URLs were tweaked so that they'd be + // different from the local URLs. Lastly, the custom collation on + // the server_non_unique_name column was removed. + sql::Connection connection; + ASSERT_TRUE(connection.Open(GetDatabasePath())); + ASSERT_TRUE(connection.BeginTransaction()); + ASSERT_TRUE(connection.Execute( + "CREATE TABLE extended_attributes(metahandle bigint, key varchar(127), " + "value blob, PRIMARY KEY(metahandle, key) ON CONFLICT REPLACE);" + "CREATE TABLE metas (metahandle bigint primary key ON CONFLICT FAIL," + "base_version bigint default -1,server_version bigint default 0," + "mtime bigint default 0,server_mtime bigint default 0," + "ctime bigint default 0,server_ctime bigint default 0," + "server_position_in_parent bigint default 0," + "local_external_id bigint default 0,id varchar(255) default 'r'," + "parent_id varchar(255) default 'r'," + "server_parent_id varchar(255) default 'r'," + "prev_id varchar(255) default 'r',next_id varchar(255) default 'r'," + "is_unsynced bit default 0,is_unapplied_update bit default 0," + "is_del bit default 0,is_dir bit default 0," + "is_bookmark_object bit default 0,server_is_dir bit default 0," + "server_is_del bit default 0," + "server_is_bookmark_object bit default 0," + "non_unique_name varchar,server_non_unique_name varchar(255)," + "bookmark_url varchar,server_bookmark_url varchar," + "singleton_tag varchar,bookmark_favicon blob," + "server_bookmark_favicon blob);" + "INSERT INTO metas VALUES(1,-1,0,129079956640320000,0," + "129079956640320000,0,0,0,'r','r','r','r','r',0,0,0,1,0,0,0,0,NULL," + "NULL,NULL,NULL,NULL,NULL,NULL);" + "INSERT INTO metas VALUES(2,669,669,128976886618480000," + "128976886618480000,128976886618480000,128976886618480000,-2097152," + "4,'s_ID_2','s_ID_9','s_ID_9','s_ID_2','s_ID_2',0,0,1,0,1,0,1,1," + "'Deleted Item','Deleted Item','http://www.google.com/'," + "'http://www.google.com/2',NULL,'AASGASGA','ASADGADGADG');" + "INSERT INTO metas VALUES(4,681,681,129002163642690000," + "129002163642690000,129002163642690000,129002163642690000,-3145728," + "3,'s_ID_4','s_ID_9','s_ID_9','s_ID_4','s_ID_4',0,0,1,0,1,0,1,1," + "'Welcome to Chromium','Welcome to Chromium'," + "'http://www.google.com/chrome/intl/en/welcome.html'," + "'http://www.google.com/chrome/intl/en/welcome.html',NULL,NULL," + "NULL);" + "INSERT INTO metas VALUES(5,677,677,129001555500000000," + "129001555500000000,129001555500000000,129001555500000000,1048576," + "7,'s_ID_5','s_ID_9','s_ID_9','s_ID_5','s_ID_5',0,0,1,0,1,0,1,1," + "'Google','Google','http://www.google.com/'," + "'http://www.google.com/',NULL,'AGASGASG','AGFDGASG');" + "INSERT INTO metas VALUES(6,694,694,129053976170000000," + "129053976170000000,129053976170000000,129053976170000000,-4194304," + "6,'s_ID_6','s_ID_9','s_ID_9','r','r',0,0,0,1,1,1,0,1," + "'The Internet','The Internet',NULL,NULL,NULL,NULL,NULL);" + "INSERT INTO metas VALUES(7,663,663,128976864758480000," + "128976864758480000,128976864758480000,128976864758480000," + "1048576,0,'s_ID_7','r','r','r','r',0,0,0,1,1,1,0,1," + "'Google Chrome','Google Chrome',NULL,NULL,'google_chrome',NULL," + "NULL);" + "INSERT INTO metas VALUES(8,664,664,128976864758480000," + "128976864758480000,128976864758480000,128976864758480000,1048576," + "0,'s_ID_8','s_ID_7','s_ID_7','r','r',0,0,0,1,1,1,0,1,'Bookmarks'," + "'Bookmarks',NULL,NULL,'google_chrome_bookmarks',NULL,NULL);" + "INSERT INTO metas VALUES(9,665,665,128976864758480000," + "128976864758480000,128976864758480000,128976864758480000," + "1048576,1,'s_ID_9','s_ID_8','s_ID_8','r','s_ID_10',0,0,0,1,1,1,0," + "1,'Bookmark Bar','Bookmark Bar',NULL,NULL,'bookmark_bar',NULL," + "NULL);" + "INSERT INTO metas VALUES(10,666,666,128976864758480000," + "128976864758480000,128976864758480000,128976864758480000,2097152," + "2,'s_ID_10','s_ID_8','s_ID_8','s_ID_9','r',0,0,0,1,1,1,0,1," + "'Other Bookmarks','Other Bookmarks',NULL,NULL,'other_bookmarks'," + "NULL,NULL);" + "INSERT INTO metas VALUES(11,683,683,129079956948440000," + "129079956948440000,129079956948440000,129079956948440000,-1048576," + "8,'s_ID_11','s_ID_6','s_ID_6','r','s_ID_13',0,0,0,0,1,0,0,1," + "'Home (The Chromium Projects)','Home (The Chromium Projects)'," + "'http://dev.chromium.org/','http://dev.chromium.org/other',NULL," + "'AGATWA','AFAGVASF');" + "INSERT INTO metas VALUES(12,685,685,129079957513650000," + "129079957513650000,129079957513650000,129079957513650000,0,9," + "'s_ID_12','s_ID_6','s_ID_6','s_ID_13','s_ID_14',0,0,0,1,1,1,0,1," + "'Extra Bookmarks','Extra Bookmarks',NULL,NULL,NULL,NULL,NULL);" + "INSERT INTO metas VALUES(13,687,687,129079957985300000," + "129079957985300000,129079957985300000,129079957985300000,-917504," + "10,'s_ID_13','s_ID_6','s_ID_6','s_ID_11','s_ID_12',0,0,0,0,1,0,0," + "1,'ICANN | Internet Corporation for Assigned Names and Numbers'," + "'ICANN | Internet Corporation for Assigned Names and Numbers'," + "'http://www.icann.com/','http://www.icann.com/',NULL," + "'PNGAXF0AAFF','DAAFASF');" + "INSERT INTO metas VALUES(14,692,692,129079958383000000," + "129079958383000000,129079958383000000,129079958383000000,1048576," + "11,'s_ID_14','s_ID_6','s_ID_6','s_ID_12','r',0,0,0,0,1,0,0,1," + "'The WebKit Open Source Project','The WebKit Open Source Project'," + "'http://webkit.org/','http://webkit.org/x',NULL,'PNGX','PNG2Y');" + "CREATE TABLE share_info (id VARCHAR(128) primary key, " + "last_sync_timestamp INT, name VARCHAR(128), " + "initial_sync_ended BIT default 0, store_birthday VARCHAR(256), " + "db_create_version VARCHAR(128), db_create_time int, " + "next_id bigint default -2, cache_guid VARCHAR(32));" + "INSERT INTO share_info VALUES('nick@chromium.org',694," + "'nick@chromium.org',1,'c27e9f59-08ca-46f8-b0cc-f16a2ed778bb'," + "'Unknown',1263522064,-65542," + "'9010788312004066376x-6609234393368420856x');" + "CREATE TABLE share_version (id VARCHAR(128) primary key, data INT);" + "INSERT INTO share_version VALUES('nick@chromium.org',68);")); + ASSERT_TRUE(connection.CommitTransaction()); +} + +TEST_F(DirectoryBackingStoreTest, MigrateVersion67To68) { + SetUpVersion67Database(); + + { + sql::Connection connection; + ASSERT_TRUE(connection.Open(GetDatabasePath())); + + // Columns existing befor version 67. + ASSERT_TRUE(connection.DoesColumnExist("metas", "name")); + ASSERT_TRUE(connection.DoesColumnExist("metas", "unsanitized_name")); + ASSERT_TRUE(connection.DoesColumnExist("metas", "server_name")); + } + + scoped_ptr<DirectoryBackingStore> dbs( + new DirectoryBackingStore(GetUsername(), GetDatabasePath())); + + dbs->BeginLoad(); + ASSERT_FALSE(dbs->needs_column_refresh_); + ASSERT_TRUE(dbs->MigrateVersion67To68()); + ASSERT_EQ(68, dbs->GetVersion()); + dbs->EndLoad(); + ASSERT_TRUE(dbs->needs_column_refresh_); +} + +TEST_F(DirectoryBackingStoreTest, MigrateVersion68To69) { + SetUpVersion68Database(); + + scoped_ptr<DirectoryBackingStore> dbs( + new DirectoryBackingStore(GetUsername(), GetDatabasePath())); + + dbs->BeginLoad(); + ASSERT_FALSE(dbs->needs_column_refresh_); + ASSERT_TRUE(dbs->MigrateVersion68To69()); + ASSERT_EQ(69, dbs->GetVersion()); + dbs->EndLoad(); + ASSERT_TRUE(dbs->needs_column_refresh_); + + sql::Connection connection; + ASSERT_TRUE(connection.Open(GetDatabasePath())); + ASSERT_TRUE(connection.DoesColumnExist("metas", "specifics")); + ASSERT_TRUE(connection.DoesColumnExist("metas", "server_specifics")); + sql::Statement s(connection.GetUniqueStatement("SELECT non_unique_name," + "is_del, is_dir, id, specifics, server_specifics FROM metas " + "WHERE metahandle = 2")); + ASSERT_TRUE(s.Step()); + ASSERT_EQ("Deleted Item", s.ColumnString(0)); + ASSERT_TRUE(s.ColumnBool(1)); + ASSERT_FALSE(s.ColumnBool(2)); + ASSERT_EQ("s_ID_2", s.ColumnString(3)); + sync_pb::EntitySpecifics specifics; + specifics.ParseFromArray(s.ColumnBlob(4), s.ColumnByteLength(4)); + ASSERT_TRUE(specifics.HasExtension(sync_pb::bookmark)); + ASSERT_EQ("http://www.google.com/", + specifics.GetExtension(sync_pb::bookmark).url()); + ASSERT_EQ("AASGASGA", specifics.GetExtension(sync_pb::bookmark).favicon()); + specifics.ParseFromArray(s.ColumnBlob(5), s.ColumnByteLength(5)); + ASSERT_TRUE(specifics.HasExtension(sync_pb::bookmark)); + ASSERT_EQ("http://www.google.com/2", + specifics.GetExtension(sync_pb::bookmark).url()); + ASSERT_EQ("ASADGADGADG", specifics.GetExtension(sync_pb::bookmark).favicon()); + ASSERT_FALSE(s.Step()); +} + +TEST_P(MigrationTest, ToCurrentVersion) { + switch (GetParam()) { + case 67: + SetUpVersion67Database(); + break; + case 68: + SetUpVersion68Database(); + break; + default: + FAIL() << "Need to supply database dump for version " << GetParam(); + } + + scoped_ptr<DirectoryBackingStore> dbs( + new DirectoryBackingStore(GetUsername(), GetDatabasePath())); + + dbs->BeginLoad(); + ASSERT_TRUE(OPENED == dbs->InitializeTables()); + ASSERT_FALSE(dbs->needs_column_refresh_); + ASSERT_EQ(kCurrentDBVersion, dbs->GetVersion()); + + { + sql::Connection connection; + ASSERT_TRUE(connection.Open(GetDatabasePath())); + + // Columns deleted in Version 67. + ASSERT_FALSE(connection.DoesColumnExist("metas", "name")); + ASSERT_FALSE(connection.DoesColumnExist("metas", "unsanitized_name")); + ASSERT_FALSE(connection.DoesColumnExist("metas", "server_name")); + + // Columns added in Version 68. + ASSERT_TRUE(connection.DoesColumnExist("metas", "specifics")); + ASSERT_TRUE(connection.DoesColumnExist("metas", "server_specifics")); + + // Columns deleted in Version 68. + ASSERT_FALSE(connection.DoesColumnExist("metas", "is_bookmark_object")); + ASSERT_FALSE(connection.DoesColumnExist("metas", + "server_is_bookmark_object")); + ASSERT_FALSE(connection.DoesColumnExist("metas", "bookmark_favicon")); + ASSERT_FALSE(connection.DoesColumnExist("metas", "bookmark_url")); + ASSERT_FALSE(connection.DoesColumnExist("metas", "server_bookmark_url")); + } + + MetahandlesIndex index; + dbs->LoadEntries(&index); + dbs->EndLoad(); + + MetahandlesIndex::iterator it = index.begin(); + ASSERT_TRUE(it != index.end()); + ASSERT_EQ(1, (*it)->ref(META_HANDLE)); + EXPECT_TRUE((*it)->ref(ID).IsRoot()); + + ASSERT_TRUE(++it != index.end()) << "Upgrade destroyed database contents."; + ASSERT_EQ(2, (*it)->ref(META_HANDLE)); + EXPECT_TRUE((*it)->ref(IS_DEL)); + EXPECT_TRUE((*it)->ref(SERVER_IS_DEL)); + EXPECT_TRUE((*it)->ref(SPECIFICS).HasExtension(sync_pb::bookmark)); + EXPECT_TRUE((*it)->ref(SERVER_SPECIFICS).HasExtension(sync_pb::bookmark)); + EXPECT_EQ("http://www.google.com/", + (*it)->ref(SPECIFICS).GetExtension(sync_pb::bookmark).url()); + EXPECT_EQ("AASGASGA", + (*it)->ref(SPECIFICS).GetExtension(sync_pb::bookmark).favicon()); + EXPECT_EQ("http://www.google.com/2", + (*it)->ref(SERVER_SPECIFICS).GetExtension(sync_pb::bookmark).url()); + EXPECT_EQ("ASADGADGADG", + (*it)->ref(SERVER_SPECIFICS).GetExtension(sync_pb::bookmark).favicon()); + EXPECT_EQ("", (*it)->ref(SINGLETON_TAG)); + EXPECT_EQ("Deleted Item", (*it)->ref(NON_UNIQUE_NAME)); + EXPECT_EQ("Deleted Item", (*it)->ref(SERVER_NON_UNIQUE_NAME)); + + ASSERT_TRUE(++it != index.end()); + ASSERT_EQ(4, (*it)->ref(META_HANDLE)); + EXPECT_TRUE((*it)->ref(IS_DEL)); + EXPECT_TRUE((*it)->ref(SERVER_IS_DEL)); + + ASSERT_TRUE(++it != index.end()); + ASSERT_EQ(5, (*it)->ref(META_HANDLE)); + EXPECT_TRUE((*it)->ref(IS_DEL)); + EXPECT_TRUE((*it)->ref(SERVER_IS_DEL)); + + ASSERT_TRUE(++it != index.end()); + ASSERT_EQ(6, (*it)->ref(META_HANDLE)); + EXPECT_TRUE((*it)->ref(IS_DIR)); + EXPECT_TRUE((*it)->ref(SERVER_IS_DIR)); + EXPECT_FALSE( + (*it)->ref(SPECIFICS).GetExtension(sync_pb::bookmark).has_url()); + EXPECT_FALSE( + (*it)->ref(SERVER_SPECIFICS).GetExtension(sync_pb::bookmark).has_url()); + EXPECT_FALSE( + (*it)->ref(SPECIFICS).GetExtension(sync_pb::bookmark).has_favicon()); + EXPECT_FALSE((*it)->ref(SERVER_SPECIFICS). + GetExtension(sync_pb::bookmark).has_favicon()); + + ASSERT_TRUE(++it != index.end()); + ASSERT_EQ(7, (*it)->ref(META_HANDLE)); + EXPECT_EQ("google_chrome", (*it)->ref(SINGLETON_TAG)); + EXPECT_FALSE((*it)->ref(SPECIFICS).HasExtension(sync_pb::bookmark)); + EXPECT_FALSE((*it)->ref(SERVER_SPECIFICS).HasExtension(sync_pb::bookmark)); + + ASSERT_TRUE(++it != index.end()); + ASSERT_EQ(8, (*it)->ref(META_HANDLE)); + EXPECT_EQ("google_chrome_bookmarks", (*it)->ref(SINGLETON_TAG)); + EXPECT_TRUE((*it)->ref(SPECIFICS).HasExtension(sync_pb::bookmark)); + EXPECT_TRUE((*it)->ref(SERVER_SPECIFICS).HasExtension(sync_pb::bookmark)); + + ASSERT_TRUE(++it != index.end()); + ASSERT_EQ(9, (*it)->ref(META_HANDLE)); + EXPECT_EQ("bookmark_bar", (*it)->ref(SINGLETON_TAG)); + EXPECT_TRUE((*it)->ref(SPECIFICS).HasExtension(sync_pb::bookmark)); + EXPECT_TRUE((*it)->ref(SERVER_SPECIFICS).HasExtension(sync_pb::bookmark)); + + ASSERT_TRUE(++it != index.end()); + ASSERT_EQ(10, (*it)->ref(META_HANDLE)); + EXPECT_FALSE((*it)->ref(IS_DEL)); + EXPECT_TRUE((*it)->ref(SPECIFICS).HasExtension(sync_pb::bookmark)); + EXPECT_TRUE((*it)->ref(SERVER_SPECIFICS).HasExtension(sync_pb::bookmark)); + EXPECT_FALSE((*it)->ref(SPECIFICS).GetExtension(sync_pb::bookmark).has_url()); + EXPECT_FALSE( + (*it)->ref(SPECIFICS).GetExtension(sync_pb::bookmark).has_favicon()); + EXPECT_FALSE( + (*it)->ref(SERVER_SPECIFICS).GetExtension(sync_pb::bookmark).has_url()); + EXPECT_FALSE((*it)->ref(SERVER_SPECIFICS). + GetExtension(sync_pb::bookmark).has_favicon()); + EXPECT_EQ("other_bookmarks", (*it)->ref(SINGLETON_TAG)); + EXPECT_EQ("Other Bookmarks", (*it)->ref(NON_UNIQUE_NAME)); + EXPECT_EQ("Other Bookmarks", (*it)->ref(SERVER_NON_UNIQUE_NAME)); + + ASSERT_TRUE(++it != index.end()); + ASSERT_EQ(11, (*it)->ref(META_HANDLE)); + EXPECT_FALSE((*it)->ref(IS_DEL)); + EXPECT_FALSE((*it)->ref(IS_DIR)); + EXPECT_TRUE((*it)->ref(SPECIFICS).HasExtension(sync_pb::bookmark)); + EXPECT_TRUE((*it)->ref(SERVER_SPECIFICS).HasExtension(sync_pb::bookmark)); + EXPECT_EQ("http://dev.chromium.org/", + (*it)->ref(SPECIFICS).GetExtension(sync_pb::bookmark).url()); + EXPECT_EQ("AGATWA", + (*it)->ref(SPECIFICS).GetExtension(sync_pb::bookmark).favicon()); + EXPECT_EQ("http://dev.chromium.org/other", + (*it)->ref(SERVER_SPECIFICS).GetExtension(sync_pb::bookmark).url()); + EXPECT_EQ("AFAGVASF", + (*it)->ref(SERVER_SPECIFICS).GetExtension(sync_pb::bookmark).favicon()); + EXPECT_EQ("", (*it)->ref(SINGLETON_TAG)); + EXPECT_EQ("Home (The Chromium Projects)", (*it)->ref(NON_UNIQUE_NAME)); + EXPECT_EQ("Home (The Chromium Projects)", (*it)->ref(SERVER_NON_UNIQUE_NAME)); + + ASSERT_TRUE(++it != index.end()); + ASSERT_EQ(12, (*it)->ref(META_HANDLE)); + EXPECT_FALSE((*it)->ref(IS_DEL)); + EXPECT_TRUE((*it)->ref(IS_DIR)); + EXPECT_EQ("Extra Bookmarks", (*it)->ref(NON_UNIQUE_NAME)); + EXPECT_EQ("Extra Bookmarks", (*it)->ref(SERVER_NON_UNIQUE_NAME)); + EXPECT_TRUE((*it)->ref(SPECIFICS).HasExtension(sync_pb::bookmark)); + EXPECT_TRUE((*it)->ref(SERVER_SPECIFICS).HasExtension(sync_pb::bookmark)); + EXPECT_FALSE( + (*it)->ref(SPECIFICS).GetExtension(sync_pb::bookmark).has_url()); + EXPECT_FALSE( + (*it)->ref(SERVER_SPECIFICS).GetExtension(sync_pb::bookmark).has_url()); + EXPECT_FALSE( + (*it)->ref(SPECIFICS).GetExtension(sync_pb::bookmark).has_favicon()); + EXPECT_FALSE((*it)->ref(SERVER_SPECIFICS). + GetExtension(sync_pb::bookmark).has_favicon()); + + ASSERT_TRUE(++it != index.end()); + ASSERT_EQ(13, (*it)->ref(META_HANDLE)); + + ASSERT_TRUE(++it != index.end()); + ASSERT_EQ(14, (*it)->ref(META_HANDLE)); + + ASSERT_TRUE(++it == index.end()); + + STLDeleteElements(&index); +} + +INSTANTIATE_TEST_CASE_P(DirectoryBackingStore, MigrationTest, + testing::Range(67, kCurrentDBVersion)); + +} // namespace syncable diff --git a/chrome/browser/sync/syncable/model_type.h b/chrome/browser/sync/syncable/model_type.h index 7bd9826..cc00f4c4 100644 --- a/chrome/browser/sync/syncable/model_type.h +++ b/chrome/browser/sync/syncable/model_type.h @@ -2,6 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// Enumerate the various item subtypes that are supported by sync. +// Each sync object is expected to have an immutable object type. +// An object's type is inferred from the type of data it holds. + #ifndef CHROME_BROWSER_SYNC_SYNCABLE_MODEL_TYPE_H_ #define CHROME_BROWSER_SYNC_SYNCABLE_MODEL_TYPE_H_ @@ -10,9 +14,14 @@ namespace syncable { enum ModelType { - UNSPECIFIED = 0, - BOOKMARKS, - MODEL_TYPE_COUNT + UNSPECIFIED, // Object type unknown. Objects may transition through + // the unknown state during their initial creation, before + // their properties are set. After deletion, object types + // are generally preserved. + TOP_LEVEL_FOLDER, // A permanent folder whose children may be of mixed + // datatypes (e.g. the "Google Chrome" folder). + BOOKMARKS, // A bookmark folder or a bookmark URL object. + MODEL_TYPE_COUNT, }; inline ModelType ModelTypeFromInt(int i) { @@ -24,4 +33,3 @@ inline ModelType ModelTypeFromInt(int i) { } // namespace syncable #endif // CHROME_BROWSER_SYNC_SYNCABLE_MODEL_TYPE_H_ - diff --git a/chrome/browser/sync/syncable/syncable.cc b/chrome/browser/sync/syncable/syncable.cc index 6816f9c..b8c66b7 100755 --- a/chrome/browser/sync/syncable/syncable.cc +++ b/chrome/browser/sync/syncable/syncable.cc @@ -35,6 +35,7 @@ #include "base/time.h" #include "chrome/browser/sync/engine/syncer.h" #include "chrome/browser/sync/engine/syncer_util.h" +#include "chrome/browser/sync/protocol/bookmark_specifics.pb.h" #include "chrome/browser/sync/protocol/service_constants.h" #include "chrome/browser/sync/syncable/directory_backing_store.h" #include "chrome/browser/sync/syncable/directory_manager.h" @@ -396,8 +397,8 @@ void ZeroFields(EntryKernel* entry, int first_field) { entry->mutable_ref(static_cast<IdField>(i)).Clear(); for ( ; i < BIT_FIELDS_END; ++i) entry->put(static_cast<BitField>(i), false); - if (i < BLOB_FIELDS_END) - i = BLOB_FIELDS_END; + if (i < PROTO_FIELDS_END) + i = PROTO_FIELDS_END; } void Directory::InsertEntry(EntryKernel* entry) { @@ -998,6 +999,51 @@ void Entry::DeleteAllExtendedAttributes(WriteTransaction *trans) { dir()->DeleteAllExtendedAttributes(trans, kernel_->ref(META_HANDLE)); } +syncable::ModelType Entry::GetServerModelType() const { + if (Get(SERVER_SPECIFICS).HasExtension(sync_pb::bookmark)) + return BOOKMARKS; + if (IsRoot()) + return TOP_LEVEL_FOLDER; + // Loose check for server-created top-level folders that aren't + // bound to a particular model type. + if (!Get(SINGLETON_TAG).empty() && Get(SERVER_IS_DIR)) + return TOP_LEVEL_FOLDER; + + // Otherwise, we don't have a server type yet. That should only happen + // if the item is an uncommitted locally created item. + // It's possible we'll need to relax these checks in the future; they're + // just here for now as a safety measure. + DCHECK(Get(IS_UNSYNCED)); + DCHECK(Get(SERVER_VERSION) == 0); + DCHECK(Get(SERVER_IS_DEL)); + // Note: can't enforce !Get(ID).ServerKnows() here because that could + // actually happen if we hit AttemptReuniteLostCommitResponses. + return UNSPECIFIED; +} + +// Note: keep this consistent with GetModelType in syncproto.h! +syncable::ModelType Entry::GetModelType() const { + if (Get(SPECIFICS).HasExtension(sync_pb::bookmark)) + return BOOKMARKS; + if (IsRoot()) + return TOP_LEVEL_FOLDER; + // Loose check for server-created top-level folders that aren't + // bound to a particular model type. + if (!Get(SINGLETON_TAG).empty() && Get(IS_DIR)) + return TOP_LEVEL_FOLDER; + + // Otherwise, we don't have a local type yet. That should only happen + // if the item doesn't exist locally -- like if it's a new update item. + // It's possible we'll need to relax these checks in the future; they're + // just here for now as a safety measure. + DCHECK(Get(ID).ServerKnows()); + DCHECK(Get(IS_UNAPPLIED_UPDATE)); + DCHECK(!Get(IS_UNSYNCED)); + DCHECK(Get(BASE_VERSION) == CHANGES_VERSION); + DCHECK(Get(IS_DEL)); + return UNSPECIFIED; +} + /////////////////////////////////////////////////////////////////////////// // MutableEntry @@ -1176,9 +1222,6 @@ void MutableEntry::UnlinkFromOrder() { } bool MutableEntry::PutPredecessor(const Id& predecessor_id) { - // TODO(ncarter): Maybe there should be an independent HAS_POSITION bit? - if (!Get(IS_BOOKMARK_OBJECT)) - return true; UnlinkFromOrder(); if (Get(IS_DEL)) { @@ -1186,6 +1229,15 @@ bool MutableEntry::PutPredecessor(const Id& predecessor_id) { return true; } + // TODO(ncarter): It should be possible to not maintain position for + // non-bookmark items. However, we'd need to robustly handle all possible + // permutations of setting IS_DEL and the SPECIFICS to identify the + // object type; or else, we'd need to add a ModelType to the + // MutableEntry's Create ctor. + // if (!ShouldMaintainPosition()) { + // return false; + // } + // This is classic insert-into-doubly-linked-list from CS 101 and your last // job interview. An "IsRoot" Id signifies the head or tail. Id successor_id; @@ -1356,22 +1408,22 @@ inline FastDump& operator<<(FastDump& dump, const DumpColon&) { std::ostream& operator<<(std::ostream& stream, const syncable::Entry& entry) { // Using ostreams directly here is dreadfully slow, because a mutex is // acquired for every <<. Users noticed it spiking CPU. - using syncable::BitField; - using syncable::BitTemp; - using syncable::BlobField; - using syncable::EntryKernel; - using syncable::g_metas_columns; - using syncable::IdField; - using syncable::Int64Field; - using syncable::StringField; using syncable::BEGIN_FIELDS; using syncable::BIT_FIELDS_END; using syncable::BIT_TEMPS_BEGIN; using syncable::BIT_TEMPS_END; - using syncable::BLOB_FIELDS_END; - using syncable::INT64_FIELDS_END; + using syncable::BitField; + using syncable::BitTemp; + using syncable::EntryKernel; using syncable::ID_FIELDS_END; + using syncable::INT64_FIELDS_END; + using syncable::IdField; + using syncable::Int64Field; + using syncable::PROTO_FIELDS_END; + using syncable::ProtoField; using syncable::STRING_FIELDS_END; + using syncable::StringField; + using syncable::g_metas_columns; int i; FastDump s(&stream); @@ -1393,9 +1445,10 @@ std::ostream& operator<<(std::ostream& stream, const syncable::Entry& entry) { const string& field = kernel->ref(static_cast<StringField>(i)); s << g_metas_columns[i].name << colon << field << separator; } - for ( ; i < BLOB_FIELDS_END; ++i) { + for ( ; i < PROTO_FIELDS_END; ++i) { s << g_metas_columns[i].name << colon - << kernel->ref(static_cast<BlobField>(i)) << separator; + << kernel->ref(static_cast<ProtoField>(i)).SerializeAsString() + << separator; } s << "TempFlags: "; for ( ; i < BIT_TEMPS_END; ++i) { diff --git a/chrome/browser/sync/syncable/syncable.h b/chrome/browser/sync/syncable/syncable.h index 05b3052..0fe199e 100755 --- a/chrome/browser/sync/syncable/syncable.h +++ b/chrome/browser/sync/syncable/syncable.h @@ -24,6 +24,7 @@ #include "chrome/browser/sync/syncable/directory_event.h" #include "chrome/browser/sync/syncable/path_name_cmp.h" #include "chrome/browser/sync/syncable/syncable_id.h" +#include "chrome/browser/sync/syncable/model_type.h" #include "chrome/browser/sync/util/dbgq.h" #include "chrome/browser/sync/util/event_sys.h" #include "chrome/browser/sync/util/row_iterator.h" @@ -116,12 +117,8 @@ enum IsDelField { enum BitField { IS_DIR = IS_DEL + 1, - IS_BOOKMARK_OBJECT, - SERVER_IS_DIR, SERVER_IS_DEL, - SERVER_IS_BOOKMARK_OBJECT, - BIT_FIELDS_END }; @@ -135,9 +132,6 @@ enum StringField { NON_UNIQUE_NAME = STRING_FIELDS_BEGIN, // The server version of |NON_UNIQUE_NAME|. SERVER_NON_UNIQUE_NAME, - // For bookmark entries, the URL of the bookmark. - BOOKMARK_URL, - SERVER_BOOKMARK_URL, // A tag string which identifies this node as a particular top-level // permanent object. The tag can be thought of as a unique key that @@ -148,27 +142,25 @@ enum StringField { enum { STRING_FIELDS_COUNT = STRING_FIELDS_END - STRING_FIELDS_BEGIN, - BLOB_FIELDS_BEGIN = STRING_FIELDS_END + PROTO_FIELDS_BEGIN = STRING_FIELDS_END }; // From looking at the sqlite3 docs, it's not directly stated, but it // seems the overhead for storing a NULL blob is very small. -enum BlobField { - // For bookmark entries, the favicon data. These will be NULL for - // non-bookmark items. - BOOKMARK_FAVICON = BLOB_FIELDS_BEGIN, - SERVER_BOOKMARK_FAVICON, - BLOB_FIELDS_END, +enum ProtoField { + SPECIFICS = PROTO_FIELDS_BEGIN, + SERVER_SPECIFICS, + PROTO_FIELDS_END, }; enum { - BLOB_FIELDS_COUNT = BLOB_FIELDS_END - BLOB_FIELDS_BEGIN + PROTO_FIELDS_COUNT = PROTO_FIELDS_END - PROTO_FIELDS_BEGIN }; enum { - FIELD_COUNT = BLOB_FIELDS_END, + FIELD_COUNT = PROTO_FIELDS_END, // Past this point we have temporaries, stored in memory only. - BEGIN_TEMPS = BLOB_FIELDS_END, + BEGIN_TEMPS = PROTO_FIELDS_END, BIT_TEMPS_BEGIN = BEGIN_TEMPS, }; @@ -225,7 +217,7 @@ typedef std::set<std::string> AttributeKeySet; struct EntryKernel { private: std::string string_fields[STRING_FIELDS_COUNT]; - Blob blob_fields[BLOB_FIELDS_COUNT]; + sync_pb::EntitySpecifics specifics_fields[PROTO_FIELDS_COUNT]; int64 int64_fields[INT64_FIELDS_COUNT]; Id id_fields[ID_FIELDS_COUNT]; std::bitset<BIT_FIELDS_COUNT> bit_fields; @@ -269,8 +261,8 @@ struct EntryKernel { inline void put(StringField field, const std::string& value) { string_fields[field - STRING_FIELDS_BEGIN] = value; } - inline void put(BlobField field, const Blob& value) { - blob_fields[field - BLOB_FIELDS_BEGIN] = value; + inline void put(ProtoField field, const sync_pb::EntitySpecifics& value) { + specifics_fields[field - PROTO_FIELDS_BEGIN].CopyFrom(value); } inline void put(BitTemp field, bool value) { bit_temps[field - BIT_TEMPS_BEGIN] = value; @@ -301,8 +293,8 @@ struct EntryKernel { inline const std::string& ref(StringField field) const { return string_fields[field - STRING_FIELDS_BEGIN]; } - inline const Blob& ref(BlobField field) const { - return blob_fields[field - BLOB_FIELDS_BEGIN]; + inline const sync_pb::EntitySpecifics& ref(ProtoField field) const { + return specifics_fields[field - PROTO_FIELDS_BEGIN]; } inline bool ref(BitTemp field) const { return bit_temps[field - BIT_TEMPS_BEGIN]; @@ -312,8 +304,8 @@ struct EntryKernel { inline std::string& mutable_ref(StringField field) { return string_fields[field - STRING_FIELDS_BEGIN]; } - inline Blob& mutable_ref(BlobField field) { - return blob_fields[field - BLOB_FIELDS_BEGIN]; + inline sync_pb::EntitySpecifics& mutable_ref(ProtoField field) { + return specifics_fields[field - PROTO_FIELDS_BEGIN]; } inline Id& mutable_ref(IdField field) { return id_fields[field - ID_FIELDS_BEGIN]; @@ -369,7 +361,7 @@ class Entry { return kernel_->ref(field); } const std::string& Get(StringField field) const; - inline Blob Get(BlobField field) const { + inline const sync_pb::EntitySpecifics& Get(ProtoField field) const { DCHECK(kernel_); return kernel_->ref(field); } @@ -378,6 +370,15 @@ class Entry { return kernel_->ref(field); } + ModelType GetServerModelType() const; + ModelType GetModelType() const; + + // If this returns false, we shouldn't bother maintaining + // a position value (sibling ordering) for this item. + bool ShouldMaintainPosition() const { + return GetModelType() == BOOKMARKS; + } + inline bool ExistsOnClientBecauseNameIsNonEmpty() const { DCHECK(kernel_); return !kernel_->ref(NON_UNIQUE_NAME).empty(); @@ -444,8 +445,15 @@ class MutableEntry : public Entry { bool Put(StringField field, const std::string& value); bool Put(BaseVersion field, int64 value); - inline bool Put(BlobField field, const Blob& value) { - return PutField(field, value); + inline bool Put(ProtoField field, const sync_pb::EntitySpecifics& value) { + DCHECK(kernel_); + // TODO(ncarter): This is unfortunately heavyweight. Can we do + // better? + if (kernel_->ref(field).SerializeAsString() != value.SerializeAsString()) { + kernel_->put(field, value); + kernel_->mark_dirty(); + } + return true; } inline bool Put(BitField field, bool value) { return PutField(field, value); diff --git a/chrome/browser/sync/syncable/syncable_columns.h b/chrome/browser/sync/syncable/syncable_columns.h index 92caf64..56f0212 100755 --- a/chrome/browser/sync/syncable/syncable_columns.h +++ b/chrome/browser/sync/syncable/syncable_columns.h @@ -44,21 +44,17 @@ static const ColumnSpec g_metas_columns[] = { {"is_unapplied_update", "bit default 0"}, {"is_del", "bit default 0"}, {"is_dir", "bit default 0"}, - {"is_bookmark_object", "bit default 0"}, {"server_is_dir", "bit default 0"}, {"server_is_del", "bit default 0"}, - {"server_is_bookmark_object", "bit default 0"}, ////////////////////////////////////// // Strings {"non_unique_name", "varchar"}, - {"server_non_unique_name", "varchar(255) COLLATE PATHNAME"}, - {"bookmark_url", "varchar"}, - {"server_bookmark_url", "varchar"}, + {"server_non_unique_name", "varchar(255)"}, {"singleton_tag", "varchar"}, ////////////////////////////////////// // Blobs. - {"bookmark_favicon", "blob"}, - {"server_bookmark_favicon", "blob"}, + {"specifics", "blob"}, + {"server_specifics", "blob"}, }; // At least enforce that there are equal number of column names and fields. diff --git a/chrome/browser/sync/syncable/syncable_unittest.cc b/chrome/browser/sync/syncable/syncable_unittest.cc index a03028d..407508a 100755 --- a/chrome/browser/sync/syncable/syncable_unittest.cc +++ b/chrome/browser/sync/syncable/syncable_unittest.cc @@ -34,6 +34,7 @@ #include "base/logging.h" #include "base/platform_thread.h" #include "base/scoped_ptr.h" +#include "chrome/browser/sync/protocol/bookmark_specifics.pb.h" #include "chrome/browser/sync/syncable/directory_backing_store.h" #include "chrome/browser/sync/syncable/directory_manager.h" #include "chrome/browser/sync/util/closure.h" @@ -626,7 +627,6 @@ TEST_F(SyncableDirectoryTest, TestSimpleFieldsPreservedDuringSaveChanges) { EntryKernel create_pre_save, update_pre_save; EntryKernel create_post_save, update_post_save; string create_name = "Create"; - string favicon_bytes = "PNG"; { WriteTransaction trans(dir_.get(), UNITTEST, __FILE__, __LINE__); @@ -634,10 +634,10 @@ TEST_F(SyncableDirectoryTest, TestSimpleFieldsPreservedDuringSaveChanges) { MutableEntry update(&trans, CREATE_NEW_UPDATE_ITEM, update_id); create.Put(IS_UNSYNCED, true); update.Put(IS_UNAPPLIED_UPDATE, true); - syncable::Blob fav(favicon_bytes.data(), - favicon_bytes.data() + favicon_bytes.size()); - create.Put(BOOKMARK_FAVICON, fav); - update.Put(BOOKMARK_FAVICON, fav); + sync_pb::EntitySpecifics specifics; + specifics.MutableExtension(sync_pb::bookmark)->set_favicon("PNG"); + specifics.MutableExtension(sync_pb::bookmark)->set_url("http://nowhere"); + create.Put(SPECIFICS, specifics); create_pre_save = create.GetKernelCopy(); update_pre_save = update.GetKernelCopy(); create_id = create.Get(ID); @@ -690,12 +690,12 @@ TEST_F(SyncableDirectoryTest, TestSimpleFieldsPreservedDuringSaveChanges) { update_post_save.ref((StringField)i)) << "String field #" << i << " changed during save/load"; } - for ( ; i < BLOB_FIELDS_END; ++i) { - EXPECT_EQ(create_pre_save.ref((BlobField)i), - create_post_save.ref((BlobField)i)) + for ( ; i < PROTO_FIELDS_END; ++i) { + EXPECT_EQ(create_pre_save.ref((ProtoField)i).SerializeAsString(), + create_post_save.ref((ProtoField)i).SerializeAsString()) << "Blob field #" << i << " changed during save/load"; - EXPECT_EQ(update_pre_save.ref((BlobField)i), - update_post_save.ref((BlobField)i)) + EXPECT_EQ(update_pre_save.ref((ProtoField)i).SerializeAsString(), + update_post_save.ref((ProtoField)i).SerializeAsString()) << "Blob field #" << i << " changed during save/load"; } } |