blob: 6f7f4ba1660b4f7a977212c8563a43c665e30b54 [file] [log] [blame]
/*
* Copyright (C) 2018 Sony Interactive Entertainment Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "SearchPopupMenuDB.h"
#include "SQLiteFileSystem.h"
#include "SQLiteTransaction.h"
#include <wtf/FileSystem.h>
#include <wtf/Vector.h>
#include <wtf/text/StringConcatenateNumbers.h>
namespace WebCore {
static const int schemaVersion = 1;
static constexpr auto createSearchTableSQL {
"CREATE TABLE IF NOT EXISTS Search ("
" name TEXT NOT NULL,"
" position INTEGER NOT NULL,"
" value TEXT,"
" UNIQUE(name, position)"
");"_s
};
static constexpr auto loadSearchTermsForNameSQL {
"SELECT value FROM Search "
"WHERE name = ? "
"ORDER BY position;"_s
};
static constexpr auto insertSearchTermSQL {
"INSERT INTO Search(name, position, value) "
"VALUES(?, ?, ?);"_s
};
static constexpr auto removeSearchTermsForNameSQL {
"DELETE FROM Search where name = ?;"_s
};
SearchPopupMenuDB& SearchPopupMenuDB::singleton()
{
static SearchPopupMenuDB instance;
return instance;
}
SearchPopupMenuDB::SearchPopupMenuDB()
: m_databaseFilename(FileSystem::pathByAppendingComponent(FileSystem::localUserSpecificStorageDirectory(), "autosave-search.db"))
{
}
SearchPopupMenuDB::~SearchPopupMenuDB()
{
closeDatabase();
}
void SearchPopupMenuDB::saveRecentSearches(const String& name, const Vector<RecentSearch>& searches)
{
if (!m_database.isOpen()) {
if (!openDatabase())
return;
}
bool success = true;
SQLiteTransaction transaction(m_database, false);
m_removeSearchTermsForNameStatement->bindText(1, name);
auto stepRet = m_removeSearchTermsForNameStatement->step();
success = stepRet == SQLITE_DONE;
m_removeSearchTermsForNameStatement->reset();
int index = 0;
for (const auto& search : searches) {
m_insertSearchTermStatement->bindText(1, name);
m_insertSearchTermStatement->bindInt(2, index);
m_insertSearchTermStatement->bindText(3, search.string);
if (success) {
stepRet = m_insertSearchTermStatement->step();
success = stepRet == SQLITE_DONE;
}
m_insertSearchTermStatement->reset();
index++;
}
if (success)
transaction.commit();
checkSQLiteReturnCode(stepRet);
}
void SearchPopupMenuDB::loadRecentSearches(const String& name, Vector<RecentSearch>& searches)
{
if (!m_database.isOpen()) {
if (!openDatabase())
return;
}
searches.clear();
m_loadSearchTermsForNameStatement->bindText(1, name);
while (m_loadSearchTermsForNameStatement->step() == SQLITE_ROW) {
// We are choosing not to use or store search times on Windows at this time, so for now it's OK to use a "distant past" time as a placeholder.
searches.append({ m_loadSearchTermsForNameStatement->getColumnText(0), -WallTime::infinity() });
}
m_loadSearchTermsForNameStatement->reset();
}
bool SearchPopupMenuDB::checkDatabaseValidity()
{
ASSERT(m_database.isOpen());
if (!m_database.tableExists("Search"))
return false;
SQLiteStatement integrity(m_database, "PRAGMA quick_check;");
if (integrity.prepare() != SQLITE_OK) {
LOG_ERROR("Failed to execute database integrity check");
return false;
}
int resultCode = integrity.step();
if (resultCode != SQLITE_ROW) {
LOG_ERROR("Integrity quick_check step returned %d", resultCode);
return false;
}
int columns = integrity.columnCount();
if (columns != 1) {
LOG_ERROR("Received %i columns performing integrity check, should be 1", columns);
return false;
}
String resultText = integrity.getColumnText(0);
if (resultText != "ok") {
LOG_ERROR("Search autosave database integrity check failed - %s", resultText.ascii().data());
return false;
}
return true;
}
void SearchPopupMenuDB::deleteAllDatabaseFiles()
{
closeDatabase();
FileSystem::deleteFile(m_databaseFilename);
FileSystem::deleteFile(m_databaseFilename + "-shm");
FileSystem::deleteFile(m_databaseFilename + "-wal");
}
bool SearchPopupMenuDB::openDatabase()
{
bool existsDatabaseFile = SQLiteFileSystem::ensureDatabaseFileExists(m_databaseFilename, false);
if (existsDatabaseFile) {
if (m_database.open(m_databaseFilename)) {
if (!checkDatabaseValidity()) {
// delete database and try to re-create again
LOG_ERROR("Search autosave database validity check failed, attempting to recreate the database");
m_database.close();
deleteAllDatabaseFiles();
existsDatabaseFile = false;
}
} else {
LOG_ERROR("Failed to open search autosave database: %s, attempting to recreate the database", m_databaseFilename.utf8().data());
deleteAllDatabaseFiles();
existsDatabaseFile = false;
}
}
if (!existsDatabaseFile) {
if (!FileSystem::makeAllDirectories(FileSystem::directoryName(m_databaseFilename)))
LOG_ERROR("Failed to create the search autosave database path %s", m_databaseFilename.utf8().data());
m_database.open(m_databaseFilename);
}
if (!m_database.isOpen())
return false;
if (!m_database.turnOnIncrementalAutoVacuum())
LOG_ERROR("Unable to turn on incremental auto-vacuum (%d %s)", m_database.lastError(), m_database.lastErrorMsg());
verifySchemaVersion();
bool databaseValidity = true;
if (!existsDatabaseFile || !m_database.tableExists("Search"))
databaseValidity = databaseValidity && (executeSimpleSql(createSearchTableSQL) == SQLITE_DONE);
if (!databaseValidity) {
// give up create database at this time (search terms will not be saved)
m_database.close();
deleteAllDatabaseFiles();
return false;
}
m_database.setSynchronous(SQLiteDatabase::SyncNormal);
m_loadSearchTermsForNameStatement = createPreparedStatement(loadSearchTermsForNameSQL);
m_insertSearchTermStatement = createPreparedStatement(insertSearchTermSQL);
m_removeSearchTermsForNameStatement = createPreparedStatement(removeSearchTermsForNameSQL);
return true;
}
void SearchPopupMenuDB::closeDatabase()
{
if (m_database.isOpen()) {
m_loadSearchTermsForNameStatement->finalize();
m_insertSearchTermStatement->finalize();
m_removeSearchTermsForNameStatement->finalize();
m_database.close();
}
}
void SearchPopupMenuDB::verifySchemaVersion()
{
int version = SQLiteStatement(m_database, "PRAGMA user_version").getColumnInt(0);
if (version == schemaVersion)
return;
switch (version) {
// Placeholder for schema version upgrade logic
// Ensure cases fall through to the next version's upgrade logic
case 0:
m_database.clearAllTables();
break;
default:
// This case can be reached when downgrading versions
LOG_ERROR("Unknown search autosave database version: %d", version);
m_database.clearAllTables();
break;
}
// Update version
executeSimpleSql(makeString("PRAGMA user_version=", schemaVersion));
}
void SearchPopupMenuDB::checkSQLiteReturnCode(int actual)
{
switch (actual) {
case SQLITE_CORRUPT:
case SQLITE_SCHEMA:
case SQLITE_FORMAT:
case SQLITE_NOTADB:
// Database has been corrupted during the run
// so we'll recreate the db.
deleteAllDatabaseFiles();
openDatabase();
}
}
int SearchPopupMenuDB::executeSimpleSql(const String& sql, bool ignoreError)
{
SQLiteStatement statement(m_database, sql);
int ret = statement.prepareAndStep();
statement.finalize();
checkSQLiteReturnCode(ret);
if (ret != SQLITE_OK && ret != SQLITE_DONE && ret != SQLITE_ROW && !ignoreError)
LOG_ERROR("Failed to execute %s error: %s", sql.ascii().data(), m_database.lastErrorMsg());
return ret;
}
std::unique_ptr<SQLiteStatement> SearchPopupMenuDB::createPreparedStatement(const String& sql)
{
auto statement = makeUnique<SQLiteStatement>(m_database, sql);
int ret = statement->prepare();
ASSERT_UNUSED(ret, ret == SQLITE_OK);
return statement;
}
}