Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(611)

Side by Side Diff: sql/recovery.cc

Issue 83323005: [sql] Annotate sql::Recovery failure cases. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Rebase Created 7 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « chrome/browser/history/thumbnail_database.cc ('k') | tools/metrics/histograms/histograms.xml » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright 2013 The Chromium Authors. All rights reserved. 1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "sql/recovery.h" 5 #include "sql/recovery.h"
6 6
7 #include "base/files/file_path.h" 7 #include "base/files/file_path.h"
8 #include "base/format_macros.h" 8 #include "base/format_macros.h"
9 #include "base/logging.h" 9 #include "base/logging.h"
10 #include "base/metrics/histogram.h"
10 #include "base/metrics/sparse_histogram.h" 11 #include "base/metrics/sparse_histogram.h"
11 #include "base/strings/string_util.h" 12 #include "base/strings/string_util.h"
12 #include "base/strings/stringprintf.h" 13 #include "base/strings/stringprintf.h"
13 #include "sql/connection.h" 14 #include "sql/connection.h"
14 #include "sql/statement.h" 15 #include "sql/statement.h"
15 #include "third_party/sqlite/sqlite3.h" 16 #include "third_party/sqlite/sqlite3.h"
16 17
17 namespace sql { 18 namespace sql {
18 19
20 namespace {
21
22 enum RecoveryEventType {
23 // Init() completed successfully.
24 RECOVERY_SUCCESS_INIT = 0,
25
26 // Failed to open temporary database to recover into.
27 RECOVERY_FAILED_OPEN_TEMPORARY,
28
29 // Failed to initialize recover vtable system.
30 RECOVERY_FAILED_VIRTUAL_TABLE_INIT,
31
32 // System SQLite doesn't support vtable.
33 RECOVERY_FAILED_VIRTUAL_TABLE_SYSTEM_SQLITE,
34
35 // Failed attempting to enable writable_schema.
36 RECOVERY_FAILED_WRITABLE_SCHEMA,
37
38 // Failed to attach the corrupt database to the temporary database.
39 RECOVERY_FAILED_ATTACH,
40
41 // Backup() successfully completed.
42 RECOVERY_SUCCESS_BACKUP,
43
44 // Failed sqlite3_backup_init(). Error code in Sqlite.RecoveryHandle.
45 RECOVERY_FAILED_BACKUP_INIT,
46
47 // Failed sqlite3_backup_step(). Error code in Sqlite.RecoveryStep.
48 RECOVERY_FAILED_BACKUP_STEP,
49
50 // AutoRecoverTable() successfully completed.
51 RECOVERY_SUCCESS_AUTORECOVER,
52
53 // The target table contained a type which the code is not equipped
54 // to handle. This should only happen if things are fubar.
55 RECOVERY_FAILED_AUTORECOVER_UNRECOGNIZED_TYPE,
56
57 // The target table does not exist.
58 RECOVERY_FAILED_AUTORECOVER_MISSING_TABLE,
59
60 // The recovery virtual table creation failed.
61 RECOVERY_FAILED_AUTORECOVER_CREATE,
62
63 // Copying data from the recovery table to the target table failed.
64 RECOVERY_FAILED_AUTORECOVER_INSERT,
65
66 // Dropping the recovery virtual table failed.
67 RECOVERY_FAILED_AUTORECOVER_DROP,
68
69 // SetupMeta() successfully completed.
70 RECOVERY_SUCCESS_SETUP_META,
71
72 // Failure creating recovery meta table.
73 RECOVERY_FAILED_META_CREATE,
74
75 // GetMetaVersionNumber() successfully completed.
76 RECOVERY_SUCCESS_META_VERSION,
77
78 // Failed in querying recovery meta table.
79 RECOVERY_FAILED_META_QUERY,
80
81 // No version key in recovery meta table.
82 RECOVERY_FAILED_META_NO_VERSION,
83
84 // Always keep this at the end.
85 RECOVERY_EVENT_MAX,
86 };
87
88 void RecordRecoveryEvent(RecoveryEventType recovery_event) {
89 UMA_HISTOGRAM_ENUMERATION("Sqlite.RecoveryEvents",
90 recovery_event, RECOVERY_EVENT_MAX);
91 }
92
93 } // namespace
94
19 // static 95 // static
20 bool Recovery::FullRecoverySupported() { 96 bool Recovery::FullRecoverySupported() {
21 // TODO(shess): See comment in Init(). 97 // TODO(shess): See comment in Init().
22 #if defined(USE_SYSTEM_SQLITE) 98 #if defined(USE_SYSTEM_SQLITE)
23 return false; 99 return false;
24 #else 100 #else
25 return true; 101 return true;
26 #endif 102 #endif
27 } 103 }
28 104
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after
95 // next database access, so immediately force an access. Enabling 171 // next database access, so immediately force an access. Enabling
96 // writable_schema allows processing through certain kinds of 172 // writable_schema allows processing through certain kinds of
97 // corruption. 173 // corruption.
98 // TODO(shess): It would be better to just close the handle, but it 174 // TODO(shess): It would be better to just close the handle, but it
99 // is necessary for the final backup which rewrites things. It 175 // is necessary for the final backup which rewrites things. It
100 // might be reasonable to close then re-open the handle. 176 // might be reasonable to close then re-open the handle.
101 ignore_result(db_->Execute("PRAGMA writable_schema=1")); 177 ignore_result(db_->Execute("PRAGMA writable_schema=1"));
102 ignore_result(db_->Execute("PRAGMA locking_mode=NORMAL")); 178 ignore_result(db_->Execute("PRAGMA locking_mode=NORMAL"));
103 ignore_result(db_->Execute("SELECT COUNT(*) FROM sqlite_master")); 179 ignore_result(db_->Execute("SELECT COUNT(*) FROM sqlite_master"));
104 180
105 if (!recover_db_.OpenTemporary()) 181 // TODO(shess): If this is a common failure case, it might be
182 // possible to fall back to a memory database. But it probably
183 // implies that the SQLite tmpdir logic is busted, which could cause
184 // a variety of other random issues in our code.
185 if (!recover_db_.OpenTemporary()) {
186 RecordRecoveryEvent(RECOVERY_FAILED_OPEN_TEMPORARY);
106 return false; 187 return false;
188 }
107 189
108 // TODO(shess): Figure out a story for USE_SYSTEM_SQLITE. The 190 // TODO(shess): Figure out a story for USE_SYSTEM_SQLITE. The
109 // virtual table implementation relies on SQLite internals for some 191 // virtual table implementation relies on SQLite internals for some
110 // types and functions, which could be copied inline to make it 192 // types and functions, which could be copied inline to make it
111 // standalone. Or an alternate implementation could try to read 193 // standalone. Or an alternate implementation could try to read
112 // through errors entirely at the SQLite level. 194 // through errors entirely at the SQLite level.
113 // 195 //
114 // For now, defer to the caller. The setup will succeed, but the 196 // For now, defer to the caller. The setup will succeed, but the
115 // later CREATE VIRTUAL TABLE call will fail, at which point the 197 // later CREATE VIRTUAL TABLE call will fail, at which point the
116 // caller can fire Unrecoverable(). 198 // caller can fire Unrecoverable().
117 #if !defined(USE_SYSTEM_SQLITE) 199 #if !defined(USE_SYSTEM_SQLITE)
118 int rc = recoverVtableInit(recover_db_.db_); 200 int rc = recoverVtableInit(recover_db_.db_);
119 if (rc != SQLITE_OK) { 201 if (rc != SQLITE_OK) {
202 RecordRecoveryEvent(RECOVERY_FAILED_VIRTUAL_TABLE_INIT);
120 LOG(ERROR) << "Failed to initialize recover module: " 203 LOG(ERROR) << "Failed to initialize recover module: "
121 << recover_db_.GetErrorMessage(); 204 << recover_db_.GetErrorMessage();
122 return false; 205 return false;
123 } 206 }
207 #else
208 // If this is infrequent enough, just wire it to Raze().
209 RecordRecoveryEvent(RECOVERY_FAILED_VIRTUAL_TABLE_SYSTEM_SQLITE);
124 #endif 210 #endif
125 211
126 // Turn on |SQLITE_RecoveryMode| for the handle, which allows 212 // Turn on |SQLITE_RecoveryMode| for the handle, which allows
127 // reading certain broken databases. 213 // reading certain broken databases.
128 if (!recover_db_.Execute("PRAGMA writable_schema=1")) 214 if (!recover_db_.Execute("PRAGMA writable_schema=1")) {
215 RecordRecoveryEvent(RECOVERY_FAILED_WRITABLE_SCHEMA);
129 return false; 216 return false;
217 }
130 218
131 if (!recover_db_.AttachDatabase(db_path, "corrupt")) 219 if (!recover_db_.AttachDatabase(db_path, "corrupt")) {
220 RecordRecoveryEvent(RECOVERY_FAILED_ATTACH);
132 return false; 221 return false;
222 }
133 223
224 RecordRecoveryEvent(RECOVERY_SUCCESS_INIT);
134 return true; 225 return true;
135 } 226 }
136 227
137 bool Recovery::Backup() { 228 bool Recovery::Backup() {
138 CHECK(db_); 229 CHECK(db_);
139 CHECK(recover_db_.is_open()); 230 CHECK(recover_db_.is_open());
140 231
141 // TODO(shess): Some of the failure cases here may need further 232 // TODO(shess): Some of the failure cases here may need further
142 // exploration. Just as elsewhere, persistent problems probably 233 // exploration. Just as elsewhere, persistent problems probably
143 // need to be razed, while anything which might succeed on a future 234 // need to be razed, while anything which might succeed on a future
(...skipping 22 matching lines...) Expand all
166 // and attempt the backup again. 257 // and attempt the backup again.
167 // 258 //
168 // For now, this code attempts a best effort and records histograms 259 // For now, this code attempts a best effort and records histograms
169 // to inform future development. 260 // to inform future development.
170 261
171 // Backup the original db from the recovered db. 262 // Backup the original db from the recovered db.
172 const char* kMain = "main"; 263 const char* kMain = "main";
173 sqlite3_backup* backup = sqlite3_backup_init(db_->db_, kMain, 264 sqlite3_backup* backup = sqlite3_backup_init(db_->db_, kMain,
174 recover_db_.db_, kMain); 265 recover_db_.db_, kMain);
175 if (!backup) { 266 if (!backup) {
267 RecordRecoveryEvent(RECOVERY_FAILED_BACKUP_INIT);
268
176 // Error code is in the destination database handle. 269 // Error code is in the destination database handle.
177 int err = sqlite3_errcode(db_->db_); 270 int err = sqlite3_extended_errcode(db_->db_);
178 UMA_HISTOGRAM_SPARSE_SLOWLY("Sqlite.RecoveryHandle", err); 271 UMA_HISTOGRAM_SPARSE_SLOWLY("Sqlite.RecoveryHandle", err);
179 LOG(ERROR) << "sqlite3_backup_init() failed: " 272 LOG(ERROR) << "sqlite3_backup_init() failed: "
180 << sqlite3_errmsg(db_->db_); 273 << sqlite3_errmsg(db_->db_);
274
181 return false; 275 return false;
182 } 276 }
183 277
184 // -1 backs up the entire database. 278 // -1 backs up the entire database.
185 int rc = sqlite3_backup_step(backup, -1); 279 int rc = sqlite3_backup_step(backup, -1);
186 int pages = sqlite3_backup_pagecount(backup); 280 int pages = sqlite3_backup_pagecount(backup);
187 // TODO(shess): sqlite3_backup_finish() appears to allow returning a 281 // TODO(shess): sqlite3_backup_finish() appears to allow returning a
188 // different value from sqlite3_backup_step(). Circle back and 282 // different value from sqlite3_backup_step(). Circle back and
189 // figure out if that can usefully inform the decision of whether to 283 // figure out if that can usefully inform the decision of whether to
190 // retry or not. 284 // retry or not.
191 sqlite3_backup_finish(backup); 285 sqlite3_backup_finish(backup);
192 DCHECK_GT(pages, 0); 286 DCHECK_GT(pages, 0);
193 287
194 if (rc != SQLITE_DONE) { 288 if (rc != SQLITE_DONE) {
289 RecordRecoveryEvent(RECOVERY_FAILED_BACKUP_STEP);
195 UMA_HISTOGRAM_SPARSE_SLOWLY("Sqlite.RecoveryStep", rc); 290 UMA_HISTOGRAM_SPARSE_SLOWLY("Sqlite.RecoveryStep", rc);
196 LOG(ERROR) << "sqlite3_backup_step() failed: " 291 LOG(ERROR) << "sqlite3_backup_step() failed: "
197 << sqlite3_errmsg(db_->db_); 292 << sqlite3_errmsg(db_->db_);
198 } 293 }
199 294
200 // The destination database was locked. Give up, but leave the data 295 // The destination database was locked. Give up, but leave the data
201 // in place. Maybe it won't be locked next time. 296 // in place. Maybe it won't be locked next time.
202 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED) { 297 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED) {
203 Shutdown(POISON); 298 Shutdown(POISON);
204 return false; 299 return false;
205 } 300 }
206 301
207 // Running out of memory should be transient, retry later. 302 // Running out of memory should be transient, retry later.
208 if (rc == SQLITE_NOMEM) { 303 if (rc == SQLITE_NOMEM) {
209 Shutdown(POISON); 304 Shutdown(POISON);
210 return false; 305 return false;
211 } 306 }
212 307
213 // TODO(shess): For now, leave the original database alone, pending 308 // TODO(shess): For now, leave the original database alone, pending
214 // results from Sqlite.RecoveryStep. Some errors should probably 309 // results from Sqlite.RecoveryStep. Some errors should probably
215 // route to RAZE_AND_POISON. 310 // route to RAZE_AND_POISON.
216 if (rc != SQLITE_DONE) { 311 if (rc != SQLITE_DONE) {
217 Shutdown(POISON); 312 Shutdown(POISON);
218 return false; 313 return false;
219 } 314 }
220 315
221 // Clean up the recovery db, and terminate the main database 316 // Clean up the recovery db, and terminate the main database
222 // connection. 317 // connection.
318 RecordRecoveryEvent(RECOVERY_SUCCESS_BACKUP);
223 Shutdown(POISON); 319 Shutdown(POISON);
224 return true; 320 return true;
225 } 321 }
226 322
227 void Recovery::Shutdown(Recovery::Disposition raze) { 323 void Recovery::Shutdown(Recovery::Disposition raze) {
228 if (!db_) 324 if (!db_)
229 return; 325 return;
230 326
231 recover_db_.Close(); 327 recover_db_.Close();
232 if (raze == RAZE_AND_POISON) { 328 if (raze == RAZE_AND_POISON) {
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after
299 column_decl += " BLOB"; 395 column_decl += " BLOB";
300 } else { 396 } else {
301 // TODO(shess): AFAICT, there remain: 397 // TODO(shess): AFAICT, there remain:
302 // - contains("CLOB") -> TEXT 398 // - contains("CLOB") -> TEXT
303 // - contains("REAL") -> REAL 399 // - contains("REAL") -> REAL
304 // - contains("FLOA") -> REAL 400 // - contains("FLOA") -> REAL
305 // - contains("DOUB") -> REAL 401 // - contains("DOUB") -> REAL
306 // - other -> "NUMERIC" 402 // - other -> "NUMERIC"
307 // Just code those in as they come up. 403 // Just code those in as they come up.
308 NOTREACHED() << " Unsupported type " << column_type; 404 NOTREACHED() << " Unsupported type " << column_type;
405 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_UNRECOGNIZED_TYPE);
309 return false; 406 return false;
310 } 407 }
311 408
312 // If column has constraint "NOT NULL", then inserting NULL into 409 // If column has constraint "NOT NULL", then inserting NULL into
313 // that column will fail. If the column has a non-NULL DEFAULT 410 // that column will fail. If the column has a non-NULL DEFAULT
314 // specified, the INSERT will handle it (see below). If the 411 // specified, the INSERT will handle it (see below). If the
315 // DEFAULT is also NULL, the row must be filtered out. 412 // DEFAULT is also NULL, the row must be filtered out.
316 // TODO(shess): The above scenario applies to INSERT OR REPLACE, 413 // TODO(shess): The above scenario applies to INSERT OR REPLACE,
317 // whereas INSERT OR IGNORE drops such rows. 414 // whereas INSERT OR IGNORE drops such rows.
318 // http://www.sqlite.org/lang_conflict.html 415 // http://www.sqlite.org/lang_conflict.html
(...skipping 10 matching lines...) Expand all
329 } else { 426 } else {
330 // The default value appears to be pre-quoted, as if it is 427 // The default value appears to be pre-quoted, as if it is
331 // literally from the sqlite_master CREATE statement. 428 // literally from the sqlite_master CREATE statement.
332 std::string default_value = s.ColumnString(4); 429 std::string default_value = s.ColumnString(4);
333 insert_columns.push_back(base::StringPrintf( 430 insert_columns.push_back(base::StringPrintf(
334 "IFNULL(%s,%s)", column_name.c_str(), default_value.c_str())); 431 "IFNULL(%s,%s)", column_name.c_str(), default_value.c_str()));
335 } 432 }
336 } 433 }
337 434
338 // Receiving no column information implies that the table doesn't exist. 435 // Receiving no column information implies that the table doesn't exist.
339 if (create_column_decls.empty()) 436 if (create_column_decls.empty()) {
437 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_MISSING_TABLE);
340 return false; 438 return false;
439 }
341 440
342 // If the PRIMARY KEY was a single INTEGER column, convert it to ROWID. 441 // If the PRIMARY KEY was a single INTEGER column, convert it to ROWID.
343 if (pk_column_count == 1 && !rowid_decl.empty()) 442 if (pk_column_count == 1 && !rowid_decl.empty())
344 create_column_decls[rowid_ofs] = rowid_decl; 443 create_column_decls[rowid_ofs] = rowid_decl;
345 444
346 // Additional columns accept anything. 445 // Additional columns accept anything.
347 // TODO(shess): ignoreN isn't well namespaced. But it will fail to 446 // TODO(shess): ignoreN isn't well namespaced. But it will fail to
348 // execute in case of conflicts. 447 // execute in case of conflicts.
349 for (size_t i = 0; i < extend_columns; ++i) { 448 for (size_t i = 0; i < extend_columns; ++i) {
350 create_column_decls.push_back( 449 create_column_decls.push_back(
351 base::StringPrintf("ignore%" PRIuS " ANY", i)); 450 base::StringPrintf("ignore%" PRIuS " ANY", i));
352 } 451 }
353 452
354 std::string recover_create(base::StringPrintf( 453 std::string recover_create(base::StringPrintf(
355 "CREATE VIRTUAL TABLE temp.recover_%s USING recover(corrupt.%s, %s)", 454 "CREATE VIRTUAL TABLE temp.recover_%s USING recover(corrupt.%s, %s)",
356 table_name, 455 table_name,
357 table_name, 456 table_name,
358 JoinString(create_column_decls, ',').c_str())); 457 JoinString(create_column_decls, ',').c_str()));
359 458
360 std::string recover_insert(base::StringPrintf( 459 std::string recover_insert(base::StringPrintf(
361 "INSERT OR REPLACE INTO main.%s SELECT %s FROM temp.recover_%s", 460 "INSERT OR REPLACE INTO main.%s SELECT %s FROM temp.recover_%s",
362 table_name, 461 table_name,
363 JoinString(insert_columns, ',').c_str(), 462 JoinString(insert_columns, ',').c_str(),
364 table_name)); 463 table_name));
365 464
366 std::string recover_drop(base::StringPrintf( 465 std::string recover_drop(base::StringPrintf(
367 "DROP TABLE temp.recover_%s", table_name)); 466 "DROP TABLE temp.recover_%s", table_name));
368 467
369 if (!db()->Execute(recover_create.c_str())) 468 if (!db()->Execute(recover_create.c_str())) {
469 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_CREATE);
370 return false; 470 return false;
471 }
371 472
372 if (!db()->Execute(recover_insert.c_str())) { 473 if (!db()->Execute(recover_insert.c_str())) {
474 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_INSERT);
373 ignore_result(db()->Execute(recover_drop.c_str())); 475 ignore_result(db()->Execute(recover_drop.c_str()));
374 return false; 476 return false;
375 } 477 }
376 478
377 *rows_recovered = db()->GetLastChangeCount(); 479 *rows_recovered = db()->GetLastChangeCount();
378 480
379 // TODO(shess): Is leaving the recover table around a breaker? 481 // TODO(shess): Is leaving the recover table around a breaker?
380 return db()->Execute(recover_drop.c_str()); 482 if (!db()->Execute(recover_drop.c_str())) {
483 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_DROP);
484 return false;
485 }
486 RecordRecoveryEvent(RECOVERY_SUCCESS_AUTORECOVER);
487 return true;
381 } 488 }
382 489
383 bool Recovery::SetupMeta() { 490 bool Recovery::SetupMeta() {
384 const char kCreateSql[] = 491 const char kCreateSql[] =
385 "CREATE VIRTUAL TABLE temp.recover_meta USING recover" 492 "CREATE VIRTUAL TABLE temp.recover_meta USING recover"
386 "(" 493 "("
387 "corrupt.meta," 494 "corrupt.meta,"
388 "key TEXT NOT NULL," 495 "key TEXT NOT NULL,"
389 "value ANY" // Whatever is stored. 496 "value ANY" // Whatever is stored.
390 ")"; 497 ")";
391 return db()->Execute(kCreateSql); 498 if (!db()->Execute(kCreateSql)) {
499 RecordRecoveryEvent(RECOVERY_FAILED_META_CREATE);
500 return false;
501 }
502 RecordRecoveryEvent(RECOVERY_SUCCESS_SETUP_META);
503 return true;
392 } 504 }
393 505
394 bool Recovery::GetMetaVersionNumber(int* version) { 506 bool Recovery::GetMetaVersionNumber(int* version) {
395 DCHECK(version); 507 DCHECK(version);
396 // TODO(shess): DCHECK(db()->DoesTableExist("temp.recover_meta")); 508 // TODO(shess): DCHECK(db()->DoesTableExist("temp.recover_meta"));
397 // Unfortunately, DoesTableExist() queries sqlite_master, not 509 // Unfortunately, DoesTableExist() queries sqlite_master, not
398 // sqlite_temp_master. 510 // sqlite_temp_master.
399 511
400 const char kVersionSql[] = 512 const char kVersionSql[] =
401 "SELECT value FROM temp.recover_meta WHERE key = 'version'"; 513 "SELECT value FROM temp.recover_meta WHERE key = 'version'";
402 sql::Statement recovery_version(db()->GetUniqueStatement(kVersionSql)); 514 sql::Statement recovery_version(db()->GetUniqueStatement(kVersionSql));
403 if (!recovery_version.Step()) 515 if (!recovery_version.Step()) {
516 if (!recovery_version.Succeeded()) {
517 RecordRecoveryEvent(RECOVERY_FAILED_META_QUERY);
518 } else {
519 RecordRecoveryEvent(RECOVERY_FAILED_META_NO_VERSION);
520 }
404 return false; 521 return false;
522 }
405 523
524 RecordRecoveryEvent(RECOVERY_SUCCESS_META_VERSION);
406 *version = recovery_version.ColumnInt(0); 525 *version = recovery_version.ColumnInt(0);
407 return true; 526 return true;
408 } 527 }
409 528
410 } // namespace sql 529 } // namespace sql
OLDNEW
« no previous file with comments | « chrome/browser/history/thumbnail_database.cc ('k') | tools/metrics/histograms/histograms.xml » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698