appcore-sync-sqlite 1.0.1-rc.3

Bounded SQLite persistence provider for AppCore synchronization state.
Documentation
// =============================================================================
//        #######
//     ###       ###     F: object_schema.rs
//    ##   ## ##   ##    P: AppCore-Runtime
//         ## ##
//                       C: 2026/10/12 00:00:00 by dnettoRaw
//    ##   ## ##   ##    U: 2026/10/12 00:00:00 by dnettoRaw
//      ###########      S: 1.0.2-rc
// =============================================================================

//! Explicit V3 -> V4 canonical-object schema and bounded startup validation.
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(())
}