Revision control

Copy as Markdown

Other Tools

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//! The SQLite database schema.
use std::num::NonZeroU32;
use rusqlite::{config::DbConfig, named_params, OptionalExtension, Transaction};
use super::connection::ConnectionOpener;
/// The schema for a physical SQLite database that contains many
/// named logical databases.
#[derive(Debug)]
pub struct Schema;
fn table_schema(schema: Option<&str>) -> String {
let separator = if schema.is_some() { "." } else { "" };
let schema = schema.unwrap_or("");
let table_name = "telemetry";
format!(
"
CREATE TABLE {schema}{separator}{table_name}(
id TEXT NOT NULL,
ping TEXT NOT NULL,
lifetime TEXT NOT NULL,
labels TEXT NOT NULL, -- can't be null or ON CONFLICT won't work
value BLOB,
UNIQUE(id, ping, labels)
);
"
)
}
impl ConnectionOpener for Schema {
const MAX_SCHEMA_VERSION: u32 = 3;
type Error = SchemaError;
fn setup(conn: &mut rusqlite::Connection) -> Result<(), Self::Error> {
conn.execute_batch(
"
-- we unconditionally want write-ahead-logging mode
PRAGMA journal_mode = WAL;
-- Sync at the most criticial moments, but not with every write
PRAGMA synchronous = NORMAL;
-- limit size of the journal. TODO(bug 2049290): value currently arbitrary.
-- needs refinement.
PRAGMA journal_size_limit = 512000; -- 512 KB.
-- We don't care about temp tables being persisted to disk
PRAGMA temp_store = MEMORY;
-- allows adding incremental cleanup later
PRAGMA auto_vacuum = INCREMENTAL;
-- How long to wait for a lock before returning SQLITE_BUSY (in ms)
PRAGMA busy_timeout = 5000;
",
)?;
// Set hardening flags.
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_DEFENSIVE, true)?;
// Turn off misfeatures: double-quoted strings and untrusted schemas.
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_DQS_DML, false)?;
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_DQS_DDL, false)?;
conn.set_db_config(DbConfig::SQLITE_DBCONFIG_TRUSTED_SCHEMA, true)?;
Ok(())
}
fn create(tx: &mut Transaction<'_>) -> Result<(), Self::Error> {
tx.execute_batch(&format!(
"
{}
CREATE TABLE migration(id INTEGER PRIMARY KEY, state TEXT NOT NULL);
CREATE TABLE submitted_pings(
document_id TEXT PRIMARY KEY,
ping TEXT NOT NULL,
date_submitted INTEGER NOT NULL,
date_uploaded INTEGER,
upload_failed INTEGER,
payload BLOB
);
CREATE INDEX submitted_pings_ping on submitted_pings(ping);
",
table_schema(None)
))?;
Ok(())
}
fn upgrade(tx: &mut Transaction<'_>, to_version: NonZeroU32) -> Result<(), Self::Error> {
match to_version.get() {
2 => {
log::info!("Upgrading user_version to 2");
// Clients upgrading to schema 2 don't have the table.
// But they did run through the migration.
tx.execute_batch(
"CREATE TABLE migration(id INTEGER PRIMARY KEY, state TEXT NOT NULL);",
)?;
let cid_exists: Option<i32> = tx
.query_row(
"SELECT 1 FROM telemetry WHERE id = 'client_id'",
[],
|row| row.get(0),
)
.optional()?;
if cid_exists.is_some() {
log::info!("Client ID already exists. Marking migration as done.");
tx.execute("INSERT INTO migration (id, state) VALUES (1, 'done') ON CONFLICT(id) DO UPDATE SET state = excluded.state", [])?;
}
Ok(())
}
3 => {
log::info!("Upgrading user_version to 3");
// Clients upgrading to schema 3 don't have the table or index
tx.execute_batch(
"
CREATE TABLE submitted_pings(
document_id TEXT PRIMARY KEY,
ping TEXT NOT NULL,
date_submitted INTEGER NOT NULL,
date_uploaded INTEGER,
upload_failed INTEGER,
payload TEXT
);
CREATE INDEX submitted_pings_ping on submitted_pings(ping);
",
)?;
Ok(())
}
to_version => Err(SchemaError::UnsupportedSchemaVersion(to_version)),
}
}
fn validate(tx: &mut Transaction<'_>) -> Result<(), Self::Error> {
// A query selecting every field, it doesn't need to return anything.
tx.execute_batch("SELECT id, ping, lifetime, labels, value FROM telemetry WHERE 1 = 0")?;
Ok(())
}
}
#[derive(thiserror::Error, Debug)]
pub enum SchemaError {
#[error("unsupported schema version: {0}")]
UnsupportedSchemaVersion(u32),
#[error("sqlite: {0}")]
Sqlite(#[from] rusqlite::Error),
}
pub fn create_in_memory_table(
tx: &mut Transaction<'_>,
database: &str,
) -> Result<(), rusqlite::Error> {
tx.execute(
"ATTACH DATABASE ':memory:' AS :database",
named_params! {":database": database},
)?;
// This must remain in sync with the schema for the main table listed above.
// Otherwise bad things will happen.
// TODO(bug 2070883): Ensure this is the same with a test or similar.
tx.execute(&table_schema(Some(database)), [])?;
Ok(())
}