use crate::{SqliteSyncError as E, SqliteSyncResult as R};
use rusqlite::{Connection, TransactionBehavior};
pub(crate) fn migrate(connection: &mut Connection) -> R<()> {
let transaction = connection
.transaction_with_behavior(TransactionBehavior::Immediate)
.map_err(E::database)?;
transaction.execute_batch("CREATE TABLE appcore_object_head(singleton INTEGER PRIMARY KEY CHECK(singleton=1),generation TEXT NOT NULL,revision INTEGER NOT NULL CHECK(revision>=0),object_count INTEGER NOT NULL CHECK(object_count>=0),change_count INTEGER NOT NULL CHECK(change_count>=0));
INSERT INTO appcore_object_head VALUES(1,lower(hex(randomblob(16))),0,0,0);
CREATE TABLE appcore_opaque_object(installation_id TEXT NOT NULL,object_key TEXT NOT NULL,revision INTEGER NOT NULL CHECK(revision>0),tombstone INTEGER NOT NULL CHECK(tombstone IN(0,1)),envelope BLOB NOT NULL CHECK(length(envelope)<=1048576),metadata BLOB NOT NULL CHECK(length(metadata)<=4096),envelope_hash TEXT NOT NULL CHECK(length(envelope_hash)=64),PRIMARY KEY(installation_id,object_key)) WITHOUT ROWID;
CREATE TABLE appcore_object_change(revision INTEGER PRIMARY KEY,installation_id TEXT NOT NULL,object_key TEXT NOT NULL,mutation_id TEXT NOT NULL,record_hash TEXT NOT NULL CHECK(length(record_hash)=64),UNIQUE(installation_id,mutation_id),FOREIGN KEY(installation_id,object_key) REFERENCES appcore_opaque_object(installation_id,object_key));
CREATE INDEX appcore_object_change_scope ON appcore_object_change(installation_id,revision,object_key);").map_err(E::database)?;
transaction
.pragma_update(None, "user_version", crate::SQLITE_SYNC_SCHEMA_V4)
.map_err(E::database)?;
transaction.commit().map_err(E::database)
}
pub(crate) fn validate(connection: &Connection) -> R<()> {
let head = crate::objects::head(connection)?;
if head.generation.len() != 32 || !head.generation.bytes().all(|c| c.is_ascii_hexdigit()) {
return Err(E::IntegrityFailed);
}
let (objects, changes): (u64, u64) = connection
.query_row(
"SELECT object_count,change_count FROM appcore_object_head WHERE singleton=1",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.map_err(E::database)?;
if objects > crate::objects::MAX_OBJECTS || changes > crate::objects::MAX_CHANGES {
return Err(E::IntegrityFailed);
}
let actual:(u64,u64,u64)=connection.query_row("SELECT (SELECT count(*) FROM appcore_opaque_object),(SELECT count(*) FROM appcore_object_change),COALESCE((SELECT max(revision) FROM appcore_object_change),0)",[],|row|Ok((row.get(0)?,row.get(1)?,row.get(2)?))).map_err(E::database)?;
if actual != (objects, changes, head.revision) || changes != head.revision {
return Err(E::IntegrityFailed);
}
let mut statement=connection.prepare("SELECT installation_id,object_key,revision FROM appcore_opaque_object ORDER BY installation_id,object_key").map_err(E::database)?;
let mut rows = statement.query([]).map_err(E::database)?;
while let Some(row) = rows.next().map_err(E::database)? {
let installation: String = row.get(0).map_err(E::database)?;
let key: String = row.get(1).map_err(E::database)?;
let revision: u64 = row.get(2).map_err(E::database)?;
crate::objects::validate_id(&installation)?;
crate::objects::validate_id(&key)?;
if revision == 0 || revision > head.revision {
return Err(E::IntegrityFailed);
}
let object = crate::objects::read_object(connection, &installation, &key)?
.ok_or(E::IntegrityFailed)?;
let saved:String=connection.query_row("SELECT record_hash FROM appcore_object_change WHERE revision=?1 AND installation_id=?2 AND object_key=?3",rusqlite::params![revision,installation,key],|row|row.get(0)).map_err(E::database)?;
if saved != crate::objects::fingerprint(&object.object) {
return Err(E::IntegrityFailed);
}
}
let invalid:u64=connection.query_row("SELECT count(*) FROM appcore_object_change WHERE revision<=0 OR revision>?1 OR length(installation_id) NOT BETWEEN 1 AND 128 OR length(object_key) NOT BETWEEN 1 AND 128 OR length(mutation_id) NOT BETWEEN 1 AND 128",[head.revision],|row|row.get(0)).map_err(E::database)?;
if invalid != 0 {
return Err(E::IntegrityFailed);
}
Ok(())
}