| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #ifdef UNSAFE_BUFFERS_BUILD |
| // TODO(crbug.com/351564777): Remove this and convert code to safer constructs. |
| #pragma allow_unsafe_buffers |
| #endif |
| |
| #include"sql/database.h" |
| |
| #include<limits.h> |
| #include<stddef.h> |
| #include<stdint.h> |
| #include<string.h> |
| |
| #include<algorithm> |
| #include<cinttypes> |
| #include<memory> |
| #include<optional> |
| #include<string> |
| #include<string_view> |
| #include<tuple> |
| #include<utility> |
| #include<vector> |
| |
| #include"base/check.h" |
| #include"base/check_op.h" |
| #include"base/dcheck_is_on.h" |
| #include"base/feature_list.h" |
| #include"base/files/file_path.h" |
| #include"base/files/file_util.h" |
| #include"base/format_macros.h" |
| #include"base/functional/bind.h" |
| #include"base/location.h" |
| #include"base/logging.h" |
| #include"base/memory/raw_ptr.h" |
| #include"base/memory/scoped_refptr.h" |
| #include"base/memory/weak_ptr.h" |
| #include"base/metrics/histogram_functions.h" |
| #include"base/no_destructor.h" |
| #include"base/not_fatal_until.h" |
| #include"base/notimplemented.h" |
| #include"base/notreached.h" |
| #include"base/sequence_checker.h" |
| #include"base/strings/cstring_view.h" |
| #include"base/strings/strcat.h" |
| #include"base/strings/string_number_conversions.h" |
| #include"base/strings/string_split.h" |
| #include"base/strings/string_util.h" |
| #include"base/strings/stringprintf.h" |
| #include"base/strings/utf_string_conversions.h" |
| #include"base/synchronization/lock.h" |
| #include"base/task/single_thread_task_runner.h" |
| #include"base/threading/scoped_blocking_call.h" |
| #include"base/time/time.h" |
| #include"base/timer/elapsed_timer.h" |
| #include"base/trace_event/memory_dump_manager.h" |
| #include"base/trace_event/trace_event.h" |
| #include"base/tracing/protos/chrome_track_event.pbzero.h"// IWYU pragma: keep |
| #include"base/types/pass_key.h" |
| #include"build/build_config.h" |
| #include"sql/database_memory_dump_provider.h" |
| #include"sql/initialization.h" |
| #include"sql/internal_api_token.h" |
| #include"sql/meta_table.h" |
| #include"sql/sql_features.h" |
| #include"sql/sqlite_result_code.h" |
| #include"sql/sqlite_result_code_values.h" |
| #include"sql/statement.h" |
| #include"sql/statement_id.h" |
| #include"sql/streaming_blob_handle.h" |
| #include"sql/transaction.h" |
| #include"third_party/perfetto/include/perfetto/tracing/traced_proto.h" |
| #include"third_party/sqlite/sqlite3.h" |
| |
| #if BUILDFLAG(IS_WIN) |
| #include"base/containers/contains.h" |
| #endif |
| |
| namespace sql{ |
| |
| namespace{ |
| |
| // The name of the main database associated with a sqlite3* connection. |
| // |
| // SQLite has the ability to ATTACH multiple databases to the same connection. |
| // As a consequence, some SQLite APIs require the connection-specific database |
| // name. This is the right name to be passed to such APIs. |
| staticconstexprchar kSqliteMainDatabaseName[]="main"; |
| |
| // Magic path value telling sqlite3_open_v2() to open an in-memory database. |
| staticconstexprchar kSqliteOpenInMemoryPath[]=":memory:"; |
| |
| // Spin for up to a second waiting for the lock to clear when setting |
| // up the database. |
| // TODO(shess): Better story on this. http://crbug.com/56559 |
| constint kBusyTimeoutSeconds=1; |
| |
| constexprint kPrepareFlags= SQLITE_PREPARE_NO_VTAB; |
| |
| // RAII-style wrapper that enables `writable_schema` until it goes out of scope. |
| // No error checking on the PRAGMA statements because it is reasonable to just |
| // forge ahead in case of an error. If turning it on fails, then most likely |
| // nothing will work, whereas if turning it off fails, it only matters if some |
| // code attempts to continue working with the database and tries to modify the |
| // sqlite_schema table (none of our code does this). |
| classScopedWritableSchema{ |
| public: |
| explicitScopedWritableSchema(base::WeakPtr<Database> db) |
| : db_(std::move(db)){ |
| CHECK(db_->is_open()); |
| std::ignore= db_->Execute("PRAGMA writable_schema=1"); |
| } |
| ~ScopedWritableSchema(){ |
| // Database invalidates its WeakPtrs before closing the SQLite connection. |
| if(db_){ |
| CHECK(db_->is_open()); |
| std::ignore= db_->Execute("PRAGMA writable_schema=0"); |
| } |
| } |
| |
| private: |
| const base::WeakPtr<Database> db_; |
| }; |
| |
| // Raze() helper that uses SQLite's online backup API. |
| // |
| // Returns the SQLite error code produced by sqlite3_backup_step(). SQLITE_DONE |
| // signals success. SQLITE_OK will never be returned. |
| // |
| // The implementation is tailored for the Raze() use case. In particular, the |
| // SQLite API use and and error handling is optimized for 1-page databases. |
| SqliteResultCodeBackupDatabaseForRaze(sqlite3* source_db, |
| sqlite3* destination_db){ |
| DCHECK(source_db); |
| DCHECK(destination_db); |
| DCHECK_NE(source_db, destination_db); |
| |
| // https://www.sqlite.org/backup.html has a high-level overview of SQLite's |
| // backup support. https://www.sqlite.org/c3ref/backup_finish.html describes |
| // the API. |
| staticconstexprchar kMainDatabaseName[]="main"; |
| sqlite3_backup* backup= sqlite3_backup_init( |
| destination_db, kMainDatabaseName, source_db, kMainDatabaseName); |
| if(!backup){ |
| // sqlite3_backup_init() fails if a transaction is ongoing. In particular, |
| // SQL statements that return multiple rows keep a read transaction open |
| // until all the Step() calls are executed. |
| returnToSqliteResultCode(chrome_sqlite3_extended_errcode(destination_db)); |
| } |
| |
| constexprint kUnlimitedPageCount=-1;// Back up entire database. |
| auto sqlite_result_code= |
| ToSqliteResultCode(sqlite3_backup_step(backup, kUnlimitedPageCount)); |
| DCHECK_NE(sqlite_result_code,SqliteResultCode::kOk) |
| <<"sqlite3_backup_step() returned SQLITE_OK (instead of SQLITE_DONE) " |
| <<"when asked to back up the entire database"; |
| |
| #if DCHECK_IS_ON() |
| if(sqlite_result_code==SqliteResultCode::kDone){ |
| // If successful, exactly one page should have been backed up. |
| DCHECK_EQ(sqlite3_backup_pagecount(backup),1) |
| << __func__<<" was intended to be used with 1-page databases"; |
| } |
| #endif// DCHECK_IS_ON() |
| |
| // sqlite3_backup_finish() releases the sqlite3_backup object. |
| // |
| // It returns an error code only if the backup encountered a permanent error. |
| // We use the the sqlite3_backup_step() result instead, because it also tells |
| // us about temporary errors, like SQLITE_BUSY. |
| // |
| // We pass the sqlite3_backup_finish() result code through |
| // ToSqliteResultCode() to catch codes that should never occur, like |
| // SQLITE_MISUSE. |
| std::ignore=ToSqliteResultCode(sqlite3_backup_finish(backup)); |
| |
| return sqlite_result_code; |
| } |
| |
| boolValidAttachmentPoint(std::string_view attachment_point){ |
| // SQLite could handle a much wider character set, with appropriate quoting. |
| // |
| // Chrome's constraint is easy to remember, and sufficient for the few |
| // existing use cases. ATTACH is a discouraged feature, so no new use cases |
| // are expected. |
| return std::ranges::all_of(attachment_point, |
| [](char ch){return base::IsAsciiLower(ch);}); |
| } |
| |
| std::stringAsUTF8ForSQL(const base::FilePath& path){ |
| #if BUILDFLAG(IS_WIN) |
| return base::WideToUTF8(path.value()); |
| #elif BUILDFLAG(IS_POSIX)|| BUILDFLAG(IS_FUCHSIA) |
| return path.value(); |
| #endif |
| } |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| // LINT.IfChange(OpenDatabaseFailedReason) |
| enumclassOpenDatabaseFailedReason{ |
| kAlreadyOpened=0, |
| kIncorrectPath=1, |
| kSqliteOpenFailed=2, |
| kLockingModeFailed=3, |
| kMetadataLoadingFailed=4, |
| kPageSizeFailed=5, |
| kPragmaSynchronousFailed=6, |
| kPragmaJournalFailed=7, |
| kMaxValue= kPragmaJournalFailed |
| }; |
| |
| // LINT.ThenChange(//tools/metrics/histograms/metadata/sql/enums.xml) |
| // Reports the reason for a failure in Database::Open(...). |
| voidRecordOpenDatabaseFailureReason(const std::string& histogram_tag, |
| OpenDatabaseFailedReason reason){ |
| base::UmaHistogramEnumeration( |
| base::StrCat({"Sql.Database.Open.FailureReason.", histogram_tag}), |
| reason); |
| } |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| // LINT.IfChange(RazeDatabaseFailedReason) |
| enumclassRazeDatabaseFailedReason{ |
| kPoisoned=0, |
| kPendingTransaction=1, |
| kCantOpenInMemory=2, |
| kAutoVacuumFailed=3, |
| kSchemaFailed=4, |
| kLocked=5, |
| kTruncateFailed=6, |
| kBackupFailed=7, |
| kPageSizeFailed=8, |
| kUnknownError=9, |
| kCheckpointFailed=10, |
| kMaxValue= kCheckpointFailed |
| }; |
| |
| // LINT.ThenChange(//tools/metrics/histograms/metadata/sql/enums.xml) |
| // Reports the reason for a failure in Database::Raze(...). |
| voidRecordRazeDatabaseFailureReason(const std::string& histogram_tag, |
| RazeDatabaseFailedReason reason){ |
| base::UmaHistogramEnumeration( |
| base::StrCat({"Sql.Database.Raze.FailureReason.", histogram_tag}), |
| reason); |
| } |
| |
| }// namespace |
| |
| DatabaseOptions::DatabaseOptions()=default; |
| DatabaseOptions::DatabaseOptions(constDatabaseOptions&)=default; |
| DatabaseOptions::DatabaseOptions(DatabaseOptions&&)=default; |
| DatabaseOptions&DatabaseOptions::operator=(constDatabaseOptions&)=default; |
| DatabaseOptions&DatabaseOptions::operator=(DatabaseOptions&&)=default; |
| DatabaseOptions::~DatabaseOptions()=default; |
| |
| // static |
| Database::ScopedErrorExpecterCallback*Database::current_expecter_cb_=nullptr; |
| |
| // static |
| boolDatabase::IsExpectedSqliteError(int sqlite_error_code){ |
| DCHECK_NE(sqlite_error_code, SQLITE_OK) |
| << __func__<<" received non-error result code"; |
| DCHECK_NE(sqlite_error_code, SQLITE_DONE) |
| << __func__<<" received non-error result code"; |
| DCHECK_NE(sqlite_error_code, SQLITE_ROW) |
| << __func__<<" received non-error result code"; |
| |
| if(!current_expecter_cb_){ |
| returnfalse; |
| } |
| return current_expecter_cb_->Run(sqlite_error_code); |
| } |
| |
| // static |
| voidDatabase::SetScopedErrorExpecter( |
| Database::ScopedErrorExpecterCallback* cb, |
| base::PassKey<test::ScopedErrorExpecter>){ |
| CHECK(!current_expecter_cb_); |
| current_expecter_cb_= cb; |
| } |
| |
| // static |
| voidDatabase::ResetScopedErrorExpecter( |
| base::PassKey<test::ScopedErrorExpecter>){ |
| CHECK(current_expecter_cb_); |
| current_expecter_cb_=nullptr; |
| } |
| |
| // static |
| base::FilePathDatabase::JournalPath(const base::FilePath& db_path){ |
| return base::FilePath(db_path.value()+ FILE_PATH_LITERAL("-journal")); |
| } |
| |
| // static |
| base::FilePathDatabase::WriteAheadLogPath(const base::FilePath& db_path){ |
| return base::FilePath(db_path.value()+ FILE_PATH_LITERAL("-wal")); |
| } |
| |
| // static |
| base::FilePathDatabase::SharedMemoryFilePath(const base::FilePath& db_path){ |
| return base::FilePath(db_path.value()+ FILE_PATH_LITERAL("-shm")); |
| } |
| |
| base::WeakPtr<Database>Database::GetWeakPtr(InternalApiToken){ |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| Database::StatementRef::StatementRef(Database* database, |
| sqlite3_stmt* stmt, |
| bool was_valid) |
| : database_(database), stmt_(stmt), was_valid_(was_valid){ |
| DCHECK_EQ(database==nullptr, stmt==nullptr); |
| if(database){ |
| database_->StatementRefCreated(this); |
| } |
| } |
| |
| Database::StatementRef::~StatementRef(){ |
| if(database_){ |
| database_->StatementRefDeleted(this); |
| } |
| Close(false); |
| } |
| |
| voidDatabase::StatementRef::Reset(bool clear_bound_variables){ |
| if(clear_bound_variables){ |
| std::ignore=ToSqliteResultCode(sqlite3_clear_bindings(stmt())); |
| bound_blobs_.clear(); |
| } |
| |
| // ToSqliteResultCode() is called to ensure that sqlite3_reset() doesn't |
| // return a concerning code, such as SQLITE_MISUSE. The processed error code |
| // is ignored because sqlite3_reset() returns an error code if the last |
| // sqlite3_step() failed, and that error was already reported when we ran |
| // sqlite3_step(), via Statement::Run() or Statement::Step(). |
| std::ignore=ToSqliteResultCode(sqlite3_reset(stmt())); |
| } |
| |
| base::span<constuint8_t>Database::StatementRef::TakeBlobMemory( |
| int param_index, |
| scoped_refptr<base::RefCountedMemory> blob){ |
| auto inserted= bound_blobs_.emplace(param_index, std::move(blob)); |
| CHECK(inserted.second)<<"Parameter unexpectedly bound twice: " |
| << param_index; |
| return*inserted.first->second; |
| } |
| |
| voidDatabase::StatementRef::ClearBlobMemory(int param_index){ |
| bound_blobs_.erase(param_index); |
| } |
| |
| voidDatabase::StatementRef::Close(bool forced){ |
| if(stmt_){ |
| // Call to InitScopedBlockingCall() cannot go at the beginning of the |
| // function because Close() is called unconditionally from destructor to |
| // clean database_. And if this is inactive statement this won't cause any |
| // disk access and destructor most probably will be called on thread not |
| // allowing disk access. |
| // TODO(paivanof@gmail.com): This should move to the beginning |
| // of the function. http://crbug.com/136655. |
| std::optional<base::ScopedBlockingCall> scoped_blocking_call; |
| InitScopedBlockingCall(FROM_HERE,&scoped_blocking_call); |
| |
| // `stmt_` references memory loaned from the sqlite3 library. Stop |
| // referencing it from the raw_ptr<> before returning it. This avoids the |
| // raw_ptr<> becoming dangling. |
| sqlite3_stmt* statement= stmt_; |
| stmt_=nullptr; |
| |
| // sqlite3_finalize()'s result code is ignored because it reports the same |
| // error as the most recent sqlite3_step(). The result code is passed |
| // through ToSqliteResultCode() to catch issues like SQLITE_MISUSE. |
| std::ignore=ToSqliteResultCode(sqlite3_finalize(statement)); |
| |
| bound_blobs_.clear(); |
| } |
| database_=nullptr;// The Database may be getting deleted. |
| |
| // Forced close is expected to happen from a statement error |
| // handler. In that case maintain the sense of |was_valid_| which |
| // previously held for this ref. |
| was_valid_= was_valid_&& forced; |
| } |
| |
| static_assert(DatabaseOptions::kDefaultPageSize== SQLITE_DEFAULT_PAGE_SIZE, |
| "DatabaseOptions::kDefaultPageSize must match the value " |
| "configured into SQLite"); |
| |
| DatabaseDiagnostics::DatabaseDiagnostics()=default; |
| DatabaseDiagnostics::~DatabaseDiagnostics()=default; |
| |
| voidDatabaseDiagnostics::WriteIntoTrace( |
| perfetto::TracedProto<TraceProto> context)const{ |
| context->set_reported_sqlite_error_code(reported_sqlite_error_code); |
| context->set_error_code(error_code); |
| context->set_last_errno(last_errno); |
| context->set_sql_statement(sql_statement); |
| context->set_version(version); |
| for(constauto& sql: schema_sql_rows){ |
| context->add_schema_sql_rows(sql); |
| } |
| for(constauto& name: schema_other_row_names){ |
| context->add_schema_other_row_names(name); |
| } |
| context->set_has_valid_header(has_valid_header); |
| context->set_has_valid_schema(has_valid_schema); |
| context->set_error_message(error_message); |
| } |
| |
| Database::Database(Database::Tag tag):Database(DatabaseOptions{}, tag){} |
| |
| Database::Database(DatabaseOptions options,Database::Tag tag) |
| : options_(options), |
| mmap_disabled_(!options.mmap_enabled_), |
| histogram_tag_(tag.value), |
| tracing_track_name_(base::StrCat({"Database: ", histogram_tag_})){ |
| DCHECK_GE(options.page_size_,512); |
| DCHECK_LE(options.page_size_,65536); |
| DCHECK(!(options.page_size_&(options.page_size_-1))) |
| <<"page_size must be a power of two"; |
| DCHECK(!options_.mmap_alt_status_discouraged_|| |
| options_.enable_views_discouraged_) |
| <<"mmap_alt_status requires views"; |
| |
| // It's valid to construct a database on a sequence and then pass it to a |
| // different sequence before usage. |
| DETACH_FROM_SEQUENCE(sequence_checker_); |
| } |
| |
| Database::~Database(){ |
| Close(); |
| } |
| |
| boolDatabase::Open(const base::FilePath& path){ |
| std::string path_string=AsUTF8ForSQL(path); |
| TRACE_EVENT1("sql","Database::Open","path", path_string); |
| |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(!path.empty()); |
| DCHECK_NE(path_string, kSqliteOpenInMemoryPath) |
| <<"Path conflicts with SQLite magic identifier"; |
| |
| // Preload the database before opening it to ensure it's working with the |
| // exclusive mode. |
| if(options_.preload_){ |
| PreloadInternal(path); |
| } |
| |
| { |
| ScopedOpenErrorReporter reporter(this, |
| "Sql.Database.Open.FirstAttempt.Error"); |
| if(OpenInternal(path_string)){ |
| returntrue; |
| } |
| } |
| // OpenInternal() may have run the error callback before returning false. If |
| // the error callback poisoned `this`, the database may have been recovered or |
| // razed, so a second attempt may succeed. |
| if(poisoned_){ |
| Close(); |
| { |
| ScopedOpenErrorReporter reporter(this, |
| "Sql.Database.Open.SecondAttempt.Error"); |
| returnOpenInternal(path_string); |
| } |
| } |
| // Otherwise, do not attempt to reopen. |
| returnfalse; |
| } |
| |
| boolDatabase::OpenInMemory(){ |
| TRACE_EVENT0("sql","Database::OpenInMemory"); |
| |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| in_memory_=true; |
| returnOpenInternal(kSqliteOpenInMemoryPath); |
| } |
| |
| voidDatabase::DetachFromSequence(){ |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DETACH_FROM_SEQUENCE(sequence_checker_); |
| } |
| |
| voidDatabase::CloseInternal(bool forced){ |
| TRACE_EVENT0("sql","Database::CloseInternal"); |
| |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| CHECK_EQ(outstanding_blob_count_,0U) |
| <<"All StreamingBlobHandles should be destroyed before closing " |
| "sql::Database"; |
| |
| // TODO(shess): Calling "PRAGMA journal_mode = DELETE" at this point |
| // will delete the -journal file. For ChromiumOS or other more |
| // embedded systems, this is probably not appropriate, whereas on |
| // desktop it might make some sense. |
| |
| // sqlite3_close() needs all prepared statements to be finalized. |
| |
| // Release cached statements. |
| statement_cache_.clear(); |
| |
| // With cached statements released, in-use statements will remain. |
| // Closing the database while statements are in use is an API |
| // violation, except for forced close (which happens from within a |
| // statement's error handler). |
| DCHECK(forced|| open_statements_.empty()); |
| |
| // Deactivate any outstanding statements so sqlite3_close() works. |
| for(StatementRef* statement_ref: open_statements_){ |
| statement_ref->Close(forced); |
| } |
| open_statements_.clear(); |
| |
| if(is_open()){ |
| // Call to InitScopedBlockingCall() cannot go at the beginning of the |
| // function because Close() must be called from destructor to clean |
| // statement_cache_, it won't cause any disk access and it most probably |
| // will happen on thread not allowing disk access. |
| // TODO(paivanof@gmail.com): This should move to the beginning |
| // of the function. http://crbug.com/136655. |
| std::optional<base::ScopedBlockingCall> scoped_blocking_call; |
| InitScopedBlockingCall(FROM_HERE,&scoped_blocking_call); |
| |
| // Resetting acquires a lock to ensure no dump is happening on the database |
| // at the same time. Unregister takes ownership of provider and it is safe |
| // since the db is reset. memory_dump_provider_ could be null if db_ was |
| // poisoned. |
| if(memory_dump_provider_){ |
| memory_dump_provider_->ResetDatabase(); |
| base::trace_event::MemoryDumpManager::GetInstance() |
| ->UnregisterAndDeleteDumpProviderSoon( |
| std::move(memory_dump_provider_)); |
| } |
| |
| // Invalidate any `WeakPtr`s held by scoping helpers. |
| weak_factory_.InvalidateWeakPtrs(); |
| |
| sqlite3* raw_db= db_; |
| db_=nullptr; |
| auto sqlite_result_code=ToSqliteResultCode(sqlite3_close(raw_db)); |
| |
| CHECK_NE(sqlite_result_code,SqliteResultCode::kBusy, |
| base::NotFatalUntil::M141) |
| <<"sqlite3_close() called while resources (statements, blobs, etc) " |
| "are still alive"; |
| DCHECK_EQ(sqlite_result_code,SqliteResultCode::kOk) |
| <<"sqlite3_close() failed in an unexpected way: " |
| << sqlite3_errmsg(raw_db); |
| |
| // Closing a SQLite database connection implicitly rolls back transactions. |
| // (See https://www.sqlite.org/c3ref/close.html for details.) Callers need |
| // not call `RollbackAllTransactions()`, but we still must account for the |
| // implicit rollback in our internal bookkeeping. |
| transaction_nesting_=0; |
| } |
| } |
| |
| boolDatabase::is_open()const{ |
| returnstatic_cast<bool>(db_)&&!poisoned_; |
| } |
| |
| voidDatabase::Close(){ |
| TRACE_EVENT0("sql","Database::Close"); |
| // If the database was already closed by RazeAndPoison(), then no |
| // need to close again. Clear the |poisoned_| bit so that incorrect |
| // API calls are caught. |
| if(poisoned_){ |
| poisoned_=false; |
| return; |
| } |
| |
| CloseInternal(false); |
| } |
| |
| // SQLite keeps unused pages associated with a database in a cache. It asks |
| // the cache for pages by an id, and if the page is present and the database is |
| // unchanged, it considers the content of the page valid and doesn't read it |
| // from disk. When memory-mapped I/O is enabled, on read SQLite uses page |
| // structures created from the memory map data before consulting the cache. On |
| // write SQLite creates a new in-memory page structure, copies the data from the |
| // memory map, and later writes it, releasing the updated page back to the |
| // cache. |
| // |
| // This means that in memory-mapped mode, the contents of the cached pages are |
| // not re-used for reads, but they are re-used for writes if the re-written page |
| // is still in the cache. The implementation of sqlite3_db_release_memory() as |
| // of SQLite 3.8.7.4 frees all pages from pcaches associated with the |
| // database, so it should free these pages. |
| // |
| // Unfortunately, the zero page is also freed. That page is never accessed |
| // using memory-mapped I/O, and the cached copy can be re-used after verifying |
| // the file change counter on disk. Also, fresh pages from cache receive some |
| // pager-level initialization before they can be used. Since the information |
| // involved will immediately be accessed in various ways, it is unclear if the |
| // additional overhead is material, or just moving processor cache effects |
| // around. |
| // |
| // TODO(shess): It would be better to release the pages immediately when they |
| // are no longer needed. This would basically happen after SQLite commits a |
| // transaction. I had implemented a pcache wrapper to do this, but it involved |
| // layering violations, and it had to be setup before any other sqlite call, |
| // which was brittle. Also, for large files it would actually make sense to |
| // maintain the existing pcache behavior for blocks past the memory-mapped |
| // segment. I think drh would accept a reasonable implementation of the overall |
| // concept for upstreaming to SQLite core. |
| // |
| // TODO(shess): Another possibility would be to set the cache size small, which |
| // would keep the zero page around, plus some pre-initialized pages, and SQLite |
| // can manage things. The downside is that updates larger than the cache would |
| // spill to the journal. That could be compensated by setting cache_spill to |
| // false. The downside then is that it allows open-ended use of memory for |
| // large transactions. |
| voidDatabase::ReleaseCacheMemoryIfNeeded(bool implicit_change_performed){ |
| TRACE_EVENT0("sql","Database::ReleaseCacheMemoryIfNeeded"); |
| // The database could have been closed during a transaction as part of error |
| // recovery. |
| if(!db_){ |
| DCHECK(poisoned_)<<"Illegal use of Database without a db"; |
| return; |
| } |
| |
| // If memory-mapping is not enabled, the page cache helps performance. |
| if(!mmap_enabled_){ |
| return; |
| } |
| |
| // On caller request, force the change comparison to fail. Done before the |
| // transaction-nesting test so that the signal can carry to transaction |
| // commit. |
| if(implicit_change_performed){ |
| --total_changes_at_last_release_; |
| } |
| |
| // Cached pages may be re-used within the same transaction. |
| DCHECK_GE(transaction_nesting_,0); |
| if(transaction_nesting_){ |
| return; |
| } |
| |
| // If no changes have been made, skip flushing. This allows the first page of |
| // the database to remain in cache across multiple reads. |
| constint64_t total_changes= sqlite3_total_changes64(db_); |
| if(total_changes== total_changes_at_last_release_){ |
| return; |
| } |
| |
| total_changes_at_last_release_= total_changes; |
| |
| // Passing the result code through ToSqliteResultCode() to catch issues such |
| // as SQLITE_MISUSE. |
| std::ignore=ToSqliteResultCode(sqlite3_db_release_memory(db_)); |
| } |
| |
| base::FilePathDatabase::DbPath()const{ |
| if(!is_open()){ |
| return base::FilePath(); |
| } |
| |
| constchar* path= sqlite3_db_filename(db_,"main"); |
| if(!path){ |
| return base::FilePath(); |
| } |
| const std::string_view db_path(path); |
| #if BUILDFLAG(IS_WIN) |
| return base::FilePath(base::UTF8ToWide(db_path)); |
| #elif BUILDFLAG(IS_POSIX)|| BUILDFLAG(IS_FUCHSIA) |
| return base::FilePath(db_path); |
| #else |
| NOTREACHED(); |
| #endif |
| } |
| |
| std::stringDatabase::CollectErrorInfo(int sqlite_error_code, |
| Statement* stmt, |
| DatabaseDiagnostics* diagnostics)const{ |
| TRACE_EVENT0("sql","Database::CollectErrorInfo"); |
| |
| DCHECK_NE(sqlite_error_code, SQLITE_OK) |
| << __func__<<" received non-error result code"; |
| DCHECK_NE(sqlite_error_code, SQLITE_DONE) |
| << __func__<<" received non-error result code"; |
| DCHECK_NE(sqlite_error_code, SQLITE_ROW) |
| << __func__<<" received non-error result code"; |
| |
| // Buffer for accumulating debugging info about the error. Place |
| // more-relevant information earlier, in case things overflow the |
| // fixed-size reporting buffer. |
| std::string debug_info; |
| |
| // The error message from the failed operation. |
| int error_code=GetErrorCode(); |
| base::StringAppendF(&debug_info,"db error: %d/%s\n", error_code, |
| GetErrorMessage()); |
| if(diagnostics){ |
| diagnostics->error_code= error_code; |
| diagnostics->error_message=GetErrorMessage(); |
| } |
| |
| // TODO(shess): |error| and |GetErrorCode()| should always be the same, but |
| // reading code does not entirely convince me. Remove if they turn out to be |
| // the same. |
| if(sqlite_error_code!=GetErrorCode()){ |
| base::StringAppendF(&debug_info,"reported error: %d\n", sqlite_error_code); |
| } |
| |
| // System error information. Interpretation of Windows errors is different |
| // from posix. |
| #if BUILDFLAG(IS_WIN) |
| int last_errno=GetLastErrno(); |
| base::StringAppendF(&debug_info,"LastError: %d\n", last_errno); |
| if(diagnostics){ |
| diagnostics->last_errno= last_errno; |
| } |
| #elif BUILDFLAG(IS_POSIX)|| BUILDFLAG(IS_FUCHSIA) |
| int last_errno=GetLastErrno(); |
| base::StringAppendF(&debug_info,"errno: %d\n", last_errno); |
| if(diagnostics){ |
| diagnostics->last_errno= last_errno; |
| } |
| #else |
| NOTREACHED();// Add appropriate log info. |
| #endif |
| |
| if(stmt){ |
| std::string sql_string= stmt->GetSQLStatement(); |
| base::StringAppendF(&debug_info,"statement: %s\n", sql_string.c_str()); |
| if(diagnostics){ |
| diagnostics->sql_statement= sql_string; |
| } |
| }else{ |
| base::StringAppendF(&debug_info,"statement: NULL\n"); |
| } |
| |
| // SQLITE_ERROR often indicates some sort of mismatch between the statement |
| // and the schema, possibly due to a failed schema migration. |
| if(sqlite_error_code== SQLITE_ERROR){ |
| staticconstexprchar kVersionSql[]= |
| "SELECT value FROM meta WHERE key='version'"; |
| sqlite3_stmt* sqlite_statement; |
| // When the number of bytes passed to sqlite3_prepare_v3() includes the null |
| // terminator, SQLite avoids a buffer copy. |
| int rc= sqlite3_prepare_v3(db_, kVersionSql,sizeof(kVersionSql), |
| SQLITE_PREPARE_NO_VTAB,&sqlite_statement, |
| /* pzTail= */nullptr); |
| if(rc== SQLITE_OK){ |
| rc= sqlite3_step(sqlite_statement); |
| if(rc== SQLITE_ROW){ |
| int version= sqlite3_column_int(sqlite_statement,0); |
| base::StringAppendF(&debug_info,"version: %d\n", version); |
| if(diagnostics){ |
| diagnostics->version= version; |
| } |
| }elseif(rc== SQLITE_DONE){ |
| debug_info+="version: none\n"; |
| }else{ |
| base::StringAppendF(&debug_info,"version: error %d\n", rc); |
| } |
| sqlite3_finalize(sqlite_statement); |
| }else{ |
| base::StringAppendF(&debug_info,"version: prepare error %d\n", rc); |
| } |
| |
| // Get all the SQL from sqlite_schema. |
| debug_info+="schema:\n"; |
| staticconstexprchar kSchemaSql[]= |
| "SELECT sql FROM sqlite_schema WHERE sql IS NOT NULL ORDER BY ROWID"; |
| rc= sqlite3_prepare_v3(db_, kSchemaSql,sizeof(kSchemaSql), |
| SQLITE_PREPARE_NO_VTAB,&sqlite_statement, |
| /* pzTail= */nullptr); |
| if(rc== SQLITE_OK){ |
| while((rc= sqlite3_step(sqlite_statement))== SQLITE_ROW){ |
| std::string text; |
| base::StringAppendF(&text,"%s", |
| reinterpret_cast<constchar*>( |
| sqlite3_column_text(sqlite_statement,0))); |
| debug_info+= text+"\n"; |
| if(diagnostics){ |
| diagnostics->schema_sql_rows.push_back(text); |
| } |
| } |
| |
| if(rc!= SQLITE_DONE){ |
| base::StringAppendF(&debug_info,"error %d\n", rc); |
| } |
| sqlite3_finalize(sqlite_statement); |
| }else{ |
| base::StringAppendF(&debug_info,"prepare error %d\n", rc); |
| } |
| |
| // Automatically generated indices have a NULL 'sql' column. For those rows, |
| // we log the name column instead. |
| debug_info+="schema rows with only name:\n"; |
| staticconstexprchar kSchemaOtherRowNamesSql[]= |
| "SELECT name FROM sqlite_schema WHERE sql IS NULL ORDER BY ROWID"; |
| rc= sqlite3_prepare_v3(db_, kSchemaOtherRowNamesSql, |
| sizeof(kSchemaOtherRowNamesSql), |
| SQLITE_PREPARE_NO_VTAB,&sqlite_statement, |
| /* pzTail= */nullptr); |
| if(rc== SQLITE_OK){ |
| while((rc= sqlite3_step(sqlite_statement))== SQLITE_ROW){ |
| std::string text; |
| base::StringAppendF(&text,"%s", |
| reinterpret_cast<constchar*>( |
| sqlite3_column_text(sqlite_statement,0))); |
| debug_info+= text+"\n"; |
| if(diagnostics){ |
| diagnostics->schema_other_row_names.push_back(text); |
| } |
| } |
| |
| if(rc!= SQLITE_DONE){ |
| base::StringAppendF(&debug_info,"error %d\n", rc); |
| } |
| sqlite3_finalize(sqlite_statement); |
| }else{ |
| base::StringAppendF(&debug_info,"prepare error %d\n", rc); |
| } |
| } |
| |
| return debug_info; |
| } |
| |
| // TODO(shess): Since this is only called in an error situation, it might be |
| // prudent to rewrite in terms of SQLite API calls, and mark the function const. |
| std::stringDatabase::CollectCorruptionInfo(){ |
| TRACE_EVENT0("sql","Database::CollectCorruptionInfo"); |
| // If the file cannot be accessed it is unlikely that an integrity check will |
| // turn up actionable information. |
| const base::FilePath db_path=DbPath(); |
| std::optional<int64_t> db_size=GetFileSize(db_path); |
| if(db_size&&*db_size<0){ |
| return std::string(); |
| } |
| |
| // Buffer for accumulating debugging info about the error. Place |
| // more-relevant information earlier, in case things overflow the |
| // fixed-size reporting buffer. |
| std::string debug_info; |
| base::StringAppendF(&debug_info,"SQLITE_CORRUPT, db size %"PRId64"\n", |
| *db_size); |
| |
| // Only check files up to 8M to keep things from blocking too long. |
| constint64_t kMaxIntegrityCheckSize=8192*1024; |
| if(*db_size> kMaxIntegrityCheckSize){ |
| debug_info+="integrity_check skipped due to size\n"; |
| }else{ |
| std::vector<std::string> messages; |
| |
| // TODO(shess): FullIntegrityCheck() splits into a vector while this joins |
| // into a string. Probably should be refactored. |
| const base::TimeTicks before= base::TimeTicks::Now(); |
| FullIntegrityCheck(&messages); |
| base::StringAppendF( |
| &debug_info,"integrity_check %"PRId64" ms, %"PRIuS" records:\n", |
| (base::TimeTicks::Now()- before).InMilliseconds(), messages.size()); |
| |
| // SQLite returns up to 100 messages by default, trim deeper to |
| // keep close to the 2000-character size limit for dumping. |
| constsize_t kMaxMessages=20; |
| for(size_t i=0; i< kMaxMessages&& i< messages.size();++i){ |
| base::StringAppendF(&debug_info,"%s\n", messages[i].c_str()); |
| } |
| } |
| |
| return debug_info; |
| } |
| |
| boolDatabase::GetMmapAltStatus(int64_t* status){ |
| TRACE_EVENT0("sql","Database::GetMmapAltStatus"); |
| |
| // The [meta] version uses a missing table as a signal for a fresh database. |
| // That will not work for the view, which would not exist in either a new or |
| // an existing database. A new database _should_ be only one page long, so |
| // just don't bother optimizing this case (start at offset 0). |
| // TODO(shess): Could the [meta] case also get simpler, then? |
| if(!DoesViewExist("MmapStatus")){ |
| *status=0; |
| returntrue; |
| } |
| |
| staticconstexprchar kMmapStatusSql[]="SELECT * FROM MmapStatus"; |
| Statement s(GetUniqueStatement(kMmapStatusSql)); |
| if(s.Step()){ |
| *status= s.ColumnInt64(0); |
| } |
| return s.Succeeded(); |
| } |
| |
| boolDatabase::SetMmapAltStatus(int64_t status){ |
| Transaction transaction(this); |
| if(!transaction.Begin()){ |
| returnfalse; |
| } |
| |
| // View may not exist on first run. |
| if(!Execute("DROP VIEW IF EXISTS MmapStatus")){ |
| returnfalse; |
| } |
| |
| // Views live in the schema, so they cannot be parameterized. For an integer |
| // value, this construct should be safe from SQL injection, if the value |
| // becomes more complicated use "SELECT quote(?)" to generate a safe quoted |
| // value. |
| const std::string create_view_sql= base::StringPrintf( |
| "CREATE VIEW MmapStatus (value) AS SELECT %"PRId64, status); |
| if(!Execute(create_view_sql)){ |
| returnfalse; |
| } |
| |
| return transaction.Commit(); |
| } |
| |
| size_tDatabase::ComputeMmapSizeForOpen(){ |
| TRACE_EVENT0("sql","Database::ComputeMmapSizeForOpen"); |
| |
| // If the database has been razed, disable memory mapping. |
| if(!db_|| poisoned_){ |
| return0; |
| } |
| |
| // How much to map if no errors are found. 50MB encompasses the 99th |
| // percentile of Chrome databases in the wild, so this should be good. |
| constsize_t kMmapEverything=256*1024*1024; |
| if(base::FeatureList::IsEnabled(sql::features::kSqlFixedMmapSize)){ |
| return kMmapEverything; |
| } |
| |
| std::optional<base::ScopedBlockingCall> scoped_blocking_call; |
| InitScopedBlockingCall(FROM_HERE,&scoped_blocking_call); |
| |
| // Progress information is tracked in the [meta] table for databases which use |
| // sql::MetaTable, otherwise it is tracked in a special view. |
| // TODO(pwnall): Migrate all databases to using a meta table. |
| int64_t mmap_ofs=0; |
| if(options_.mmap_alt_status_discouraged_){ |
| if(!GetMmapAltStatus(&mmap_ofs)){ |
| return0; |
| } |
| }else{ |
| // If [meta] doesn't exist, yet, it's a new database, assume the best. |
| // sql::MetaTable::Init() will preload kMmapSuccess. |
| if(!MetaTable::DoesTableExist(this)){ |
| return kMmapEverything; |
| } |
| |
| if(!MetaTable::GetMmapStatus(this,&mmap_ofs)){ |
| return0; |
| } |
| } |
| |
| // Database read failed in the past, don't memory map. |
| if(mmap_ofs==MetaTable::kMmapFailure){ |
| return0; |
| } |
| |
| if(mmap_ofs!=MetaTable::kMmapSuccess){ |
| // Continue reading from previous offset. |
| DCHECK_GE(mmap_ofs,0); |
| |
| // GetSqliteVfsFile() returns null for in-memory and temporary databases. |
| // This is fine, we don't want to enable memory-mapping in those cases |
| // anyway. |
| // |
| // First, memory-mapping is a no-op for in-memory databases. |
| // |
| // Second, temporary databases are only used for corruption recovery, which |
| // occurs in response to I/O errors. An environment with heightened I/O |
| // errors translates into a higher risk of mmap-induced Chrome crashes. |
| sqlite3_int64 db_size=0; |
| sqlite3_file* file=GetSqliteVfsFile(); |
| if(!file|| file->pMethods->xFileSize(file,&db_size)!= SQLITE_OK){ |
| return0; |
| } |
| |
| // Read more of the database looking for errors. The VFS interface is used |
| // to assure that the reads are valid for SQLite. |g_reads_allowed| is used |
| // to limit checking to 20MB per run of Chromium. |
| // |
| // Read the data left, or |g_reads_allowed|, whichever is smaller. |
| // |g_reads_allowed| limits the total amount of I/O to spend verifying data |
| // in a single Chromium run. |
| sqlite3_int64 amount= db_size- mmap_ofs; |
| if(amount<0){ |
| amount=0; |
| } |
| if(amount>0){ |
| static base::NoDestructor<base::Lock> lock; |
| base::AutoLock auto_lock(*lock); |
| static sqlite3_int64 g_reads_allowed=20*1024*1024; |
| if(g_reads_allowed< amount){ |
| amount= g_reads_allowed; |
| } |
| g_reads_allowed-= amount; |
| } |
| |
| // |amount| can be <= 0 if |g_reads_allowed| ran out of quota, or if the |
| // database was truncated after a previous pass. |
| if(amount<=0&& mmap_ofs< db_size){ |
| DCHECK_EQ(0, amount); |
| }else{ |
| staticconstint kPageSize=4096; |
| char buf[kPageSize]; |
| while(amount>0){ |
| int rc= file->pMethods->xRead(file, buf,sizeof(buf), mmap_ofs); |
| if(rc== SQLITE_OK){ |
| mmap_ofs+=sizeof(buf); |
| amount-=sizeof(buf); |
| }elseif(rc== SQLITE_IOERR_SHORT_READ){ |
| // Reached EOF for a database with page size < |kPageSize|. |
| mmap_ofs= db_size; |
| break; |
| }else{ |
| // TODO(shess): Consider calling OnSqliteError(). |
| mmap_ofs=MetaTable::kMmapFailure; |
| break; |
| } |
| } |
| |
| // Log these events after update to distinguish meta update failure. |
| if(mmap_ofs>= db_size){ |
| mmap_ofs=MetaTable::kMmapSuccess; |
| }else{ |
| DCHECK(mmap_ofs>0|| mmap_ofs==MetaTable::kMmapFailure); |
| } |
| |
| if(options_.mmap_alt_status_discouraged_){ |
| if(!SetMmapAltStatus(mmap_ofs)){ |
| return0; |
| } |
| }else{ |
| if(!MetaTable::SetMmapStatus(this, mmap_ofs)){ |
| return0; |
| } |
| } |
| } |
| } |
| |
| if(mmap_ofs==MetaTable::kMmapFailure){ |
| return0; |
| } |
| if(mmap_ofs==MetaTable::kMmapSuccess){ |
| return kMmapEverything; |
| } |
| return mmap_ofs; |
| } |
| |
| sqlite3_file*Database::GetSqliteVfsFile(){ |
| CHECK(db_)<<"Database not opened"; |
| |
| // sqlite3_file_control() accepts a null pointer to mean the "main" database |
| // attached to a connection. https://www.sqlite.org/c3ref/file_control.html |
| constexprconstchar* kMainDatabaseName=nullptr; |
| |
| sqlite3_file* result=nullptr; |
| auto sqlite_result_code=ToSqliteResultCode(sqlite3_file_control( |
| db_, kMainDatabaseName, SQLITE_FCNTL_FILE_POINTER,&result)); |
| |
| // SQLITE_FCNTL_FILE_POINTER is handled directly by SQLite, not by the VFS. It |
| // is only supposed to fail with SQLITE_ERROR if the database name is not |
| // recognized. However, "main" should always be recognized. |
| DCHECK_EQ(sqlite_result_code,SqliteResultCode::kOk) |
| <<"sqlite3_file_control(SQLITE_FCNTL_FILE_POINTER) failed"; |
| |
| // SQLite does not return null when called on an in-memory or temporary |
| // database. Instead, it returns returns a VFS file object with a null |
| // pMethods member. |
| DCHECK(result) |
| <<"sqlite3_file_control() succeded but returned a null sqlite3_file*"; |
| if(!result->pMethods){ |
| // If this assumption fails, sql::Database will still function correctly, |
| // but will miss some configuration optimizations. The DCHECK is here to |
| // alert us (via test failures and ASAN canary builds) of such cases. |
| DCHECK_EQ(DbPath().AsUTF8Unsafe(),"") |
| <<"sqlite3_file_control() returned a sqlite3_file* with null pMethods " |
| <<"in a case when it shouldn't have."; |
| |
| returnnullptr; |
| } |
| |
| return result; |
| } |
| |
| voidDatabase::RecordTimingHistogram(std::string_view name_prefix, |
| base::TimeDelta timing)const{ |
| base::UmaHistogramCustomMicrosecondsTimes( |
| base::StrCat({name_prefix, histogram_tag()}), timing, |
| base::Microseconds(0), base::Minutes(1),100); |
| } |
| |
| perfetto::NamedTrackDatabase::GetTracingNamedTrack()const{ |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| return perfetto::NamedTrack(perfetto::DynamicString(tracing_track_name_), |
| reinterpret_cast<uint64_t>(this), |
| perfetto::ThreadTrack::Current()); |
| } |
| |
| voidDatabase::TrimMemory(){ |
| TRACE_EVENT0("sql","Database::TrimMemory"); |
| |
| if(!db_){ |
| return; |
| } |
| |
| // Passing the result code through ToSqliteResultCode() to catch issues such |
| // as SQLITE_MISUSE. |
| std::ignore=ToSqliteResultCode(sqlite3_db_release_memory(db_)); |
| |
| // It is tempting to use sqlite3_release_memory() here as well. However, the |
| // API is documented to be a no-op unless SQLite is built with |
| // SQLITE_ENABLE_MEMORY_MANAGEMENT. We do not use this option, because it is |
| // incompatible with per-database page cache pools. Behind the scenes, |
| // SQLITE_ENABLE_MEMORY_MANAGEMENT causes SQLite to use a global page cache |
| // pool, and sqlite3_release_memory() releases unused pages from this global |
| // pool. |
| #if defined(SQLITE_ENABLE_MEMORY_MANAGEMENT) |
| #error"This method assumes SQLITE_ENABLE_MEMORY_MANAGEMENT is not defined" |
| #endif// defined(SQLITE_ENABLE_MEMORY_MANAGEMENT) |
| } |
| |
| // Create an in-memory database with the existing database's page |
| // size, then backup that database over the existing database. |
| boolDatabase::RazeInternal(){ |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| std::optional<base::ScopedBlockingCall> scoped_blocking_call; |
| InitScopedBlockingCall(FROM_HERE,&scoped_blocking_call); |
| |
| if(!db_){ |
| DCHECK(poisoned_)<<"Cannot raze null db"; |
| RecordRazeDatabaseFailureReason(histogram_tag_, |
| RazeDatabaseFailedReason::kPoisoned); |
| returnfalse; |
| } |
| |
| DCHECK_GE(transaction_nesting_,0); |
| if(transaction_nesting_>0){ |
| DLOG(FATAL)<<"Cannot raze within a transaction"; |
| RecordRazeDatabaseFailureReason( |
| histogram_tag_,RazeDatabaseFailedReason::kPendingTransaction); |
| returnfalse; |
| } |
| |
| Database null_db( |
| DatabaseOptions() |
| .set_exclusive_locking(true) |
| .set_page_size(options_.page_size_) |
| .set_enable_views_discouraged(options_.enable_views_discouraged_), |
| "RazeNullDB"); |
| if(!null_db.OpenInMemory()){ |
| DLOG(FATAL)<<"Unable to open in-memory database."; |
| RecordRazeDatabaseFailureReason( |
| histogram_tag_,RazeDatabaseFailedReason::kCantOpenInMemory); |
| returnfalse; |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // Android compiles with SQLITE_DEFAULT_AUTOVACUUM. Unfortunately, |
| // in-memory databases do not respect this define. |
| // TODO(shess): Figure out a way to set this without using platform |
| // specific code. AFAICT from sqlite3.c, the only way to do it |
| // would be to create an actual filesystem database, which is |
| // unfortunate. |
| if(!null_db.Execute("PRAGMA auto_vacuum = 1")){ |
| RecordRazeDatabaseFailureReason( |
| histogram_tag_,RazeDatabaseFailedReason::kAutoVacuumFailed); |
| returnfalse; |
| } |
| #endif |
| |
| // The page size doesn't take effect until a database has pages, and |
| // at this point the null database has none. Changing the schema |
| // version will create the first page. This will not affect the |
| // schema version in the resulting database, as SQLite's backup |
| // implementation propagates the schema version from the original |
| // database to the new version of the database, incremented by one |
| // so that other readers see the schema change and act accordingly. |
| if(!null_db.Execute("PRAGMA schema_version = 1")){ |
| RecordRazeDatabaseFailureReason(histogram_tag_, |
| RazeDatabaseFailedReason::kSchemaFailed); |
| returnfalse; |
| } |
| |
| // SQLite tracks the expected number of database pages in the first |
| // page, and if it does not match the total retrieved from a |
| // filesystem call, treats the database as corrupt. This situation |
| // breaks almost all SQLite calls. "PRAGMA writable_schema" can be |
| // used to hint to SQLite to soldier on in that case, specifically |
| // for purposes of recovery. [See SQLITE_CORRUPT_BKPT case in |
| // sqlite3.c lockBtree().] |
| // TODO(shess): With this, "PRAGMA auto_vacuum" and "PRAGMA |
| // page_size" can be used to query such a database. |
| ScopedWritableSchema writable_schema(weak_factory_.GetWeakPtr()); |
| |
| #if BUILDFLAG(IS_WIN) |
| // On Windows, truncate silently fails when applied to memory-mapped files. |
| // Disable memory-mapping so that the truncate succeeds. Note that other |
| // Database connections may have memory-mapped the file, so this may not |
| // entirely prevent the problem. |
| // [Source: <https://sqlite.org/mmap.html> plus experiments.] |
| std::ignore=Execute("PRAGMA mmap_size = 0"); |
| #endif |
| |
| SqliteResultCode sqlite_result_code=BackupDatabaseForRaze(null_db.db_, db_); |
| |
| // The destination database was locked. |
| if(sqlite_result_code==SqliteResultCode::kBusy){ |
| RecordRazeDatabaseFailureReason(histogram_tag_, |
| RazeDatabaseFailedReason::kLocked); |
| returnfalse; |
| } |
| |
| // SQLITE_NOTADB can happen if page 1 of db_ exists, but is not |
| // formatted correctly. SQLITE_IOERR_SHORT_READ can happen if db_ |
| // isn't even big enough for one page. Either way, reach in and |
| // truncate it before trying again. |
| // TODO(shess): Maybe it would be worthwhile to just truncate from |
| // the get-go? |
| if(sqlite_result_code==SqliteResultCode::kNotADatabase|| |
| sqlite_result_code==SqliteResultCode::kIoShortRead){ |
| sqlite3_file* file=GetSqliteVfsFile(); |
| if(!file|| file->pMethods->xTruncate(file,0)!= SQLITE_OK){ |
| DLOG(FATAL)<<"Failed to truncate file."; |
| RecordRazeDatabaseFailureReason( |
| histogram_tag_,RazeDatabaseFailedReason::kTruncateFailed); |
| returnfalse; |
| } |
| |
| sqlite_result_code=BackupDatabaseForRaze(null_db.db_, db_); |
| if(sqlite_result_code!=SqliteResultCode::kDone){ |
| RecordRazeDatabaseFailureReason(histogram_tag_, |
| RazeDatabaseFailedReason::kBackupFailed); |
| returnfalse; |
| } |
| } |
| |
| // Page size of |db_| and |null_db| differ. |
| if(sqlite_result_code==SqliteResultCode::kReadOnly){ |
| // Enter TRUNCATE mode to change page size. |
| // TODO(shuagga@microsoft.com): Need a guarantee here that there is no other |
| // database connection open. |
| std::ignore=Execute("PRAGMA journal_mode=TRUNCATE;"); |
| const std::string page_size_sql= base::StrCat( |
| {"PRAGMA page_size=", base::NumberToString(options_.page_size_)}); |
| if(!Execute(page_size_sql)){ |
| RecordRazeDatabaseFailureReason( |
| histogram_tag_,RazeDatabaseFailedReason::kPageSizeFailed); |
| returnfalse; |
| } |
| // Page size isn't changed until the database is vacuumed. |
| std::ignore=Execute("VACUUM"); |
| // Re-enter WAL mode. |
| if(UseWALMode()){ |
| std::ignore=Execute("PRAGMA journal_mode=WAL;"); |
| } |
| |
| sqlite_result_code=BackupDatabaseForRaze(null_db.db_, db_); |
| if(sqlite_result_code!=SqliteResultCode::kDone){ |
| RecordRazeDatabaseFailureReason(histogram_tag_, |
| RazeDatabaseFailedReason::kBackupFailed); |
| |
| returnfalse; |
| } |
| } |
| |
| if(sqlite_result_code!=SqliteResultCode::kDone){ |
| NOTIMPLEMENTED()<<"Unhandled sqlite3_backup_step() error: " |
| << sqlite_result_code; |
| RecordRazeDatabaseFailureReason(histogram_tag_, |
| RazeDatabaseFailedReason::kUnknownError); |
| returnfalse; |
| } |
| |
| // Checkpoint to propagate transactions to the database file and empty the WAL |
| // file. |
| // The database can still contain old data if the Checkpoint fails so fail the |
| // Raze. |
| if(!CheckpointDatabase()){ |
| RecordRazeDatabaseFailureReason( |
| histogram_tag_,RazeDatabaseFailedReason::kCheckpointFailed); |
| returnfalse; |
| } |
| |
| returntrue; |
| } |
| |
| boolDatabase::Raze(){ |
| TRACE_EVENT0("sql","Database::Raze"); |
| |
| base::ElapsedTimer raze_timer; |
| bool result=RazeInternal(); |
| RecordTimingHistogram("Sql.Database.RazeTime.", raze_timer.Elapsed()); |
| |
| return result; |
| } |
| |
| boolDatabase::RazeAndPoison(){ |
| TRACE_EVENT0("sql","Database::RazeAndPoison"); |
| |
| if(!db_){ |
| DCHECK(poisoned_)<<"Cannot raze null db"; |
| returnfalse; |
| } |
| |
| // Raze() cannot run in a transaction. |
| RollbackAllTransactions(); |
| |
| bool result=Raze(); |
| |
| CloseInternal(true); |
| |
| // Mark the database so that future API calls fail appropriately, |
| // but don't DCHECK (because after calling this function they are |
| // expected to fail). |
| poisoned_=true; |
| |
| return result; |
| } |
| |
| voidDatabase::Poison(){ |
| TRACE_EVENT0("sql","Database::Poison"); |
| |
| if(!db_){ |
| DCHECK(poisoned_)<<"Cannot poison null db"; |
| return; |
| } |
| |
| CloseInternal(true); |
| |
| // Mark the database so that future API calls fail appropriately, |
| // but don't DCHECK (because after calling this function they are |
| // expected to fail). |
| poisoned_=true; |
| } |
| |
| // TODO(shess): To the extent possible, figure out the optimal |
| // ordering for these deletes which will prevent other Database connections |
| // from seeing odd behavior. For instance, it may be necessary to |
| // manually lock the main database file in a SQLite-compatible fashion |
| // (to prevent other processes from opening it), then delete the |
| // journal files, then delete the main database file. Another option |
| // might be to lock the main database file and poison the header with |
| // junk to prevent other processes from opening it successfully (like |
| // Gears "SQLite poison 3" trick). |
| // |
| // static |
| boolDatabase::Delete(const base::FilePath& path){ |
| TRACE_EVENT1("sql","Database::Delete","path", path.MaybeAsASCII()); |
| |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| |
| base::FilePath journal_path=Database::JournalPath(path); |
| base::FilePath wal_path=Database::WriteAheadLogPath(path); |
| |
| std::string journal_str=AsUTF8ForSQL(journal_path); |
| std::string wal_str=AsUTF8ForSQL(wal_path); |
| std::string path_str=AsUTF8ForSQL(path); |
| |
| EnsureSqliteInitialized(); |
| |
| sqlite3_vfs* vfs= sqlite3_vfs_find(nullptr); |
| CHECK(vfs); |
| CHECK(vfs->xDelete); |
| CHECK(vfs->xAccess); |
| |
| vfs->xDelete(vfs, journal_str.c_str(),0); |
| vfs->xDelete(vfs, wal_str.c_str(),0); |
| vfs->xDelete(vfs, path_str.c_str(),0); |
| |
| int journal_exists=0; |
| vfs->xAccess(vfs, journal_str.c_str(), SQLITE_ACCESS_EXISTS,&journal_exists); |
| |
| int wal_exists=0; |
| vfs->xAccess(vfs, wal_str.c_str(), SQLITE_ACCESS_EXISTS,&wal_exists); |
| |
| int path_exists=0; |
| vfs->xAccess(vfs, path_str.c_str(), SQLITE_ACCESS_EXISTS,&path_exists); |
| |
| return!journal_exists&&!wal_exists&&!path_exists; |
| } |
| |
| boolDatabase::BeginTransaction(InternalApiToken){ |
| TRACE_EVENT0("sql","Database::BeginTransaction"); |
| |
| if(needs_rollback_){ |
| DCHECK_GT(transaction_nesting_,0); |
| |
| // When we're going to rollback, fail on this begin and don't actually |
| // mark us as entering the nested transaction. |
| returnfalse; |
| } |
| |
| bool success=true; |
| DCHECK_GE(transaction_nesting_,0); |
| if(!transaction_nesting_){ |
| needs_rollback_=false; |
| |
| Statement begin(GetCachedStatement(SQL_FROM_HERE,"BEGIN TRANSACTION")); |
| if(!begin.Run()){ |
| returnfalse; |
| } |
| } |
| ++transaction_nesting_; |
| return success; |
| } |
| |
| voidDatabase::RollbackTransaction(InternalApiToken){ |
| TRACE_EVENT0("sql","Database::RollbackTransaction"); |
| |
| DCHECK_GE(transaction_nesting_,0); |
| if(!transaction_nesting_){ |
| DCHECK(poisoned_)<<"Rolling back a nonexistent transaction"; |
| return; |
| } |
| |
| DCHECK_GT(transaction_nesting_,0); |
| --transaction_nesting_; |
| |
| if(transaction_nesting_>0){ |
| // Mark the outermost transaction as needing rollback. |
| needs_rollback_=true; |
| return; |
| } |
| |
| DoRollback(); |
| } |
| |
| boolDatabase::CommitTransaction(InternalApiToken){ |
| TRACE_EVENT0("sql","Database::CommitTransaction"); |
| |
| DCHECK_GE(transaction_nesting_,0); |
| if(!transaction_nesting_){ |
| DCHECK(poisoned_)<<"Committing a nonexistent transaction"; |
| returnfalse; |
| } |
| |
| DCHECK_GT(transaction_nesting_,0); |
| --transaction_nesting_; |
| |
| if(transaction_nesting_>0){ |
| // Mark any nested transactions as failing after we've already got one. |
| return!needs_rollback_; |
| } |
| |
| if(needs_rollback_){ |
| DoRollback(); |
| returnfalse; |
| } |
| |
| Statement commit(GetCachedStatement(SQL_FROM_HERE,"COMMIT")); |
| |
| bool succeeded= commit.Run(); |
| |
| // Release dirty cache pages after the transaction closes. |
| ReleaseCacheMemoryIfNeeded(false); |
| |
| return succeeded; |
| } |
| |
| boolDatabase::BeginTransactionDeprecated(){ |
| returnBeginTransaction(InternalApiToken()); |
| } |
| |
| boolDatabase::CommitTransactionDeprecated(){ |
| returnCommitTransaction(InternalApiToken()); |
| } |
| |
| voidDatabase::RollbackTransactionDeprecated(){ |
| RollbackTransaction(InternalApiToken()); |
| } |
| |
| voidDatabase::RollbackAllTransactions(){ |
| TRACE_EVENT0("sql","Database::RollbackAllTransactions"); |
| |
| DCHECK_GE(transaction_nesting_,0); |
| if(transaction_nesting_>0){ |
| transaction_nesting_=0; |
| DoRollback(); |
| } |
| } |
| |
| boolDatabase::AttachDatabase(const base::FilePath& other_db_path, |
| std::string_view attachment_point){ |
| TRACE_EVENT0("sql","Database::AttachDatabase"); |
| |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(ValidAttachmentPoint(attachment_point)); |
| |
| Statement statement(GetUniqueStatement("ATTACH ? AS ?")); |
| #if BUILDFLAG(IS_WIN) |
| statement.BindString16(0, base::AsStringPiece16(other_db_path.value())); |
| #else |
| statement.BindString(0, other_db_path.value()); |
| #endif |
| statement.BindString(1, attachment_point); |
| return statement.Run(); |
| } |
| |
| boolDatabase::DetachDatabase(std::string_view attachment_point){ |
| TRACE_EVENT0("sql","Database::DetachDatabase"); |
| |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| DCHECK(ValidAttachmentPoint(attachment_point)); |
| |
| Statement statement(GetUniqueStatement("DETACH ?")); |
| statement.BindString(0, attachment_point); |
| return statement.Run(); |
| } |
| |
| // TODO(crbug.com/40779018): Change this to execute exactly one statement. |
| SqliteResultCodeDatabase::ExecuteAndReturnResultCode( |
| base::cstring_view initial_sql){ |
| TRACE_EVENT0("sql","Database::ExecuteAndReturnErrorCode"); |
| |
| if(!db_){ |
| DCHECK(poisoned_)<<"Illegal use of Database without a db"; |
| returnSqliteResultCode::kError; |
| } |
| |
| std::optional<base::ScopedBlockingCall> scoped_blocking_call; |
| InitScopedBlockingCall(FROM_HERE,&scoped_blocking_call); |
| constchar* sql= initial_sql.c_str(); |
| |
| SqliteResultCode sqlite_result_code=SqliteResultCode::kOk; |
| while((sqlite_result_code==SqliteResultCode::kOk)&&*sql){ |
| sqlite3_stmt* sqlite_statement; |
| constchar* leftover_sql; |
| sqlite_result_code=ToSqliteResultCode( |
| sqlite3_prepare_v3(db_, sql,/* nByte= */-1, kPrepareFlags, |
| &sqlite_statement,&leftover_sql)); |
| |
| #if DCHECK_IS_ON() |
| // Report SQL compilation errors. On developer machines, the errors are most |
| // likely caused by invalid SQL in an under-development feature. In |
| // production, SQL compilation errors are caused by database schema |
| // corruption. |
| // |
| // DCHECK would not be appropriate here, because on-disk data is always |
| // subject to corruption, so Chrome cannot assume that the database schema |
| // will remain intact. |
| if(sqlite_result_code==SqliteResultCode::kError){ |
| DLOG(ERROR)<<"SQL compilation error: "<<GetErrorMessage() |
| <<". Statement: "<< sql; |
| } |
| #endif// DCHECK_IS_ON() |
| |
| // Stop if compiling the SQL statement fails. |
| if(sqlite_result_code!=SqliteResultCode::kOk){ |
| DCHECK_NE(sqlite_result_code,SqliteResultCode::kDone) |
| <<"sqlite3_prepare_v3() returned unexpected non-error result code"; |
| DCHECK_NE(sqlite_result_code,SqliteResultCode::kRow) |
| <<"sqlite3_prepare_v3() returned unexpected non-error result code"; |
| break; |
| } |
| |
| sql= leftover_sql; |
| |
| // This happens if |sql| originally only contained comments or whitespace. |
| // TODO(shess): Audit to see if this can become a DCHECK(). Having |
| // extraneous comments and whitespace in the SQL statements increases |
| // runtime cost and can easily be shifted out to the C++ layer. |
| if(!sqlite_statement){ |
| continue; |
| } |
| |
| while(true){ |
| sqlite_result_code=ToSqliteResultCode(sqlite3_step(sqlite_statement)); |
| if(sqlite_result_code!=SqliteResultCode::kRow){ |
| break; |
| } |
| |
| // TODO(shess): Audit to see if this can become a DCHECK. I think PRAGMA |
| // is the only legitimate case for this. Previously recorded histograms |
| // show significant use of this code path. |
| } |
| |
| // sqlite3_finalize() returns SQLITE_OK if the most recent sqlite3_step() |
| // returned SQLITE_DONE or SQLITE_ROW, otherwise the error code. |
| sqlite_result_code=ToSqliteResultCode(sqlite3_finalize(sqlite_statement)); |
| DCHECK_NE(sqlite_result_code,SqliteResultCode::kDone) |
| <<"sqlite3_finalize() returned unexpected non-error result code"; |
| DCHECK_NE(sqlite_result_code,SqliteResultCode::kRow) |
| <<"sqlite3_finalize() returned unexpected non-error result code"; |
| |
| // sqlite3_exec() does this, presumably to avoid spinning the parser for |
| // trailing whitespace. |
| // TODO(shess): Audit to see if this can become a DCHECK. |
| while(base::IsAsciiWhitespace(*sql)){ |
| sql++; |
| } |
| } |
| |
| // Most calls to Execute() modify the database. The main exceptions would be |
| // calls such as CREATE TABLE IF NOT EXISTS which could modify the database |
| // but sometimes don't. |
| ReleaseCacheMemoryIfNeeded(true); |
| |
| DCHECK_NE(sqlite_result_code,SqliteResultCode::kDone) |
| << __func__<<" about to return unexpected non-error result code"; |
| DCHECK_NE(sqlite_result_code,SqliteResultCode::kRow) |
| << __func__<<" about to return unexpected non-error result code"; |
| return sqlite_result_code; |
| } |
| |
| boolDatabase::Execute(base::cstring_view sql){ |
| TRACE_EVENT0("sql","Database::Execute"); |
| |
| returnExecuteWithTimeout(sql, base::TimeDelta()); |
| } |
| |
| boolDatabase::ExecuteWithTimeout(base::cstring_view sql, |
| base::TimeDelta timeout){ |
| TRACE_EVENT1("sql","Database::ExecuteWithTimeout","query", sql); |
| |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if(!db_){ |
| DCHECK(poisoned_)<<"Illegal use of Database without a db"; |
| returnfalse; |
| } |
| |
| // Passing zero or a negative value to sqlite3_busy_timeout() would clear any |
| // busy handlers defined prior to this point. |
| if(timeout.is_positive()){ |
| DCHECK_LT(timeout.InMilliseconds(), INT_MAX); |
| sqlite3_busy_timeout(db_,static_cast<int>(timeout.InMilliseconds())); |
| } |
| SqliteResultCode sqlite_result_code=ExecuteAndReturnResultCode(sql); |
| sqlite3_busy_timeout(db_,0); |
| if(sqlite_result_code!=SqliteResultCode::kOk){ |
| MaybeReportErrorDuringOpen(sqlite_result_code); |
| OnSqliteError(ToSqliteErrorCode(sqlite_result_code),nullptr, sql.c_str()); |
| // At this point, `this` may have been modified or even deleted as a result |
| // of the caller-provided error callback. |
| } |
| return sqlite_result_code==SqliteResultCode::kOk; |
| } |
| |
| boolDatabase::ExecuteScriptForTesting(base::cstring_view sql_script){ |
| if(!db_){ |
| DCHECK(poisoned_)<<"Illegal use of Database without a db"; |
| returnfalse; |
| } |
| |
| std::optional<base::ScopedBlockingCall> scoped_blocking_call; |
| InitScopedBlockingCall(FROM_HERE,&scoped_blocking_call); |
| |
| constchar* sql= sql_script.c_str(); |
| while(*sql){ |
| sqlite3_stmt* sqlite_statement; |
| auto sqlite_result_code=ToSqliteResultCode(sqlite3_prepare_v3( |
| db_, sql,/*nByte=*/-1, kPrepareFlags,&sqlite_statement,&sql)); |
| if(sqlite_result_code!=SqliteResultCode::kOk){ |
| returnfalse; |
| } |
| |
| if(!sqlite_statement){ |
| // Trailing comment or whitespace after the last semicolon. |
| returntrue; |
| } |
| |
| // TODO(pwnall): Investigate restricting ExecuteScriptForTesting() to |
| // statements that don't produce any result rows. |
| do{ |
| sqlite_result_code=ToSqliteResultCode(sqlite3_step(sqlite_statement)); |
| }while(sqlite_result_code==SqliteResultCode::kRow); |
| |
| // sqlite3_finalize() returns SQLITE_OK if the most recent sqlite3_step() |
| // returned SQLITE_DONE or SQLITE_ROW, otherwise the error code. |
| sqlite_result_code=ToSqliteResultCode(sqlite3_finalize(sqlite_statement)); |
| if(sqlite_result_code!=SqliteResultCode::kOk){ |
| returnfalse; |
| } |
| } |
| |
| returntrue; |
| } |
| |
| scoped_refptr<Database::StatementRef>Database::GetCachedStatement( |
| StatementID id, |
| base::cstring_view sql){ |
| auto it= statement_cache_.find(id); |
| if(it!= statement_cache_.end()){ |
| StatementRef& statement=*it->second; |
| // Statement is in the cache. It should still be valid. We're the only |
| // entity invalidating cached statements, and we remove them from the cache |
| // when we do that. |
| DCHECK(statement.is_valid()); |
| DCHECK_EQ(base::cstring_view(sqlite3_sql(statement.stmt())), sql) |
| <<"GetCachedStatement used with same ID but different SQL"; |
| |
| // Reset the statement so it can be reused. |
| statement.Reset(/*clear_bound_variables=*/true); |
| return it->second; |
| } |
| |
| scoped_refptr<StatementRef> statement=GetUniqueStatement(sql); |
| if(statement->is_valid()){ |
| statement_cache_[id]= statement;// Only cache valid statements. |
| DCHECK_EQ(std::string(sqlite3_sql(statement->stmt())), std::string(sql)) |
| <<"Input SQL does not match SQLite's normalized version"; |
| } |
| return statement; |
| } |
| |
| scoped_refptr<Database::StatementRef>Database::GetUniqueStatement( |
| base::cstring_view sql){ |
| returnGetStatementImpl(sql,/*is_readonly=*/false); |
| } |
| |
| scoped_refptr<Database::StatementRef>Database::GetReadonlyStatement( |
| base::cstring_view sql){ |
| returnGetStatementImpl(sql,/*is_readonly=*/true); |
| } |
| |
| scoped_refptr<Database::StatementRef>Database::GetStatementImpl( |
| base::cstring_view sql, |
| bool is_readonly){ |
| // Return inactive statement. |
| if(!db_){ |
| return base::MakeRefCounted<StatementRef>(nullptr,nullptr, poisoned_); |
| } |
| |
| std::optional<base::ScopedBlockingCall> scoped_blocking_call; |
| InitScopedBlockingCall(FROM_HERE,&scoped_blocking_call); |
| |
| #if DCHECK_IS_ON() |
| constchar* unused_sql=nullptr; |
| constchar** unused_sql_ptr=&unused_sql; |
| #else |
| constexprconstchar** unused_sql_ptr=nullptr; |
| #endif// DCHECK_IS_ON() |
| // TODO(pwnall): Cached statements (but not unique statements) should be |
| // prepared with prepFlags set to SQLITE_PREPARE_PERSISTENT. |
| sqlite3_stmt* sqlite_statement; |
| auto sqlite_result_code=ToSqliteResultCode(sqlite3_prepare_v3( |
| db_, sql.c_str(),/* nByte= */-1, kPrepareFlags, |
| &sqlite_statement, unused_sql_ptr)); |
| |
| #if DCHECK_IS_ON() |
| // Report SQL compilation errors. On developer machines, the errors are most |
| // likely caused by invalid SQL in an under-development feature. In |
| // production, SQL compilation errors are caused by database schema |
| // corruption. |
| // |
| // DCHECK would not be appropriate here, because on-disk data is always |
| // subject to corruption, so Chrome cannot assume that the database schema |
| // will remain intact. |
| if(sqlite_result_code==SqliteResultCode::kError){ |
| DLOG(ERROR)<<"SQL compilation error: "<<GetErrorMessage() |
| <<". Statement: "<< sql; |
| } |
| #endif// DCHECK_IS_ON() |
| |
| if(sqlite_result_code!=SqliteResultCode::kOk){ |
| DCHECK_NE(sqlite_result_code,SqliteResultCode::kDone) |
| <<"sqlite3_prepare_v3() returned unexpected non-error result code"; |
| DCHECK_NE(sqlite_result_code,SqliteResultCode::kRow) |
| <<"sqlite3_prepare_v3() returned unexpected non-error result code"; |
| OnSqliteError(ToSqliteErrorCode(sqlite_result_code),nullptr, sql.c_str()); |
| return base::MakeRefCounted<StatementRef>(nullptr,nullptr,false); |
| } |
| |
| // If readonly statement is expected and the statement is not readonly, return |
| // an invalid statement and close the created statement. |
| if(is_readonly&& sqlite3_stmt_readonly(sqlite_statement)==0){ |
| DLOG(ERROR)<<"Readonly SQL statement failed readonly test "<< sql; |
| // Make a `StatementRef` that will close the created statement. |
| base::MakeRefCounted<StatementRef>(this, sqlite_statement,true); |
| |
| return base::MakeRefCounted<StatementRef>(nullptr,nullptr,false); |
| } |
| |
| #if DCHECK_IS_ON() |
| DCHECK_EQ(unused_sql, sql.c_str()+ sql.size()) |
| <<"Unused text: "<< std::string(unused_sql)<<"\n" |
| <<"in prepared SQL statement: "<< std::string(sql); |
| #endif// DCHECK_IS_ON() |
| |
| DCHECK(sqlite_statement)<<"No SQL statement in string: "<< sql; |
| |
| return base::MakeRefCounted<StatementRef>(this, sqlite_statement,true); |
| } |
| |
| std::optional<StreamingBlobHandle>Database::GetStreamingBlob( |
| base::cstring_view table, |
| base::cstring_view column, |
| int64_t row_id, |
| bool readonly){ |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if(!db_){ |
| DCHECK(poisoned_)<<"Illegal use of Database without a db"; |
| return std::nullopt; |
| } |
| |
| sqlite3_blob* blob_handle=nullptr; |
| auto sqlite_result_code= |
| sqlite3_blob_open(db_, kSqliteMainDatabaseName, table.c_str(), |
| column.c_str(), row_id, readonly?0:1,&blob_handle); |
| if(sqlite_result_code!= SQLITE_OK){ |
| OnSqliteError(ToSqliteErrorCode(ToSqliteResultCode((sqlite_result_code))), |
| nullptr,"-- sqlite3_blob_open()"); |
| |
| return std::nullopt; |
| } |
| |
| CHECK(blob_handle); |
| ++outstanding_blob_count_; |
| returnStreamingBlobHandle(base::PassKey<Database>(), blob_handle, |
| base::BindOnce(&Database::OnStreamingBlobClosed, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| voidDatabase::OnStreamingBlobClosed(SqliteResultCode result, |
| constchar* error_source){ |
| --outstanding_blob_count_; |
| if(handling_error_nesting_==0&&!IsSqliteSuccessCode(result)){ |
| OnSqliteError(ToSqliteErrorCode(result),nullptr, error_source); |
| } |
| } |
| |
| std::stringDatabase::GetSchema(){ |
| // The ORDER BY should not be necessary, but relying on organic |
| // order for something like this is questionable. |
| staticconstexprchar kSql[]= |
| "SELECT type, name, tbl_name, sql " |
| "FROM sqlite_schema ORDER BY 1, 2, 3, 4"; |
| Statement statement(GetUniqueStatement(kSql)); |
| |
| std::string schema; |
| while(statement.Step()){ |
| schema+= statement.ColumnStringView(0); |
| schema+='|'; |
| schema+= statement.ColumnStringView(1); |
| schema+='|'; |
| schema+= statement.ColumnStringView(2); |
| schema+='|'; |
| schema+= statement.ColumnStringView(3); |
| schema+='\n'; |
| } |
| |
| return schema; |
| } |
| |
| boolDatabase::IsSQLValid(base::cstring_view sql){ |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| std::optional<base::ScopedBlockingCall> scoped_blocking_call; |
| InitScopedBlockingCall(FROM_HERE,&scoped_blocking_call); |
| if(!db_){ |
| DCHECK(poisoned_)<<"Illegal use of Database without a db"; |
| returnfalse; |
| } |
| |
| #if DCHECK_IS_ON() |
| constchar* unused_sql=nullptr; |
| constchar** unused_sql_ptr=&unused_sql; |
| #else |
| constexprconstchar** unused_sql_ptr=nullptr; |
| #endif// DCHECK_IS_ON() |
| |
| sqlite3_stmt* sqlite_statement=nullptr; |
| auto sqlite_result_code=ToSqliteResultCode(sqlite3_prepare_v3( |
| db_, sql.c_str(),/* nByte= */-1, kPrepareFlags, |
| &sqlite_statement, unused_sql_ptr)); |
| if(sqlite_result_code!=SqliteResultCode::kOk){ |
| returnfalse; |
| } |
| |
| #if DCHECK_IS_ON() |
| DCHECK_EQ(unused_sql, sql.c_str()+ sql.size()) |
| <<"Unused text: "<< std::string(unused_sql)<<"\n" |
| <<"in SQL statement: "<< std::string(sql); |
| #endif// DCHECK_IS_ON() |
| |
| DCHECK(sqlite_statement)<<"No SQL statement in string: "<< sql; |
| |
| sqlite_result_code=ToSqliteResultCode(sqlite3_finalize(sqlite_statement)); |
| DCHECK_EQ(sqlite_result_code,SqliteResultCode::kOk) |
| <<"sqlite3_finalize() failed for valid statement"; |
| returntrue; |
| } |
| |
| boolDatabase::DoesIndexExist(std::string_view index_name){ |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| returnDoesSchemaItemExist(index_name,"index"); |
| } |
| |
| boolDatabase::DoesTableExist(std::string_view table_name){ |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| returnDoesSchemaItemExist(table_name,"table"); |
| } |
| |
| boolDatabase::DoesViewExist(std::string_view view_name){ |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| returnDoesSchemaItemExist(view_name,"view"); |
| } |
| |
| boolDatabase::DoesSchemaItemExist(std::string_view name, |
| std::string_view type){ |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| staticconstexprchar kSql[]= |
| "SELECT 1 FROM sqlite_schema WHERE type=? AND name=?"; |
| Statement statement(GetUniqueStatement(kSql)); |
| |
| if(!statement.is_valid()){ |
| // The database is corrupt. |
| returnfalse; |
| } |
| |
| statement.BindString(0, type); |
| statement.BindString(1, name); |
| |
| return statement.Step();// Table exists if any row was returned. |
| } |
| |
| boolDatabase::DoesColumnExist(base::cstring_view table_name, |
| base::cstring_view column_name){ |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if(!db_){ |
| DCHECK(poisoned_)<<"Illegal use of Database without a db"; |
| returnfalse; |
| } |
| |
| // sqlite3_table_column_metadata uses out-params to return column definition |
| // details, such as the column type and whether it allows NULL values. These |
| // aren't needed to compute the current method's result, so we pass in nullptr |
| // for all the out-params. |
| auto sqlite_result_code=ToSqliteResultCode(sqlite3_table_column_metadata( |
| db_,"main", table_name.c_str(), column_name.c_str(), |
| /* pzDataType= */nullptr, |
| /* pzCollSeq= */nullptr,/* pNotNull= */nullptr, |
| /* pPrimaryKey= */nullptr,/* pAutoinc= */nullptr)); |
| return sqlite_result_code==SqliteResultCode::kOk; |
| } |
| |
| int64_tDatabase::GetLastInsertRowId()const{ |
| if(!db_){ |
| DCHECK(poisoned_)<<"Illegal use of Database without a db"; |
| return0; |
| } |
| int64_t last_rowid= sqlite3_last_insert_rowid(db_); |
| DCHECK(last_rowid!=0)<<"No successful INSERT in a table with ROWID"; |
| return last_rowid; |
| } |
| |
| int64_tDatabase::GetLastChangeCount(){ |
| if(!db_){ |
| DCHECK(poisoned_)<<"Illegal use of Database without a db"; |
| return0; |
| } |
| return sqlite3_changes64(db_); |
| } |
| |
| intDatabase::GetMemoryUsage(){ |
| if(!db_){ |
| DCHECK(poisoned_)<<"Illegal use of Database without a db"; |
| return0; |
| } |
| |
| // The following calls all set the high watermark to zero. |
| // See https://www.sqlite.org/c3ref/c_dbstatus_options.html |
| int high_watermark=0; |
| |
| int cache_memory=0, schema_memory=0, statement_memory=0; |
| |
| auto sqlite_result_code=ToSqliteResultCode(sqlite3_db_status( |
| db_, SQLITE_DBSTATUS_CACHE_USED,&cache_memory,&high_watermark, |
| /*resetFlg=*/0)); |
| DCHECK_EQ(sqlite_result_code,SqliteResultCode::kOk) |
| <<"sqlite3_db_status(SQLITE_DBSTATUS_CACHE_USED) failed"; |
| |
| #if DCHECK_IS_ON() |
| int shared_cache_memory=0; |
| sqlite_result_code=ToSqliteResultCode( |
| sqlite3_db_status(db_, SQLITE_DBSTATUS_CACHE_USED_SHARED, |
| &shared_cache_memory,&high_watermark,/*resetFlg=*/0)); |
| DCHECK_EQ(sqlite_result_code,SqliteResultCode::kOk) |
| <<"sqlite3_db_status(SQLITE_DBSTATUS_CACHE_USED_SHARED) failed"; |
| DCHECK_EQ(shared_cache_memory, cache_memory) |
| <<"Memory counting assumes that each database uses a private page cache"; |
| #endif// DCHECK_IS_ON() |
| |
| sqlite_result_code=ToSqliteResultCode(sqlite3_db_status( |
| db_, SQLITE_DBSTATUS_SCHEMA_USED,&schema_memory,&high_watermark, |
| /*resetFlg=*/0)); |
| DCHECK_EQ(sqlite_result_code,SqliteResultCode::kOk) |
| <<"sqlite3_db_status(SQLITE_DBSTATUS_SCHEMA_USED) failed"; |
| |
| sqlite_result_code=ToSqliteResultCode(sqlite3_db_status( |
| db_, SQLITE_DBSTATUS_STMT_USED,&statement_memory,&high_watermark, |
| /*resetFlg=*/0)); |
| DCHECK_EQ(sqlite_result_code,SqliteResultCode::kOk) |
| <<"sqlite3_db_status(SQLITE_DBSTATUS_STMT_USED) failed"; |
| |
| return cache_memory+ schema_memory+ statement_memory; |
| } |
| |
| intDatabase::GetErrorCode()const{ |
| if(!db_){ |
| return SQLITE_ERROR; |
| } |
| return sqlite3_extended_errcode(db_); |
| } |
| |
| intDatabase::GetLastErrno()const{ |
| if(!db_){ |
| return-1; |
| } |
| |
| int err=0; |
| if(SQLITE_OK!= |
| sqlite3_file_control(db_,nullptr, SQLITE_FCNTL_LAST_ERRNO,&err)){ |
| return-2; |
| } |
| |
| return err; |
| } |
| |
| constchar*Database::GetErrorMessage()const{ |
| if(!db_){ |
| return"sql::Database is not opened."; |
| } |
| return sqlite3_errmsg(db_); |
| } |
| |
| Database::ScopedOpenErrorReporter::ScopedOpenErrorReporter( |
| Database* db, |
| std::string_view histogram) |
| : db_(db), histogram_(histogram){ |
| db_->open_error_reporting_callback_= |
| base::BindRepeating(&Database::ScopedOpenErrorReporter::OnErrorDuringOpen, |
| base::Unretained(this)); |
| } |
| |
| Database::ScopedOpenErrorReporter::~ScopedOpenErrorReporter(){ |
| db_->open_error_reporting_callback_.Reset(); |
| } |
| |
| voidDatabase::ScopedOpenErrorReporter::OnErrorDuringOpen( |
| SqliteResultCode code){ |
| // Use `base::UmaHistogramSparse` because sqlite result codes aren't |
| // sequential. The large integers they represent make it so that the |
| // non-sparse histograms end up with too many buckets. |
| if(db_->histogram_tag().empty()){ |
| base::UmaHistogramSparse(base::StrCat({histogram_,".NoTag"}), |
| static_cast<int>(code)); |
| }else{ |
| base::UmaHistogramSparse( |
| base::StrCat({histogram_,".", db_->histogram_tag()}), |
| static_cast<int>(code)); |
| } |
| } |
| |
| voidDatabase::MaybeReportErrorDuringOpen(SqliteResultCode code){ |
| if(open_error_reporting_callback_){ |
| open_error_reporting_callback_.Run(code); |
| } |
| } |
| |
| boolDatabase::OpenInternal(const std::string& db_file_path){ |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| TRACE_EVENT1("sql","Database::OpenInternal","path", db_file_path); |
| base::ElapsedTimer timer; |
| |
| if(is_open()){ |
| DLOG(FATAL)<<"sql::Database is already open."; |
| RecordOpenDatabaseFailureReason(histogram_tag_, |
| OpenDatabaseFailedReason::kAlreadyOpened); |
| returnfalse; |
| } |
| |
| std::optional<base::ScopedBlockingCall> scoped_blocking_call; |
| InitScopedBlockingCall(FROM_HERE,&scoped_blocking_call); |
| |
| EnsureSqliteInitialized(); |
| |
| // If |poisoned_| is set, it means an error handler called |
| // RazeAndPoison(). Until regular Close() is called, the caller |
| // should be treating the database as open, but is_open() currently |
| // only considers the sqlite3 handle's state. |
| DCHECK(!poisoned_)<<"sql::Database is already open."; |
| poisoned_=false; |
| |
| // The flags are documented at https://www.sqlite.org/c3ref/open.html. |
| // |
| // Chrome uses SQLITE_OPEN_PRIVATECACHE because SQLite is used by many |
| // disparate features with their own databases, and having separate page |
| // caches makes it easier to reason about each feature's performance in |
| // isolation. |
| // |
| // SQLITE_OPEN_EXRESCODE enables the full range of SQLite error codes. See |
| // https://www.sqlite.org/rescode.html for details. |
| int open_flags= SQLITE_OPEN_EXRESCODE| SQLITE_OPEN_PRIVATECACHE; |
| |
| if(options_.read_only_){ |
| open_flags|=(SQLITE_OPEN_READONLY); |
| }else{ |
| open_flags|=(SQLITE_OPEN_READWRITE| SQLITE_OPEN_CREATE); |
| } |
| |
| std::string uri_file_path= db_file_path; |
| if(options_.exclusive_database_file_lock_){ |
| #if BUILDFLAG(IS_WIN) |
| constbool in_memory= db_file_path== kSqliteOpenInMemoryPath; |
| if(!in_memory){ |
| // Do not allow query injection. |
| if(base::Contains(db_file_path,'?')){ |
| RecordOpenDatabaseFailureReason( |
| histogram_tag_,OpenDatabaseFailedReason::kIncorrectPath); |
| returnfalse; |
| } |
| open_flags|= SQLITE_OPEN_URI; |
| uri_file_path= base::StrCat({"file:", db_file_path,"?exclusive=true"}); |
| } |
| #else |
| NOTREACHED() |
| <<"exclusive_database_file_lock is only supported on Windows."; |
| #endif// BUILDFLAG(IS_WIN) |
| } |
| |
| sqlite3* db=nullptr; |
| SqliteResultCode sqlite_result_code; |
| { |
| TRACE_EVENT1("sql","Database::OpenInternal sqlite3_open_v2","path", |
| db_file_path); |
| base::ElapsedTimer library_call_timer; |
| |
| sqlite_result_code=ToSqliteResultCode( |
| sqlite3_open_v2(uri_file_path.c_str(),&db, open_flags, |
| options_.vfs_name_discouraged_)); |
| |
| // If SQLITE_OPEN_READWRITE is specified, the database must not be opened in |
| // read-only mode. If it is, set the result code to |
| // SqliteResultCode::kReadOnly to prevent subsequent statements from |
| // executing and to disallow database use. This is crucial because on |
| // Windows, SQLite attempts to open the database in read-only mode if the |
| // initial read/write attempt fails. See the winOpen SQLite function for |
| // details: |
| // https://source.chromium.org/chromium/chromium/src/+/main:third_party/sqlite/src/src/os_win.c;l=5266-5269;drc=9bf5bea60709d4afa37a085b86de3651b0ddd5c9 |
| if(sqlite_result_code==SqliteResultCode::kOk&& db){ |
| constbool is_readonly= |
| sqlite3_db_readonly(db, kSqliteMainDatabaseName)==1; |
| if(options_.read_only_){ |
| DCHECK(is_readonly); |
| }elseif(is_readonly){ |
| sqlite_result_code=SqliteResultCode::kReadOnly; |
| } |
| } |
| |
| RecordTimingHistogram("Sql.Database.Success.SqliteOpenTime.", |
| library_call_timer.Elapsed()); |
| } |
| |
| if(sqlite_result_code==SqliteResultCode::kOk){ |
| db_= db; |
| }else{ |
| // sqlite3_open_v2() will usually create a database connection handle, even |
| // if an error occurs (see https://www.sqlite.org/c3ref/open.html). |
| if(db){ |
| // Deallocate resources allocated during the failed open. |
| // See https://www.sqlite.org/c3ref/close.html. |
| sqlite3_close(db); |
| } |
| |
| RecordOpenDatabaseFailureReason( |
| histogram_tag_,OpenDatabaseFailedReason::kSqliteOpenFailed); |
| MaybeReportErrorDuringOpen(sqlite_result_code); |
| OnSqliteError(ToSqliteErrorCode(sqlite_result_code),nullptr, |
| "-- sqlite3_open_v2()"); |
| returnfalse; |
| } |
| |
| ConfigureSqliteDatabaseObject(); |
| |
| // If indicated, enable shared mode ("NORMAL") on the database, so it can be |
| // opened by multiple processes. This needs to happen before WAL mode is |
| // enabled. |
| // |
| // TODO(crbug.com/40146017): Remove support for non-exclusive mode. |
| static_assert( |
| SQLITE_DEFAULT_LOCKING_MODE==1, |
| "Chrome assumes SQLite is configured to default to EXCLUSIVE locking"); |
| if(!options_.exclusive_locking_){ |
| if(!Execute("PRAGMA locking_mode=NORMAL")){ |
| RecordOpenDatabaseFailureReason( |
| histogram_tag_,OpenDatabaseFailedReason::kLockingModeFailed); |
| returnfalse; |
| } |
| } |
| |
| if(!options_.read_only_){ |
| // The sqlite3_open*() methods only perform I/O on the database file if a |
| // hot journal is found. Force SQLite to parse the header and database |
| // schema, so we can signal irrecoverable corruption early. |
| // |
| // sqlite3_table_column_metadata() causes SQLite to parse the database |
| // schema. Since the schema is stored inside a table B-tree, parsing the |
| // schema implies parsing the database header. |
| // |
| // sqlite3_table_column_metadata() can be used with a null database name, |
| // but that will cause it to search for the table in all databases that are |
| // ATTACHed to the connection. While Chrome features (almost) never use |
| // ATTACHed databases, we prefer to be explicit here. |
| // |
| // sqlite3_table_column_metadata() can be used with a null column name, and |
| // will report on the existence of the table with the given name. This is |
| // sufficient for the purpose of getting SQLite to parse the database |
| // schema. See https://www.sqlite.org/c3ref/table_column_metadata.html for |
| // details. |
| staticconstexprchar kSqliteSchemaTable[]="sqlite_schema"; |
| sqlite_result_code=ToSqliteResultCode(sqlite3_table_column_metadata( |
| db_, kSqliteMainDatabaseName, kSqliteSchemaTable, |
| /*zColumnName=*/nullptr, |
| /*pzDataType=*/nullptr,/*pzCollSeq=*/nullptr,/*pNotNull=*/nullptr, |
| /*pPrimaryKey=*/nullptr,/*pAutoinc=*/nullptr)); |
| if(sqlite_result_code!=SqliteResultCode::kOk){ |
| MaybeReportErrorDuringOpen(sqlite_result_code); |
| OnSqliteError(ToSqliteErrorCode(sqlite_result_code),nullptr, |
| "-- sqlite3_table_column_metadata()"); |
| RecordOpenDatabaseFailureReason( |
| histogram_tag_,OpenDatabaseFailedReason::kMetadataLoadingFailed); |
| returnfalse; |
| } |
| } |
| |
| const base::TimeDelta kBusyTimeout= base::Seconds(kBusyTimeoutSeconds); |
| |
| if(options_.read_only_){ |
| // This options isn't compatible with read-only mode. |
| CHECK_EQ(options_.page_size_,DatabaseOptions::kDefaultPageSize); |
| }else{ |
| // Needs to happen before entering WAL mode. Will only work if this the |
| // first time the database is being opened in WAL mode. |
| const std::string page_size_sql= |
| base::StringPrintf("PRAGMA page_size=%d", options_.page_size_); |
| if(!ExecuteWithTimeout(page_size_sql, kBusyTimeout)){ |
| RecordOpenDatabaseFailureReason( |
| histogram_tag_,OpenDatabaseFailedReason::kPageSizeFailed); |
| returnfalse; |
| } |
| |
| // https://www.sqlite.org/pragma.html#pragma_journal_mode |
| // WAL - Use a write-ahead log instead of a journal file. |
| // DELETE (default) - delete -journal file to commit. |
| // TRUNCATE - truncate -journal file to commit. |
| // PERSIST - zero out header of -journal file to commit. |
| // TRUNCATE should be faster than DELETE because it won't need directory |
| // changes for each transaction. PERSIST may break the spirit of using |
| // secure_delete. |
| // |
| // Needs to be performed after setting exclusive locking mode. Otherwise can |
| // fail if underlying VFS doesn't support shared memory. |
| if(UseWALMode()){ |
| // Set the synchronous flag to NORMAL. This means that writers don't flush |
| // the WAL file after every write. The WAL file is only flushed on a |
| // checkpoint. In this case, transactions might lose durability on a power |
| // loss (but still durable after an application crash). |
| // TODO(shuagga@microsoft.com): Evaluate if this loss of durability is a |
| // concern. |
| if(!Execute("PRAGMA synchronous=NORMAL")){ |
| RecordOpenDatabaseFailureReason( |
| histogram_tag_,OpenDatabaseFailedReason::kPragmaSynchronousFailed); |
| returnfalse; |
| } |
| |
| // Opening the db in WAL mode can fail (eg if the underlying VFS doesn't |
| // support shared memory and we are not in exclusive locking mode). |
| if(!Execute("PRAGMA journal_mode=WAL")){ |
| RecordOpenDatabaseFailureReason( |
| histogram_tag_,OpenDatabaseFailedReason::kPragmaJournalFailed); |
| returnfalse; |
| } |
| }else{ |
| // For speed, change the journal mode from the default DELETE to TRUNCATE. |
| // Both modes will delete the rollback journal at the conclusion of every |
| // transaction, but TRUNCATE is faster because it avoids touching the |
| // journal's parent directory[0]. |
| // |
| // PERSIST may be even faster because it zeroes out the journal's header |
| // without fully deleting its contents. Chrome used PERSIST until 2015, |
| // but switched to TRUNCATE to ensure that potentially-sensitive |
| // information is deleted from disk[1]. |
| // |
| // Per the SQLite docs[2], setting the journal mode has a sharp edge: the |
| // operation may succeed without actually changing the mode! It only makes |
| // sense to tolerate this successful failure because the default mode also |
| // deletes the journal's contents. |
| // |
| // [0]: https://crbug.com/118470#c4 |
| // [1]: https://crbug.com/493008 |
| // [2]: https://www.sqlite.org/pragma.html#pragma_journal_mode |
| if(!Execute("PRAGMA journal_mode=TRUNCATE")){ |
| RecordOpenDatabaseFailureReason( |
| histogram_tag_,OpenDatabaseFailedReason::kPragmaJournalFailed); |
| returnfalse; |
| } |
| } |
| } |
| |
| CHECK(db_); |
| |
| if(options_.read_only_){ |
| // These options are not compatible with read-only mode. |
| CHECK(!options_.flush_to_media_); |
| CHECK_EQ(options_.cache_size_,0); |
| }else{ |
| if(options_.flush_to_media_){ |
| std::ignore=Execute("PRAGMA fullfsync=1"); |
| } |
| |
| if(options_.cache_size_!=0){ |
| const std::string cache_size_sql= base::StrCat( |
| {"PRAGMA cache_size=", base::NumberToString(options_.cache_size_)}); |
| std::ignore=ExecuteWithTimeout(cache_size_sql, kBusyTimeout); |
| } |
| |
| static_assert(SQLITE_SECURE_DELETE==1, |
| "Chrome assumes secure_delete is on by default."); |
| |
| // When SQLite needs to grow a database file, it uses a configurable |
| // increment. Larger values reduce filesystem fragmentation and mmap() |
| // churn, as the database file is grown less often. Smaller values waste |
| // less disk space. |
| // |
| // We currently set different values for small vs large files. |
| // |
| // TODO(crbug.com/40827336): Replace file size-based heuristic with a |
| // DatabaseOptions member. Use the DatabaseOptions value for temporary |
| // databases as well. |
| sqlite3_file* file=GetSqliteVfsFile(); |
| |
| // GetSqliteVfsFile() returns null for in-memory and temporary databases. |
| // This is fine, because these databases start out empty, so the heuristic |
| // below would never set a chunk size on them anyway. |
| if(file){ |
| sqlite3_int64 db_size=0; |
| sqlite_result_code= |
| ToSqliteResultCode(file->pMethods->xFileSize(file,&db_size)); |
| if(sqlite_result_code==SqliteResultCode::kOk&& db_size>16*1024){ |
| int chunk_size=4*1024; |
| if(db_size>128*1024){ |
| chunk_size=32*1024; |
| } |
| |
| sqlite3_file_control(db_,/*zDbName=*/nullptr, SQLITE_FCNTL_CHUNK_SIZE, |
| &chunk_size); |
| } |
| } |
| } |
| |
| size_t mmap_size= mmap_disabled_?0:ComputeMmapSizeForOpen(); |
| |
| // We explicitly issue a "PRGAMA mmap_size=0" to disable memory-mapping. We |
| // could skip executing the PRAGMA in that case, and use a static_assert to |
| // ensure that SQLITE_DEFAULT_MMAP_SIZE > 0. We didn't choose this alternative |
| // because would cost us a bit more logic, and the optimization would apply to |
| // edge cases, such as in-memory databases. More details at |
| // https://www.sqlite.org/pragma.html#pragma_mmap_size. |
| std::ignore=Execute( |
| base::StrCat({"PRAGMA mmap_size=", base::NumberToString(mmap_size)})); |
| |
| // Determine if memory-mapping has actually been enabled. The Execute() above |
| // can succeed without changing the amount mapped. |
| mmap_enabled_=false; |
| { |
| Statement pragma_mmap_size(GetUniqueStatement("PRAGMA mmap_size")); |
| if(pragma_mmap_size.Step()&& pragma_mmap_size.ColumnInt64(0)>0){ |
| mmap_enabled_=true; |
| } |
| } |
| |
| DCHECK(!memory_dump_provider_); |
| memory_dump_provider_= |
| std::make_unique<DatabaseMemoryDumpProvider>(db_, histogram_tag_); |
| base::trace_event::MemoryDumpManager::GetInstance()->RegisterDumpProvider( |
| memory_dump_provider_.get(),"sql::Database",/*task_runner=*/nullptr); |
| |
| RecordTimingHistogram("Sql.Database.Success.OpenInternalTime.", |
| timer.Elapsed()); |
| |
| return is_open(); |
| } |
| |
| voidDatabase::PreloadInternal(const base::FilePath& path){ |
| TRACE_EVENT0("sql","Database::PreloadInternal"); |
| |
| // TODO(crbug.com/40904059): Consider moving this to a DCHECK after fixing |
| // or migrating callsites that call Preload(...) on in-memory databases. |
| if(!in_memory_){ |
| return; |
| } |
| |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| |
| // Maximum number of bytes that will be prefetched from the database. |
| // |
| // This limit is very aggressive. The main trade-off involved is that having |
| // SQLite block on reading from disk has a high impact on Chrome startup cost |
| // for the databases that are on the critical path to startup. So, the limit |
| // must exceed the expected sizes of databases on the critical path. |
| staticconstexprint kPreReadSize=128*1024*1024;// 128 MB |
| base::PreReadFile(path,/*is_executable=*/false,/*sequential=*/false, |
| kPreReadSize); |
| } |
| |
| voidDatabase::ConfigureSqliteDatabaseObject(){ |
| auto sqlite_result_code=ToSqliteResultCode( |
| sqlite3_db_config(db_, SQLITE_DBCONFIG_ENABLE_FKEY,0,nullptr)); |
| DCHECK_EQ(sqlite_result_code,SqliteResultCode::kOk) |
| <<"sqlite3_db_config(SQLITE_DBCONFIG_ENABLE_FKEY) should not fail"; |
| |
| sqlite_result_code=ToSqliteResultCode( |
| sqlite3_db_config(db_, SQLITE_DBCONFIG_ENABLE_TRIGGER, |
| options_.enable_triggers_?1:0,nullptr)); |
| DCHECK_EQ(sqlite_result_code,SqliteResultCode::kOk) |
| <<"sqlite3_db_config() should not fail"; |
| |
| sqlite_result_code=ToSqliteResultCode( |
| sqlite3_db_config(db_, SQLITE_DBCONFIG_ENABLE_VIEW, |
| options_.enable_views_discouraged_?1:0,nullptr)); |
| DCHECK_EQ(sqlite_result_code,SqliteResultCode::kOk) |
| <<"sqlite3_db_config() should not fail"; |
| } |
| |
| voidDatabase::DoRollback(){ |
| TRACE_EVENT0("sql","Database::DoRollback"); |
| |
| Statement rollback(GetCachedStatement(SQL_FROM_HERE,"ROLLBACK")); |
| |
| rollback.Run(); |
| |
| // The cache may have been accumulating dirty pages for commit. Note that in |
| // some cases sql::Transaction can fire rollback after a database is closed. |
| if(is_open()){ |
| ReleaseCacheMemoryIfNeeded(false); |
| } |
| |
| needs_rollback_=false; |
| } |
| |
| voidDatabase::StatementRefCreated(StatementRef* ref){ |
| DCHECK(!open_statements_.count(ref)) |
| << __func__<<" already called with this statement"; |
| open_statements_.insert(ref); |
| } |
| |
| voidDatabase::StatementRefDeleted(StatementRef* ref){ |
| DCHECK(open_statements_.count(ref)) |
| << __func__<<" called with non-existing statement"; |
| open_statements_.erase(ref); |
| } |
| |
| voidDatabase::OnSqliteError(SqliteErrorCode sqlite_error_code, |
| sql::Statement* statement, |
| constchar* sql_statement){ |
| TRACE_EVENT1("sql","Database::OnSqliteError","sqlite_error_code", |
| sqlite_error_code); |
| |
| DCHECK_NE(statement!=nullptr, sql_statement!=nullptr) |
| << __func__<<" should either get a Statement or a raw SQL string"; |
| |
| base::WeakPtr<Database> weak_this= |
| weak_factory_lifetime_tracker_.GetWeakPtr(); |
| ++handling_error_nesting_; |
| |
| // Use `base::UmaHistogramSparse` because sqlite result codes aren't |
| // sequential. The large integers they represent make it so that the |
| // non-sparse histograms end up with too many buckets. |
| if(!histogram_tag().empty()){ |
| base::UmaHistogramSparse( |
| base::StrCat({"Sql.Database.Statement.Error.", histogram_tag()}), |
| static_cast<int>(sqlite_error_code)); |
| } |
| |
| // Log errors for developers. |
| // |
| // This block is wrapped around a DCHECK_IS_ON() check so we don't waste CPU |
| // cycles computing the strings that make up the log message in production. |
| #if DCHECK_IS_ON() |
| std::string logged_statement; |
| if(statement){ |
| logged_statement= statement->GetSQLStatement(); |
| }else{ |
| logged_statement= sql_statement; |
| } |
| |
| std::string database_id= histogram_tag_; |
| if(database_id.empty()){ |
| database_id=DbPath().BaseName().AsUTF8Unsafe(); |
| } |
| |
| // This logging block cannot be a DCHECK, because valid usage of sql::Database |
| // can still encounter SQLite errors in production. For example, valid SQL |
| // statements can fail when a database is corrupted. |
| // |
| // This logging block should not use LOG(ERROR) because many features built on |
| // top of sql::Database can recover from most errors. |
| DVLOG(1)<<"SQLite error! This may indicate a programming error!\n" |
| <<"Database: "<< database_id |
| <<" sqlite_error_code: "<< sqlite_error_code |
| <<" errno: "<<GetLastErrno() |
| <<"\nSQLite error description: "<<GetErrorMessage() |
| <<"\nSQL statement: "<< logged_statement; |
| #endif// DCHECK_IS_ON() |
| |
| // Inform the error expecter that we've encountered the error. |
| std::ignore=IsExpectedSqliteError(static_cast<int>(sqlite_error_code)); |
| |
| if(!error_callback_.is_null()){ |
| // Create an additional reference to the state in `error_callback_`, so the |
| // state doesn't go away if the callback changes `error_callback_` by |
| // calling set_error_callback() or reset_error_callback(). This avoids a |
| // subtle source of use-after-frees. See https://crbug.com/254584. |
| ErrorCallback error_callback_copy= error_callback_; |
| error_callback_copy.Run(static_cast<int>(sqlite_error_code), statement); |
| } |
| |
| if(weak_this){ |
| --weak_this->handling_error_nesting_; |
| } |
| } |
| |
| std::stringDatabase::GetDiagnosticInfo(int sqlite_error_code, |
| Statement* statement, |
| DatabaseDiagnostics* diagnostics){ |
| DCHECK_NE(sqlite_error_code, SQLITE_OK) |
| << __func__<<" received non-error result code"; |
| DCHECK_NE(sqlite_error_code, SQLITE_DONE) |
| << __func__<<" received non-error result code"; |
| DCHECK_NE(sqlite_error_code, SQLITE_ROW) |
| << __func__<<" received non-error result code"; |
| |
| // Prevent reentrant calls to the error callback. |
| ErrorCallback original_callback= std::move(error_callback_); |
| error_callback_.Reset(); |
| |
| if(diagnostics){ |
| diagnostics->reported_sqlite_error_code= sqlite_error_code; |
| } |
| |
| // Trim extended error codes. |
| constint primary_error_code= sqlite_error_code&0xff; |
| |
| // CollectCorruptionInfo() is implemented in terms of sql::Database, |
| // TODO(shess): Rewrite IntegrityCheckHelper() in terms of raw SQLite. |
| std::string result= |
| (primary_error_code== SQLITE_CORRUPT) |
| ?CollectCorruptionInfo() |
| :CollectErrorInfo(sqlite_error_code, statement, diagnostics); |
| |
| // The following queries must be executed after CollectErrorInfo() above, so |
| // if they result in their own errors, they don't interfere with |
| // CollectErrorInfo(). |
| constbool has_valid_header=Execute("PRAGMA auto_vacuum"); |
| constbool has_valid_schema=Execute("SELECT COUNT(*) FROM sqlite_schema"); |
| |
| // Restore the original error callback. |
| error_callback_= std::move(original_callback); |
| |
| base::StringAppendF(&result,"Has valid header: %s\n", |
| (has_valid_header?"Yes":"No")); |
| base::StringAppendF(&result,"Has valid schema: %s\n", |
| (has_valid_schema?"Yes":"No")); |
| if(diagnostics){ |
| diagnostics->has_valid_header= has_valid_header; |
| diagnostics->has_valid_schema= has_valid_schema; |
| } |
| |
| return result; |
| } |
| |
| boolDatabase::FullIntegrityCheck(std::vector<std::string>* messages){ |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| messages->clear(); |
| |
| // The PRAGMA below has the side effect of setting SQLITE_RecoveryMode, which |
| // allows SQLite to process through certain cases of corruption. |
| if(!Execute("PRAGMA writable_schema=ON")){ |
| // The "PRAGMA integrity_check" statement executed below may return less |
| // useful information. However, incomplete information is still better than |
| // nothing, so we press on. |
| messages->push_back("PRAGMA writable_schema=ON failed"); |
| } |
| |
| // We need to bypass sql::Statement and use raw SQLite C API calls here. |
| // |
| // "PRAGMA integrity_check" reports SQLITE_CORRUPT when the database is |
| // corrupt. Reporting SQLITE_CORRUPT is undesirable in this case, because it |
| // causes our sql::Statement infrastructure to call the database error |
| // handler, which triggers feature-level error handling. However, |
| // FullIntegrityCheck() callers presumably already know that the database is |
| // corrupted, and are trying to collect diagnostic information for reporting. |
| sqlite3_stmt* statement=nullptr; |
| |
| // https://www.sqlite.org/c3ref/prepare.html states that SQLite will perform |
| // slightly better if sqlite_prepare_v3() receives a zero-terminated statement |
| // string, and a statement size that includes the zero byte. Fortunately, |
| // C++'s string literal and sizeof() operator do exactly that. |
| constexprchar kIntegrityCheckSql[]="PRAGMA integrity_check"; |
| constauto prepare_result_code=ToSqliteResultCode( |
| sqlite3_prepare_v3(db_, kIntegrityCheckSql,sizeof(kIntegrityCheckSql), |
| kPrepareFlags,&statement,/*pzTail=*/nullptr)); |
| if(prepare_result_code!=SqliteResultCode::kOk){ |
| returnfalse; |
| } |
| |
| // "PRAGMA integrity_check" currently returns multiple lines as a single row. |
| // |
| // However, since https://www.sqlite.org/pragma.html#pragma_integrity_check |
| // states that multiple records may be returned, the code below can handle |
| // multiple records, each of which has multiple lines. |
| std::vector<std::string> result_lines; |
| |
| while(ToSqliteResultCode(sqlite3_step(statement))== |
| SqliteResultCode::kRow){ |
| constuint8_t* row= chrome_sqlite3_column_text(statement,/*iCol=*/0); |
| DCHECK(row)<<"PRAGMA integrity_check should never return NULL rows"; |
| |
| constint row_size= sqlite3_column_bytes(statement,/*iCol=*/0); |
| std::string_view row_string(reinterpret_cast<constchar*>(row), row_size); |
| |
| const std::vector<std::string_view> row_lines= base::SplitStringPiece( |
| row_string,"\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); |
| for(std::string_view row_line: row_lines){ |
| result_lines.emplace_back(row_line); |
| } |
| } |
| |
| constauto finalize_result_code= |
| ToSqliteResultCode(sqlite3_finalize(statement)); |
| // sqlite3_finalize() may return SQLITE_CORRUPT when the integrity check |
| // discovers any problems. We still consider this case a success, as long as |
| // the statement produced at least one diagnostic message. |
| constbool success=(result_lines.size()>0)|| |
| (finalize_result_code==SqliteResultCode::kOk); |
| *messages= std::move(result_lines); |
| |
| // Best-effort attempt to undo the "PRAGMA writable_schema=ON" executed above. |
| std::ignore=Execute("PRAGMA writable_schema=OFF"); |
| |
| return success; |
| } |
| |
| boolDatabase::ReportMemoryUsage(base::trace_event::ProcessMemoryDump* pmd, |
| const std::string& dump_name){ |
| return memory_dump_provider_&& |
| memory_dump_provider_->ReportMemoryUsage(pmd, dump_name); |
| } |
| |
| boolDatabase::UseWALMode()const{ |
| #if BUILDFLAG(IS_FUCHSIA) |
| // WAL mode is only enabled on Fuchsia for databases with exclusive |
| // locking, because this case does not require shared memory support. |
| // At the time this was implemented (May 2020), Fuchsia's shared |
| // memory support was insufficient for SQLite's needs. |
| return options_.wal_mode_&& options_.exclusive_locking_; |
| #else |
| return options_.wal_mode_; |
| #endif// BUILDFLAG(IS_FUCHSIA) |
| } |
| |
| boolDatabase::CheckpointDatabase(){ |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| std::optional<base::ScopedBlockingCall> scoped_blocking_call; |
| InitScopedBlockingCall(FROM_HERE,&scoped_blocking_call); |
| |
| auto sqlite_result_code=ToSqliteResultCode(sqlite3_wal_checkpoint_v2( |
| db_, kSqliteMainDatabaseName, SQLITE_CHECKPOINT_PASSIVE, |
| /*pnLog=*/nullptr,/*pnCkpt=*/nullptr)); |
| |
| return sqlite_result_code==SqliteResultCode::kOk; |
| } |
| |
| }// namespace sql |