blob: f179a636546b79ced978a55dca2482d6197a7d89 [file] [log] [blame]
[email protected]8d409412013-07-19 18:25:301// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "sql/recovery.h"
6
7#include "base/files/file_path.h"
[email protected]a8848a72013-11-18 04:18:478#include "base/format_macros.h"
[email protected]8d409412013-07-19 18:25:309#include "base/logging.h"
[email protected]c7e6ac02013-11-28 05:27:4710#include "base/metrics/histogram.h"
[email protected]8d409412013-07-19 18:25:3011#include "base/metrics/sparse_histogram.h"
[email protected]a8848a72013-11-18 04:18:4712#include "base/strings/string_util.h"
13#include "base/strings/stringprintf.h"
[email protected]8d409412013-07-19 18:25:3014#include "sql/connection.h"
[email protected]a8848a72013-11-18 04:18:4715#include "sql/statement.h"
[email protected]8d409412013-07-19 18:25:3016#include "third_party/sqlite/sqlite3.h"
17
18namespace sql {
19
[email protected]c7e6ac02013-11-28 05:27:4720namespace {
21
22enum 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
88void RecordRecoveryEvent(RecoveryEventType recovery_event) {
89 UMA_HISTOGRAM_ENUMERATION("Sqlite.RecoveryEvents",
90 recovery_event, RECOVERY_EVENT_MAX);
91}
92
93} // namespace
94
[email protected]8d409412013-07-19 18:25:3095// static
[email protected]df5d95c42013-10-07 22:24:2296bool Recovery::FullRecoverySupported() {
97 // TODO(shess): See comment in Init().
98#if defined(USE_SYSTEM_SQLITE)
99 return false;
100#else
101 return true;
102#endif
103}
104
105// static
[email protected]8d409412013-07-19 18:25:30106scoped_ptr<Recovery> Recovery::Begin(
107 Connection* connection,
108 const base::FilePath& db_path) {
109 scoped_ptr<Recovery> r(new Recovery(connection));
110 if (!r->Init(db_path)) {
111 // TODO(shess): Should Init() failure result in Raze()?
112 r->Shutdown(POISON);
113 return scoped_ptr<Recovery>();
114 }
115
116 return r.Pass();
117}
118
119// static
120bool Recovery::Recovered(scoped_ptr<Recovery> r) {
121 return r->Backup();
122}
123
124// static
125void Recovery::Unrecoverable(scoped_ptr<Recovery> r) {
126 CHECK(r->db_);
127 // ~Recovery() will RAZE_AND_POISON.
128}
129
[email protected]74cdede2013-09-25 05:39:57130// static
131void Recovery::Rollback(scoped_ptr<Recovery> r) {
132 // TODO(shess): HISTOGRAM to track? Or just have people crash out?
133 // Crash and dump?
134 r->Shutdown(POISON);
135}
136
[email protected]8d409412013-07-19 18:25:30137Recovery::Recovery(Connection* connection)
138 : db_(connection),
139 recover_db_() {
140 // Result should keep the page size specified earlier.
141 if (db_->page_size_)
142 recover_db_.set_page_size(db_->page_size_);
143
144 // TODO(shess): This may not handle cases where the default page
145 // size is used, but the default has changed. I do not think this
146 // has ever happened. This could be handled by using "PRAGMA
147 // page_size", at the cost of potential additional failure cases.
148}
149
150Recovery::~Recovery() {
151 Shutdown(RAZE_AND_POISON);
152}
153
154bool Recovery::Init(const base::FilePath& db_path) {
155 // Prevent the possibility of re-entering this code due to errors
156 // which happen while executing this code.
157 DCHECK(!db_->has_error_callback());
158
159 // Break any outstanding transactions on the original database to
160 // prevent deadlocks reading through the attached version.
161 // TODO(shess): A client may legitimately wish to recover from
162 // within the transaction context, because it would potentially
163 // preserve any in-flight changes. Unfortunately, any attach-based
164 // system could not handle that. A system which manually queried
165 // one database and stored to the other possibly could, but would be
166 // more complicated.
167 db_->RollbackAllTransactions();
168
[email protected]74cdede2013-09-25 05:39:57169 // Disable exclusive locking mode so that the attached database can
170 // access things. The locking_mode change is not active until the
171 // next database access, so immediately force an access. Enabling
172 // writable_schema allows processing through certain kinds of
173 // corruption.
174 // TODO(shess): It would be better to just close the handle, but it
175 // is necessary for the final backup which rewrites things. It
176 // might be reasonable to close then re-open the handle.
177 ignore_result(db_->Execute("PRAGMA writable_schema=1"));
178 ignore_result(db_->Execute("PRAGMA locking_mode=NORMAL"));
179 ignore_result(db_->Execute("SELECT COUNT(*) FROM sqlite_master"));
180
[email protected]c7e6ac02013-11-28 05:27:47181 // 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);
[email protected]8d409412013-07-19 18:25:30187 return false;
[email protected]c7e6ac02013-11-28 05:27:47188 }
[email protected]8d409412013-07-19 18:25:30189
[email protected]dd325f052013-08-06 02:37:40190 // TODO(shess): Figure out a story for USE_SYSTEM_SQLITE. The
191 // virtual table implementation relies on SQLite internals for some
192 // types and functions, which could be copied inline to make it
193 // standalone. Or an alternate implementation could try to read
194 // through errors entirely at the SQLite level.
195 //
196 // For now, defer to the caller. The setup will succeed, but the
197 // later CREATE VIRTUAL TABLE call will fail, at which point the
198 // caller can fire Unrecoverable().
199#if !defined(USE_SYSTEM_SQLITE)
200 int rc = recoverVtableInit(recover_db_.db_);
201 if (rc != SQLITE_OK) {
[email protected]c7e6ac02013-11-28 05:27:47202 RecordRecoveryEvent(RECOVERY_FAILED_VIRTUAL_TABLE_INIT);
[email protected]dd325f052013-08-06 02:37:40203 LOG(ERROR) << "Failed to initialize recover module: "
204 << recover_db_.GetErrorMessage();
205 return false;
206 }
[email protected]c7e6ac02013-11-28 05:27:47207#else
208 // If this is infrequent enough, just wire it to Raze().
209 RecordRecoveryEvent(RECOVERY_FAILED_VIRTUAL_TABLE_SYSTEM_SQLITE);
[email protected]dd325f052013-08-06 02:37:40210#endif
211
[email protected]8d409412013-07-19 18:25:30212 // Turn on |SQLITE_RecoveryMode| for the handle, which allows
213 // reading certain broken databases.
[email protected]c7e6ac02013-11-28 05:27:47214 if (!recover_db_.Execute("PRAGMA writable_schema=1")) {
215 RecordRecoveryEvent(RECOVERY_FAILED_WRITABLE_SCHEMA);
[email protected]8d409412013-07-19 18:25:30216 return false;
[email protected]c7e6ac02013-11-28 05:27:47217 }
[email protected]8d409412013-07-19 18:25:30218
[email protected]c7e6ac02013-11-28 05:27:47219 if (!recover_db_.AttachDatabase(db_path, "corrupt")) {
220 RecordRecoveryEvent(RECOVERY_FAILED_ATTACH);
[email protected]8d409412013-07-19 18:25:30221 return false;
[email protected]c7e6ac02013-11-28 05:27:47222 }
[email protected]8d409412013-07-19 18:25:30223
[email protected]c7e6ac02013-11-28 05:27:47224 RecordRecoveryEvent(RECOVERY_SUCCESS_INIT);
[email protected]8d409412013-07-19 18:25:30225 return true;
226}
227
228bool Recovery::Backup() {
229 CHECK(db_);
230 CHECK(recover_db_.is_open());
231
232 // TODO(shess): Some of the failure cases here may need further
233 // exploration. Just as elsewhere, persistent problems probably
234 // need to be razed, while anything which might succeed on a future
235 // run probably should be allowed to try. But since Raze() uses the
236 // same approach, even that wouldn't work when this code fails.
237 //
238 // The documentation for the backup system indicate a relatively
239 // small number of errors are expected:
240 // SQLITE_BUSY - cannot lock the destination database. This should
241 // only happen if someone has another handle to the
242 // database, Chromium generally doesn't do that.
243 // SQLITE_LOCKED - someone locked the source database. Should be
244 // impossible (perhaps anti-virus could?).
245 // SQLITE_READONLY - destination is read-only.
246 // SQLITE_IOERR - since source database is temporary, probably
247 // indicates that the destination contains blocks
248 // throwing errors, or gross filesystem errors.
249 // SQLITE_NOMEM - out of memory, should be transient.
250 //
251 // AFAICT, SQLITE_BUSY and SQLITE_NOMEM could perhaps be considered
252 // transient, with SQLITE_LOCKED being unclear.
253 //
254 // SQLITE_READONLY and SQLITE_IOERR are probably persistent, with a
255 // strong chance that Raze() would not resolve them. If Delete()
256 // deletes the database file, the code could then re-open the file
257 // and attempt the backup again.
258 //
259 // For now, this code attempts a best effort and records histograms
260 // to inform future development.
261
262 // Backup the original db from the recovered db.
263 const char* kMain = "main";
264 sqlite3_backup* backup = sqlite3_backup_init(db_->db_, kMain,
265 recover_db_.db_, kMain);
266 if (!backup) {
[email protected]c7e6ac02013-11-28 05:27:47267 RecordRecoveryEvent(RECOVERY_FAILED_BACKUP_INIT);
268
[email protected]8d409412013-07-19 18:25:30269 // Error code is in the destination database handle.
[email protected]c7e6ac02013-11-28 05:27:47270 int err = sqlite3_extended_errcode(db_->db_);
[email protected]8d409412013-07-19 18:25:30271 UMA_HISTOGRAM_SPARSE_SLOWLY("Sqlite.RecoveryHandle", err);
272 LOG(ERROR) << "sqlite3_backup_init() failed: "
273 << sqlite3_errmsg(db_->db_);
[email protected]c7e6ac02013-11-28 05:27:47274
[email protected]8d409412013-07-19 18:25:30275 return false;
276 }
277
278 // -1 backs up the entire database.
279 int rc = sqlite3_backup_step(backup, -1);
280 int pages = sqlite3_backup_pagecount(backup);
281 // TODO(shess): sqlite3_backup_finish() appears to allow returning a
282 // different value from sqlite3_backup_step(). Circle back and
283 // figure out if that can usefully inform the decision of whether to
284 // retry or not.
285 sqlite3_backup_finish(backup);
286 DCHECK_GT(pages, 0);
287
288 if (rc != SQLITE_DONE) {
[email protected]c7e6ac02013-11-28 05:27:47289 RecordRecoveryEvent(RECOVERY_FAILED_BACKUP_STEP);
[email protected]8d409412013-07-19 18:25:30290 UMA_HISTOGRAM_SPARSE_SLOWLY("Sqlite.RecoveryStep", rc);
291 LOG(ERROR) << "sqlite3_backup_step() failed: "
292 << sqlite3_errmsg(db_->db_);
293 }
294
295 // The destination database was locked. Give up, but leave the data
296 // in place. Maybe it won't be locked next time.
297 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED) {
298 Shutdown(POISON);
299 return false;
300 }
301
302 // Running out of memory should be transient, retry later.
303 if (rc == SQLITE_NOMEM) {
304 Shutdown(POISON);
305 return false;
306 }
307
308 // TODO(shess): For now, leave the original database alone, pending
309 // results from Sqlite.RecoveryStep. Some errors should probably
310 // route to RAZE_AND_POISON.
311 if (rc != SQLITE_DONE) {
312 Shutdown(POISON);
313 return false;
314 }
315
316 // Clean up the recovery db, and terminate the main database
317 // connection.
[email protected]c7e6ac02013-11-28 05:27:47318 RecordRecoveryEvent(RECOVERY_SUCCESS_BACKUP);
[email protected]8d409412013-07-19 18:25:30319 Shutdown(POISON);
320 return true;
321}
322
323void Recovery::Shutdown(Recovery::Disposition raze) {
324 if (!db_)
325 return;
326
327 recover_db_.Close();
328 if (raze == RAZE_AND_POISON) {
329 db_->RazeAndClose();
330 } else if (raze == POISON) {
331 db_->Poison();
332 }
333 db_ = NULL;
334}
335
[email protected]a8848a72013-11-18 04:18:47336bool Recovery::AutoRecoverTable(const char* table_name,
337 size_t extend_columns,
338 size_t* rows_recovered) {
339 // Query the info for the recovered table in database [main].
340 std::string query(
341 base::StringPrintf("PRAGMA main.table_info(%s)", table_name));
342 Statement s(db()->GetUniqueStatement(query.c_str()));
343
344 // The columns of the recover virtual table.
345 std::vector<std::string> create_column_decls;
346
347 // The columns to select from the recover virtual table when copying
348 // to the recovered table.
349 std::vector<std::string> insert_columns;
350
351 // If PRIMARY KEY is a single INTEGER column, then it is an alias
352 // for ROWID. The primary key can be compound, so this can only be
353 // determined after processing all column data and tracking what is
354 // seen. |pk_column_count| counts the columns in the primary key.
355 // |rowid_decl| stores the ROWID version of the last INTEGER column
356 // seen, which is at |rowid_ofs| in |create_column_decls|.
357 size_t pk_column_count = 0;
[email protected]481c3e82014-07-18 01:40:47358 size_t rowid_ofs = 0; // Only valid if rowid_decl is set.
[email protected]a8848a72013-11-18 04:18:47359 std::string rowid_decl; // ROWID version of column |rowid_ofs|.
360
361 while (s.Step()) {
362 const std::string column_name(s.ColumnString(1));
363 const std::string column_type(s.ColumnString(2));
364 const bool not_null = s.ColumnBool(3);
365 const int default_type = s.ColumnType(4);
366 const bool default_is_null = (default_type == COLUMN_TYPE_NULL);
367 const int pk_column = s.ColumnInt(5);
368
Scott Hessdcf120482015-02-10 21:33:29369 // http://www.sqlite.org/pragma.html#pragma_table_info documents column 5 as
370 // the 1-based index of the column in the primary key, otherwise 0.
371 if (pk_column > 0)
[email protected]a8848a72013-11-18 04:18:47372 ++pk_column_count;
[email protected]a8848a72013-11-18 04:18:47373
374 // Construct column declaration as "name type [optional constraint]".
375 std::string column_decl = column_name;
376
377 // SQLite's affinity detection is documented at:
378 // http://www.sqlite.org/datatype3.html#affname
379 // The gist of it is that CHAR, TEXT, and INT use substring matches.
[email protected]ae4f1622013-12-08 06:49:12380 // TODO(shess): It would be nice to unit test the type handling,
381 // but it is not obvious to me how to write a test which would
382 // fail appropriately when something was broken. It would have to
383 // somehow use data which would allow detecting the various type
384 // coercions which happen. If STRICT could be enabled, type
385 // mismatches could be detected by which rows are filtered.
[email protected]a8848a72013-11-18 04:18:47386 if (column_type.find("INT") != std::string::npos) {
387 if (pk_column == 1) {
388 rowid_ofs = create_column_decls.size();
389 rowid_decl = column_name + " ROWID";
390 }
391 column_decl += " INTEGER";
392 } else if (column_type.find("CHAR") != std::string::npos ||
393 column_type.find("TEXT") != std::string::npos) {
394 column_decl += " TEXT";
395 } else if (column_type == "BLOB") {
396 column_decl += " BLOB";
[email protected]ae4f1622013-12-08 06:49:12397 } else if (column_type.find("DOUB") != std::string::npos) {
398 column_decl += " FLOAT";
[email protected]a8848a72013-11-18 04:18:47399 } else {
400 // TODO(shess): AFAICT, there remain:
401 // - contains("CLOB") -> TEXT
[email protected]ae4f1622013-12-08 06:49:12402 // - contains("REAL") -> FLOAT
403 // - contains("FLOA") -> FLOAT
[email protected]a8848a72013-11-18 04:18:47404 // - other -> "NUMERIC"
405 // Just code those in as they come up.
406 NOTREACHED() << " Unsupported type " << column_type;
[email protected]c7e6ac02013-11-28 05:27:47407 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_UNRECOGNIZED_TYPE);
[email protected]a8848a72013-11-18 04:18:47408 return false;
409 }
410
411 // If column has constraint "NOT NULL", then inserting NULL into
412 // that column will fail. If the column has a non-NULL DEFAULT
413 // specified, the INSERT will handle it (see below). If the
414 // DEFAULT is also NULL, the row must be filtered out.
415 // TODO(shess): The above scenario applies to INSERT OR REPLACE,
416 // whereas INSERT OR IGNORE drops such rows.
417 // http://www.sqlite.org/lang_conflict.html
418 if (not_null && default_is_null)
419 column_decl += " NOT NULL";
420
421 create_column_decls.push_back(column_decl);
422
423 // Per the NOTE in the header file, convert NULL values to the
424 // DEFAULT. All columns could be IFNULL(column_name,default), but
425 // the NULL case would require special handling either way.
426 if (default_is_null) {
427 insert_columns.push_back(column_name);
428 } else {
429 // The default value appears to be pre-quoted, as if it is
430 // literally from the sqlite_master CREATE statement.
431 std::string default_value = s.ColumnString(4);
432 insert_columns.push_back(base::StringPrintf(
433 "IFNULL(%s,%s)", column_name.c_str(), default_value.c_str()));
434 }
435 }
436
437 // Receiving no column information implies that the table doesn't exist.
[email protected]c7e6ac02013-11-28 05:27:47438 if (create_column_decls.empty()) {
439 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_MISSING_TABLE);
[email protected]a8848a72013-11-18 04:18:47440 return false;
[email protected]c7e6ac02013-11-28 05:27:47441 }
[email protected]a8848a72013-11-18 04:18:47442
443 // If the PRIMARY KEY was a single INTEGER column, convert it to ROWID.
444 if (pk_column_count == 1 && !rowid_decl.empty())
445 create_column_decls[rowid_ofs] = rowid_decl;
446
447 // Additional columns accept anything.
448 // TODO(shess): ignoreN isn't well namespaced. But it will fail to
449 // execute in case of conflicts.
450 for (size_t i = 0; i < extend_columns; ++i) {
451 create_column_decls.push_back(
452 base::StringPrintf("ignore%" PRIuS " ANY", i));
453 }
454
455 std::string recover_create(base::StringPrintf(
456 "CREATE VIRTUAL TABLE temp.recover_%s USING recover(corrupt.%s, %s)",
457 table_name,
458 table_name,
459 JoinString(create_column_decls, ',').c_str()));
460
461 std::string recover_insert(base::StringPrintf(
462 "INSERT OR REPLACE INTO main.%s SELECT %s FROM temp.recover_%s",
463 table_name,
464 JoinString(insert_columns, ',').c_str(),
465 table_name));
466
467 std::string recover_drop(base::StringPrintf(
468 "DROP TABLE temp.recover_%s", table_name));
469
[email protected]c7e6ac02013-11-28 05:27:47470 if (!db()->Execute(recover_create.c_str())) {
471 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_CREATE);
[email protected]a8848a72013-11-18 04:18:47472 return false;
[email protected]c7e6ac02013-11-28 05:27:47473 }
[email protected]a8848a72013-11-18 04:18:47474
475 if (!db()->Execute(recover_insert.c_str())) {
[email protected]c7e6ac02013-11-28 05:27:47476 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_INSERT);
[email protected]a8848a72013-11-18 04:18:47477 ignore_result(db()->Execute(recover_drop.c_str()));
478 return false;
479 }
480
481 *rows_recovered = db()->GetLastChangeCount();
482
483 // TODO(shess): Is leaving the recover table around a breaker?
[email protected]c7e6ac02013-11-28 05:27:47484 if (!db()->Execute(recover_drop.c_str())) {
485 RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_DROP);
486 return false;
487 }
488 RecordRecoveryEvent(RECOVERY_SUCCESS_AUTORECOVER);
489 return true;
[email protected]a8848a72013-11-18 04:18:47490}
491
492bool Recovery::SetupMeta() {
493 const char kCreateSql[] =
494 "CREATE VIRTUAL TABLE temp.recover_meta USING recover"
495 "("
496 "corrupt.meta,"
497 "key TEXT NOT NULL,"
498 "value ANY" // Whatever is stored.
499 ")";
[email protected]c7e6ac02013-11-28 05:27:47500 if (!db()->Execute(kCreateSql)) {
501 RecordRecoveryEvent(RECOVERY_FAILED_META_CREATE);
502 return false;
503 }
504 RecordRecoveryEvent(RECOVERY_SUCCESS_SETUP_META);
505 return true;
[email protected]a8848a72013-11-18 04:18:47506}
507
508bool Recovery::GetMetaVersionNumber(int* version) {
509 DCHECK(version);
510 // TODO(shess): DCHECK(db()->DoesTableExist("temp.recover_meta"));
511 // Unfortunately, DoesTableExist() queries sqlite_master, not
512 // sqlite_temp_master.
513
514 const char kVersionSql[] =
515 "SELECT value FROM temp.recover_meta WHERE key = 'version'";
516 sql::Statement recovery_version(db()->GetUniqueStatement(kVersionSql));
[email protected]c7e6ac02013-11-28 05:27:47517 if (!recovery_version.Step()) {
518 if (!recovery_version.Succeeded()) {
519 RecordRecoveryEvent(RECOVERY_FAILED_META_QUERY);
520 } else {
521 RecordRecoveryEvent(RECOVERY_FAILED_META_NO_VERSION);
522 }
[email protected]a8848a72013-11-18 04:18:47523 return false;
[email protected]c7e6ac02013-11-28 05:27:47524 }
[email protected]a8848a72013-11-18 04:18:47525
[email protected]c7e6ac02013-11-28 05:27:47526 RecordRecoveryEvent(RECOVERY_SUCCESS_META_VERSION);
[email protected]a8848a72013-11-18 04:18:47527 *version = recovery_version.ColumnInt(0);
528 return true;
529}
530
[email protected]8d409412013-07-19 18:25:30531} // namespace sql