blob: e3a95c8e5e003f1bffc47a1af08b9d8928dca024 [file] [log] [blame]
// Copyright 2021, The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use anyhow::{anyhow, Context, Result};
use rusqlite::{params, OptionalExtension, Transaction, NO_PARAMS};
pub fn create_or_get_version(tx: &Transaction, current_version: u32) -> Result<u32> {
tx.execute(
"CREATE TABLE IF NOT EXISTS persistent.version (
id INTEGER PRIMARY KEY,
version INTEGER);",
NO_PARAMS,
)
.context("In create_or_get_version: Failed to create version table.")?;
let version = tx
.query_row("SELECT version FROM persistent.version WHERE id = 0;", NO_PARAMS, |row| {
row.get(0)
})
.optional()
.context("In create_or_get_version: Failed to read version.")?;
let version = if let Some(version) = version {
version
} else {
// If no version table existed it could mean one of two things:
// 1) This database is completely new. In this case the version has to be set
// to the current version and the current version which also needs to be
// returned.
// 2) The database predates db versioning. In this case the version needs to be
// set to 0, and 0 needs to be returned.
let version = if tx
.query_row(
"SELECT name FROM persistent.sqlite_master
WHERE type = 'table' AND name = 'keyentry';",
NO_PARAMS,
|_| Ok(()),
)
.optional()
.context("In create_or_get_version: Failed to check for keyentry table.")?
.is_none()
{
current_version
} else {
0
};
tx.execute("INSERT INTO persistent.version (id, version) VALUES(0, ?);", params![version])
.context("In create_or_get_version: Failed to insert initial version.")?;
version
};
Ok(version)
}
pub fn update_version(tx: &Transaction, new_version: u32) -> Result<()> {
let updated = tx
.execute("UPDATE persistent.version SET version = ? WHERE id = 0;", params![new_version])
.context("In update_version: Failed to update row.")?;
if updated == 1 {
Ok(())
} else {
Err(anyhow!("In update_version: No rows were updated."))
}
}
pub fn upgrade_database<F>(tx: &Transaction, current_version: u32, upgraders: &[F]) -> Result<()>
where
F: Fn(&Transaction) -> Result<u32> + 'static,
{
if upgraders.len() < current_version as usize {
return Err(anyhow!("In upgrade_database: Insufficient upgraders provided."));
}
let mut db_version = create_or_get_version(tx, current_version)
.context("In upgrade_database: Failed to get database version.")?;
while db_version < current_version {
db_version = upgraders[db_version as usize](tx).with_context(|| {
format!("In upgrade_database: Trying to upgrade from db version {}.", db_version)
})?;
}
update_version(tx, db_version).context("In upgrade_database.")
}
#[cfg(test)]
mod test {
use super::*;
use rusqlite::{Connection, TransactionBehavior, NO_PARAMS};
#[test]
fn upgrade_database_test() {
let mut conn = Connection::open_in_memory().unwrap();
conn.execute("ATTACH DATABASE 'file::memory:' as persistent;", NO_PARAMS).unwrap();
let upgraders: Vec<_> = (0..30_u32)
.map(move |i| {
move |tx: &Transaction| {
tx.execute(
"INSERT INTO persistent.test (test_field) VALUES(?);",
params![i + 1],
)
.with_context(|| format!("In upgrade_from_{}_to_{}.", i, i + 1))?;
Ok(i + 1)
}
})
.collect();
for legacy in &[false, true] {
if *legacy {
conn.execute(
"CREATE TABLE IF NOT EXISTS persistent.keyentry (
id INTEGER UNIQUE,
key_type INTEGER,
domain INTEGER,
namespace INTEGER,
alias BLOB,
state INTEGER,
km_uuid BLOB);",
NO_PARAMS,
)
.unwrap();
}
for from in 1..29 {
for to in from..30 {
conn.execute("DROP TABLE IF EXISTS persistent.version;", NO_PARAMS).unwrap();
conn.execute("DROP TABLE IF EXISTS persistent.test;", NO_PARAMS).unwrap();
conn.execute(
"CREATE TABLE IF NOT EXISTS persistent.test (
id INTEGER PRIMARY KEY,
test_field INTEGER);",
NO_PARAMS,
)
.unwrap();
{
let tx =
conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap();
create_or_get_version(&tx, from).unwrap();
tx.commit().unwrap();
}
{
let tx =
conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap();
upgrade_database(&tx, to, &upgraders).unwrap();
tx.commit().unwrap();
}
// In the legacy database case all upgraders starting from 0 have to run. So
// after the upgrade step, the expectations need to be adjusted.
let from = if *legacy { 0 } else { from };
// There must be exactly to - from rows.
assert_eq!(
to - from,
conn.query_row(
"SELECT COUNT(test_field) FROM persistent.test;",
NO_PARAMS,
|row| row.get(0)
)
.unwrap()
);
// Each row must have the correct relation between id and test_field. If this
// is not the case, the upgraders were not executed in the correct order.
assert_eq!(
to - from,
conn.query_row(
"SELECT COUNT(test_field) FROM persistent.test
WHERE id = test_field - ?;",
params![from],
|row| row.get(0)
)
.unwrap()
);
}
}
}
}
#[test]
fn create_or_get_version_new_database() {
let mut conn = Connection::open_in_memory().unwrap();
conn.execute("ATTACH DATABASE 'file::memory:' as persistent;", NO_PARAMS).unwrap();
{
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap();
let version = create_or_get_version(&tx, 3).unwrap();
tx.commit().unwrap();
assert_eq!(version, 3);
}
// Was the version table created as expected?
assert_eq!(
Ok("version".to_owned()),
conn.query_row(
"SELECT name FROM persistent.sqlite_master
WHERE type = 'table' AND name = 'version';",
NO_PARAMS,
|row| row.get(0),
)
);
// There is exactly one row in the version table.
assert_eq!(
Ok(1),
conn.query_row("SELECT COUNT(id) from persistent.version;", NO_PARAMS, |row| row
.get(0))
);
// The version must be set to 3
assert_eq!(
Ok(3),
conn.query_row(
"SELECT version from persistent.version WHERE id = 0;",
NO_PARAMS,
|row| row.get(0)
)
);
// Will subsequent calls to create_or_get_version still return the same version even
// if the current version changes.
{
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap();
let version = create_or_get_version(&tx, 5).unwrap();
tx.commit().unwrap();
assert_eq!(version, 3);
}
// There is still exactly one row in the version table.
assert_eq!(
Ok(1),
conn.query_row("SELECT COUNT(id) from persistent.version;", NO_PARAMS, |row| row
.get(0))
);
// Bump the version.
{
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap();
update_version(&tx, 5).unwrap();
tx.commit().unwrap();
}
// Now the version should have changed.
{
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap();
let version = create_or_get_version(&tx, 7).unwrap();
tx.commit().unwrap();
assert_eq!(version, 5);
}
// There is still exactly one row in the version table.
assert_eq!(
Ok(1),
conn.query_row("SELECT COUNT(id) from persistent.version;", NO_PARAMS, |row| row
.get(0))
);
// The version must be set to 5
assert_eq!(
Ok(5),
conn.query_row(
"SELECT version from persistent.version WHERE id = 0;",
NO_PARAMS,
|row| row.get(0)
)
);
}
#[test]
fn create_or_get_version_legacy_database() {
let mut conn = Connection::open_in_memory().unwrap();
conn.execute("ATTACH DATABASE 'file::memory:' as persistent;", NO_PARAMS).unwrap();
// A legacy (version 0) database is detected if the keyentry table exists but no
// version table.
conn.execute(
"CREATE TABLE IF NOT EXISTS persistent.keyentry (
id INTEGER UNIQUE,
key_type INTEGER,
domain INTEGER,
namespace INTEGER,
alias BLOB,
state INTEGER,
km_uuid BLOB);",
NO_PARAMS,
)
.unwrap();
{
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap();
let version = create_or_get_version(&tx, 3).unwrap();
tx.commit().unwrap();
// In the legacy case, version 0 must be returned.
assert_eq!(version, 0);
}
// Was the version table created as expected?
assert_eq!(
Ok("version".to_owned()),
conn.query_row(
"SELECT name FROM persistent.sqlite_master
WHERE type = 'table' AND name = 'version';",
NO_PARAMS,
|row| row.get(0),
)
);
// There is exactly one row in the version table.
assert_eq!(
Ok(1),
conn.query_row("SELECT COUNT(id) from persistent.version;", NO_PARAMS, |row| row
.get(0))
);
// The version must be set to 0
assert_eq!(
Ok(0),
conn.query_row(
"SELECT version from persistent.version WHERE id = 0;",
NO_PARAMS,
|row| row.get(0)
)
);
// Will subsequent calls to create_or_get_version still return the same version even
// if the current version changes.
{
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap();
let version = create_or_get_version(&tx, 5).unwrap();
tx.commit().unwrap();
assert_eq!(version, 0);
}
// There is still exactly one row in the version table.
assert_eq!(
Ok(1),
conn.query_row("SELECT COUNT(id) from persistent.version;", NO_PARAMS, |row| row
.get(0))
);
// Bump the version.
{
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap();
update_version(&tx, 5).unwrap();
tx.commit().unwrap();
}
// Now the version should have changed.
{
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate).unwrap();
let version = create_or_get_version(&tx, 7).unwrap();
tx.commit().unwrap();
assert_eq!(version, 5);
}
// There is still exactly one row in the version table.
assert_eq!(
Ok(1),
conn.query_row("SELECT COUNT(id) from persistent.version;", NO_PARAMS, |row| row
.get(0))
);
// The version must be set to 5
assert_eq!(
Ok(5),
conn.query_row(
"SELECT version from persistent.version WHERE id = 0;",
NO_PARAMS,
|row| row.get(0)
)
);
}
}