blob: cee3b56f40cc131914526a26be17ca3f31482f58 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/conversions/conversion_storage_sql_migrations.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "content/browser/conversions/conversion_storage_sql.h"
#include "content/browser/conversions/sql_utils.h"
#include "content/browser/conversions/storable_impression.h"
#include "sql/database.h"
#include "sql/meta_table.h"
#include "sql/statement.h"
#include "sql/transaction.h"
namespace content {
namespace {
// |ConversionStorageSql::GetActiveImpressions()| cannot be used for migration
// logic as it may use columns that are not present in older versions.
std::vector<StorableImpression> GetImpressions(sql::Database* db,
int64_t start_impression_id,
int num_impressions) {
DCHECK_GE(num_impressions, 0);
const char kGetImpressionsSql[] =
"SELECT impression_data, impression_origin, conversion_origin, "
"reporting_origin, impression_time, expiry_time, impression_id "
"FROM impressions "
"WHERE impression_id >= ? "
"ORDER BY impression_id "
"LIMIT ?";
sql::Statement statement(
db->GetCachedStatement(SQL_FROM_HERE, kGetImpressionsSql));
statement.BindInt64(0, start_impression_id);
statement.BindInt(1, num_impressions);
std::vector<StorableImpression> impressions;
while (statement.Step()) {
std::string impression_data = statement.ColumnString(0);
url::Origin impression_origin =
DeserializeOrigin(statement.ColumnString(1));
url::Origin conversion_destination =
DeserializeOrigin(statement.ColumnString(2));
url::Origin reporting_origin = DeserializeOrigin(statement.ColumnString(3));
base::Time impression_time = statement.ColumnTime(4);
base::Time expiry_time = statement.ColumnTime(5);
int64_t impression_id = statement.ColumnInt64(6);
// All impressions prior to the addition of the |source_type| column are
// |kNavigation|.
StorableImpression impression(impression_data, impression_origin,
conversion_destination, reporting_origin,
impression_time, expiry_time,
StorableImpression::SourceType::kNavigation,
/*priority=*/0, impression_id);
impressions.push_back(std::move(impression));
}
if (!statement.Succeeded())
return {};
return impressions;
}
} // namespace
bool ConversionStorageSqlMigrations::UpgradeSchema(
ConversionStorageSql* conversion_storage,
sql::Database* db,
sql::MetaTable* meta_table) {
base::ThreadTicks start_timestamp = base::ThreadTicks::Now();
if (meta_table->GetVersionNumber() == 1) {
if (!MigrateToVersion2(conversion_storage, db, meta_table))
return false;
}
if (meta_table->GetVersionNumber() == 2) {
if (!MigrateToVersion3(conversion_storage, db, meta_table))
return false;
}
if (meta_table->GetVersionNumber() == 3) {
if (!MigrateToVersion4(conversion_storage, db, meta_table))
return false;
}
if (meta_table->GetVersionNumber() == 4) {
if (!MigrateToVersion5(conversion_storage, db, meta_table))
return false;
}
if (meta_table->GetVersionNumber() == 5) {
if (!MigrateToVersion6(conversion_storage, db, meta_table))
return false;
}
// Add similar if () blocks for new versions here.
base::UmaHistogramMediumTimes("Conversions.Storage.MigrationTime",
base::ThreadTicks::Now() - start_timestamp);
return true;
}
bool ConversionStorageSqlMigrations::MigrateToVersion2(
ConversionStorageSql* conversion_storage,
sql::Database* db,
sql::MetaTable* meta_table) {
// Wrap each migration in its own transaction. This results in smaller
// transactions, so it's less likely that a transaction's buffer will need to
// spill to disk. Also, if the database grows a lot and Chrome stops (user
// quit, process kill, etc.) during the migration process, per-migration
// transactions make it more likely that we'll make forward progress each time
// Chrome stops.
sql::Transaction transaction(db);
if (!transaction.Begin())
return false;
// Add a new conversion_destination column to the impressions table. This
// follows the steps documented at
// https://sqlite.org/lang_altertable.html#otheralter. Other approaches, like
// using "ALTER ... ADD COLUMN" require setting a DEFAULT value for the column
// which is undesirable.
const char kNewImpressionTableSql[] =
"CREATE TABLE IF NOT EXISTS new_impressions"
"(impression_id INTEGER PRIMARY KEY,"
"impression_data TEXT NOT NULL,"
"impression_origin TEXT NOT NULL,"
"conversion_origin TEXT NOT NULL,"
"reporting_origin TEXT NOT NULL,"
"impression_time INTEGER NOT NULL,"
"expiry_time INTEGER NOT NULL,"
"num_conversions INTEGER DEFAULT 0,"
"active INTEGER DEFAULT 1,"
"conversion_destination TEXT NOT NULL)";
if (!db->Execute(kNewImpressionTableSql))
return false;
// Transfer the existing rows to the new table, inserting a placeholder for
// the conversion_destination column.
const char kPopulateNewImpressionTableSql[] =
"INSERT INTO new_impressions SELECT "
"impression_id,impression_data,impression_origin,"
"conversion_origin,reporting_origin,impression_time,"
"expiry_time,num_conversions,active,'' "
"FROM impressions";
if (!db->Execute(kPopulateNewImpressionTableSql))
return false;
const char kDropOldImpressionTableSql[] = "DROP TABLE impressions";
if (!db->Execute(kDropOldImpressionTableSql))
return false;
const char kRenameImpressionTableSql[] =
"ALTER TABLE new_impressions RENAME TO impressions";
if (!db->Execute(kRenameImpressionTableSql))
return false;
// Update each of the impression rows to have the correct associated
// conversion_destination. This is only relevant for active impressions, as
// the column is only used for matching impressions to conversions, but we
// update all impressions regardless.
//
// We update `kNumImpressionsPerUpdate` rows at a time, to avoid pulling the
// entire impressions table into memory.
int64_t start_impression_id = 0;
const size_t kNumImpressionsPerUpdate = 100u;
std::vector<StorableImpression> impressions =
GetImpressions(db, start_impression_id, kNumImpressionsPerUpdate);
const char kUpdateDestinationSql[] =
"UPDATE impressions SET conversion_destination = ? WHERE impression_id = "
"?";
sql::Statement update_destination_statement(
db->GetCachedStatement(SQL_FROM_HERE, kUpdateDestinationSql));
while (!impressions.empty()) {
// Perform the column updates for each row we pulled into memory.
for (const auto& impression : impressions) {
update_destination_statement.Reset(/*clear_bound_vars=*/true);
// The conversion destination is derived from the conversion origin
// dynamically.
update_destination_statement.BindString(
0, impression.ConversionDestination().Serialize());
update_destination_statement.BindInt64(1, *impression.impression_id());
update_destination_statement.Run();
// Track the largest row id. This is more efficient than sorting all the
// rows.
if (*impression.impression_id() > start_impression_id)
start_impression_id = *impression.impression_id();
}
// Fetch the next batch of rows from the database.
start_impression_id += 1;
impressions =
GetImpressions(db, start_impression_id, kNumImpressionsPerUpdate);
}
// Create the pre-existing impression table indices on the new table.
const char kImpressionExpiryIndexSql[] =
"CREATE INDEX IF NOT EXISTS impression_expiry_idx "
"ON impressions(expiry_time)";
if (!db->Execute(kImpressionExpiryIndexSql))
return false;
const char kImpressionOriginIndexSql[] =
"CREATE INDEX IF NOT EXISTS impression_origin_idx "
"ON impressions(impression_origin)";
if (!db->Execute(kImpressionOriginIndexSql))
return false;
// Replace the pre-existing conversion_origin_idx with an index that uses the
// conversion destination, as attribution logic now depends on the
// conversion_destination.
const char kConversionDestinationIndexSql[] =
"CREATE INDEX IF NOT EXISTS conversion_destination_idx "
"ON impressions(active, conversion_destination, reporting_origin)";
if (!db->Execute(kConversionDestinationIndexSql))
return false;
meta_table->SetVersionNumber(2);
return transaction.Commit();
}
bool ConversionStorageSqlMigrations::MigrateToVersion3(
ConversionStorageSql* conversion_storage,
sql::Database* db,
sql::MetaTable* meta_table) {
// Wrap each migration in its own transaction. See comment in
// |MigrateToVersion2|.
sql::Transaction transaction(db);
if (!transaction.Begin())
return false;
// Add new source_type and attributed_truthfully columns to the impressions
// table. This follows the steps documented at
// https://sqlite.org/lang_altertable.html#otheralter. Other approaches, like
// using "ALTER ... ADD COLUMN" require setting a DEFAULT value for the column
// which is undesirable.
const char kNewImpressionTableSql[] =
"CREATE TABLE IF NOT EXISTS new_impressions"
"(impression_id INTEGER PRIMARY KEY,"
"impression_data TEXT NOT NULL,"
"impression_origin TEXT NOT NULL,"
"conversion_origin TEXT NOT NULL,"
"reporting_origin TEXT NOT NULL,"
"impression_time INTEGER NOT NULL,"
"expiry_time INTEGER NOT NULL,"
"num_conversions INTEGER DEFAULT 0,"
"active INTEGER DEFAULT 1,"
"conversion_destination TEXT NOT NULL,"
"source_type INTEGER NOT NULL,"
"attributed_truthfully INTEGER NOT NULL)";
if (!db->Execute(kNewImpressionTableSql))
return false;
// Transfer the existing rows to the new table, inserting default values for
// the source_type and attributed_truthfully columns.
const char kPopulateNewImpressionTableSql[] =
"INSERT INTO new_impressions SELECT "
"impression_id,impression_data,impression_origin,"
"conversion_origin,reporting_origin,impression_time,"
"expiry_time,num_conversions,active,conversion_destination,?,? "
"FROM impressions";
sql::Statement populate_statement(
db->GetCachedStatement(SQL_FROM_HERE, kPopulateNewImpressionTableSql));
// Only navigation type was supported prior to this column being added.
populate_statement.BindInt(
0, static_cast<int>(StorableImpression::SourceType::kNavigation));
populate_statement.BindBool(1, true);
if (!populate_statement.Run())
return false;
const char kDropOldImpressionTableSql[] = "DROP TABLE impressions";
if (!db->Execute(kDropOldImpressionTableSql))
return false;
const char kRenameImpressionTableSql[] =
"ALTER TABLE new_impressions RENAME TO impressions";
if (!db->Execute(kRenameImpressionTableSql))
return false;
// Create the pre-existing impression table indices on the new table.
const char kImpressionExpiryIndexSql[] =
"CREATE INDEX IF NOT EXISTS impression_expiry_idx "
"ON impressions(expiry_time)";
if (!db->Execute(kImpressionExpiryIndexSql))
return false;
const char kImpressionOriginIndexSql[] =
"CREATE INDEX IF NOT EXISTS impression_origin_idx "
"ON impressions(impression_origin)";
if (!db->Execute(kImpressionOriginIndexSql))
return false;
const char kConversionDestinationIndexSql[] =
"CREATE INDEX IF NOT EXISTS conversion_destination_idx "
"ON impressions(active, conversion_destination, reporting_origin)";
if (!db->Execute(kConversionDestinationIndexSql))
return false;
meta_table->SetVersionNumber(3);
return transaction.Commit();
}
bool ConversionStorageSqlMigrations::MigrateToVersion4(
ConversionStorageSql* conversion_storage,
sql::Database* db,
sql::MetaTable* meta_table) {
// Wrap each migration in its own transaction. See comment in
// |MigrateToVersion2|.
sql::Transaction transaction(db);
if (!transaction.Begin())
return false;
if (!conversion_storage->rate_limit_table_.CreateTable(db))
return false;
meta_table->SetVersionNumber(4);
return transaction.Commit();
}
bool ConversionStorageSqlMigrations::MigrateToVersion5(
ConversionStorageSql* conversion_storage,
sql::Database* db,
sql::MetaTable* meta_table) {
// Wrap each migration in its own transaction. See comment in
// |MigrateToVersion2|.
sql::Transaction transaction(db);
if (!transaction.Begin())
return false;
// Any corresponding impressions will naturally be cleaned up by the expiry
// logic.
const char kDropZeroCreditConversionsSql[] =
"DELETE FROM conversions WHERE attribution_credit = 0";
if (!db->Execute(kDropZeroCreditConversionsSql))
return false;
const char kDropAttributionCreditColumnSql[] =
"ALTER TABLE conversions DROP COLUMN attribution_credit";
if (!db->Execute(kDropAttributionCreditColumnSql))
return false;
meta_table->SetVersionNumber(5);
return transaction.Commit();
}
bool ConversionStorageSqlMigrations::MigrateToVersion6(
ConversionStorageSql* conversion_storage,
sql::Database* db,
sql::MetaTable* meta_table) {
// Wrap each migration in its own transaction. See comment in
// |MigrateToVersion2|.
sql::Transaction transaction(db);
if (!transaction.Begin())
return false;
// Add new priority column to the impressions table. This follows the steps
// documented at https://sqlite.org/lang_altertable.html#otheralter. Other
// approaches, like using "ALTER ... ADD COLUMN" require setting a DEFAULT
// value for the column which is undesirable.
const char kNewImpressionTableSql[] =
"CREATE TABLE IF NOT EXISTS new_impressions"
"(impression_id INTEGER PRIMARY KEY,"
"impression_data TEXT NOT NULL,"
"impression_origin TEXT NOT NULL,"
"conversion_origin TEXT NOT NULL,"
"reporting_origin TEXT NOT NULL,"
"impression_time INTEGER NOT NULL,"
"expiry_time INTEGER NOT NULL,"
"num_conversions INTEGER DEFAULT 0,"
"active INTEGER DEFAULT 1,"
"conversion_destination TEXT NOT NULL,"
"source_type INTEGER NOT NULL,"
"attributed_truthfully INTEGER NOT NULL,"
"priority INTEGER NOT NULL)";
if (!db->Execute(kNewImpressionTableSql))
return false;
// Transfer the existing rows to the new table, inserting default values for
// the priority column.
const char kPopulateNewImpressionTableSql[] =
"INSERT INTO new_impressions SELECT "
"impression_id,impression_data,impression_origin,"
"conversion_origin,reporting_origin,impression_time,"
"expiry_time,num_conversions,active,conversion_destination,source_type,"
"attributed_truthfully,0 "
"FROM impressions";
sql::Statement populate_statement(
db->GetCachedStatement(SQL_FROM_HERE, kPopulateNewImpressionTableSql));
if (!populate_statement.Run())
return false;
const char kDropOldImpressionTableSql[] = "DROP TABLE impressions";
if (!db->Execute(kDropOldImpressionTableSql))
return false;
const char kRenameImpressionTableSql[] =
"ALTER TABLE new_impressions RENAME TO impressions";
if (!db->Execute(kRenameImpressionTableSql))
return false;
// Create the pre-existing impression table indices on the new table.
const char kImpressionExpiryIndexSql[] =
"CREATE INDEX IF NOT EXISTS impression_expiry_idx "
"ON impressions(expiry_time)";
if (!db->Execute(kImpressionExpiryIndexSql))
return false;
const char kImpressionOriginIndexSql[] =
"CREATE INDEX IF NOT EXISTS impression_origin_idx "
"ON impressions(impression_origin)";
if (!db->Execute(kImpressionOriginIndexSql))
return false;
const char kConversionDestinationIndexSql[] =
"CREATE INDEX IF NOT EXISTS conversion_destination_idx "
"ON impressions(active, conversion_destination, reporting_origin)";
if (!db->Execute(kConversionDestinationIndexSql))
return false;
meta_table->SetVersionNumber(6);
return transaction.Commit();
}
} // namespace content