app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

contract.rs (43680B)


      1 //! Sealed HarvestCircle service-state identity and schema contract.
      2 
      3 use core::{fmt, num::NonZeroU32};
      4 use std::error::Error;
      5 
      6 use radroots_runtime_paths::RuntimeContext;
      7 use radroots_service_sqlite::{
      8     MigrationCatalog, MigrationChecksum, MigrationDescriptor, SchemaCatalog,
      9     SchemaCatalogContractError, SchemaDigest, SchemaObject, SchemaObjectKind, SchemaVersionCatalog,
     10     ServiceSqliteApplicationId, ServiceSqlitePaths,
     11 };
     12 
     13 pub const HARVESTCIRCLE_SERVICE_ID: &str = "harvestcircle";
     14 pub const HARVESTCIRCLE_INSTANCE_ID: &str = "desktop";
     15 pub const HARVESTCIRCLE_APPLICATION_ID: u32 = 0x4843_5231;
     16 pub(crate) const HARVESTCIRCLE_INITIAL_STATE_SCHEMA_VERSION: u32 = 1;
     17 pub const HARVESTCIRCLE_STATE_SCHEMA_VERSION: u32 = 3;
     18 pub const HARVESTCIRCLE_IDENTITY_CAPACITY: usize = 256;
     19 pub const HARVESTCIRCLE_UNFINISHED_DURABLE_OPERATION_CAPACITY: usize = 1_024;
     20 pub const HARVESTCIRCLE_DURABLE_OPERATION_CAPACITY: usize = 4_096;
     21 pub const HARVESTCIRCLE_DURABLE_OPERATION_CLEANUP_BATCH: usize = 256;
     22 pub const HARVESTCIRCLE_TERMINAL_RECEIPT_RETENTION_SECONDS: i64 = 7 * 24 * 60 * 60;
     23 pub const HARVESTCIRCLE_PREFERENCE_VALUE_UTF8_BYTES: usize = 4_096;
     24 pub const HARVESTCIRCLE_RELAY_ENDPOINT_CAPACITY: usize = 16;
     25 pub const HARVESTCIRCLE_RELAY_URL_UTF8_BYTES: usize = 2_048;
     26 pub const HARVESTCIRCLE_EVENTS_PER_RELAY_CAPACITY: usize = 64;
     27 pub const HARVESTCIRCLE_EVENTS_TOTAL_CAPACITY: usize = 1_024;
     28 pub const HARVESTCIRCLE_OBSERVER_CAPACITY: usize = 32;
     29 pub const HARVESTCIRCLE_ACTOR_MAILBOX_CAPACITY: usize = 64;
     30 pub const HARVESTCIRCLE_COMMAND_DEADLINE_MIN_MS: u64 = 1;
     31 pub const HARVESTCIRCLE_COMMAND_DEADLINE_MAX_MS: u64 = 30_000;
     32 
     33 pub(crate) const CREATE_ACCOUNT_IDENTITIES_SQL: &str = r#"CREATE TABLE account_identities (
     34     public_key BLOB NOT NULL PRIMARY KEY CHECK (length(public_key) = 32),
     35     npub TEXT NOT NULL UNIQUE CHECK (length(CAST(npub AS BLOB)) = 63),
     36     label TEXT CHECK (label IS NULL OR length(CAST(label AS BLOB)) BETWEEN 1 AND 80),
     37     created_at_unix_s INTEGER NOT NULL CHECK (created_at_unix_s >= 0),
     38     last_used_at_unix_s INTEGER CHECK (last_used_at_unix_s IS NULL OR last_used_at_unix_s >= 0)
     39 ) STRICT"#;
     40 
     41 pub(crate) const CREATE_LOCAL_SIGNER_BINDINGS_SQL: &str = r#"CREATE TABLE local_signer_bindings (
     42     account_public_key BLOB NOT NULL,
     43     binding_public_key BLOB NOT NULL,
     44     binding_kind TEXT NOT NULL CHECK (binding_kind = 'local_secret'),
     45     availability TEXT NOT NULL CHECK (
     46         availability IN ('available', 'credential_missing', 'store_unavailable')
     47     ),
     48     PRIMARY KEY (account_public_key, binding_public_key),
     49     UNIQUE (account_public_key, binding_kind),
     50     FOREIGN KEY (account_public_key) REFERENCES account_identities(public_key) ON DELETE CASCADE,
     51     CHECK (length(account_public_key) = 32),
     52     CHECK (length(binding_public_key) = 32),
     53     CHECK (account_public_key = binding_public_key)
     54 ) STRICT"#;
     55 
     56 pub(crate) const CREATE_RUNTIME_STATE_SQL: &str = r#"CREATE TABLE runtime_state (
     57     singleton INTEGER NOT NULL PRIMARY KEY CHECK (singleton = 1),
     58     selected_public_key BLOB REFERENCES account_identities(public_key) ON DELETE SET NULL,
     59     active_account_public_key BLOB,
     60     active_binding_public_key BLOB,
     61     session_generation INTEGER NOT NULL DEFAULT 0 CHECK (session_generation >= 0),
     62     FOREIGN KEY (active_account_public_key, active_binding_public_key)
     63         REFERENCES local_signer_bindings(account_public_key, binding_public_key)
     64         ON DELETE SET NULL,
     65     CHECK (selected_public_key IS NULL OR length(selected_public_key) = 32),
     66     CHECK (
     67         (active_account_public_key IS NULL AND active_binding_public_key IS NULL)
     68         OR
     69         (length(active_account_public_key) = 32 AND length(active_binding_public_key) = 32)
     70     )
     71 ) STRICT"#;
     72 
     73 pub(crate) const CREATE_PROFILE_CACHE_SQL: &str = r#"CREATE TABLE profile_cache (
     74     subject_public_key BLOB NOT NULL PRIMARY KEY
     75         REFERENCES account_identities(public_key) ON DELETE CASCADE,
     76     event_id BLOB NOT NULL CHECK (length(event_id) = 32),
     77     event_created_at_unix_s INTEGER NOT NULL CHECK (event_created_at_unix_s >= 0),
     78     name TEXT CHECK (name IS NULL OR length(CAST(name AS BLOB)) BETWEEN 1 AND 128),
     79     display_name TEXT CHECK (display_name IS NULL OR length(CAST(display_name AS BLOB)) BETWEEN 1 AND 128),
     80     nip05 TEXT CHECK (nip05 IS NULL OR length(CAST(nip05 AS BLOB)) BETWEEN 1 AND 320),
     81     about TEXT CHECK (about IS NULL OR length(CAST(about AS BLOB)) BETWEEN 1 AND 4096),
     82     picture TEXT CHECK (picture IS NULL OR length(CAST(picture AS BLOB)) BETWEEN 1 AND 2048),
     83     refreshed_at_unix_s INTEGER NOT NULL CHECK (refreshed_at_unix_s >= 0),
     84     refresh_status TEXT NOT NULL CHECK (refresh_status IN ('success', 'offline', 'invalid_data'))
     85 ) STRICT"#;
     86 
     87 pub(crate) const CREATE_ACCOUNT_PREFERENCES_SQL: &str = r#"CREATE TABLE account_preferences (
     88     owner_public_key BLOB NOT NULL REFERENCES account_identities(public_key) ON DELETE CASCADE,
     89     preference_key TEXT NOT NULL CHECK (preference_key = 'namespace_probe'),
     90     preference_value TEXT NOT NULL CHECK (
     91         length(CAST(preference_value AS BLOB)) BETWEEN 1 AND 4096
     92     ),
     93     PRIMARY KEY (owner_public_key, preference_key),
     94     CHECK (length(owner_public_key) = 32)
     95 ) STRICT"#;
     96 
     97 pub(crate) const CREATE_DURABLE_OPERATIONS_SQL: &str = r#"CREATE TABLE durable_operations (
     98     request_id TEXT NOT NULL PRIMARY KEY CHECK (
     99         length(CAST(request_id AS BLOB)) = 36
    100         AND request_id = lower(request_id)
    101         AND request_id NOT GLOB '*[^0-9a-f-]*'
    102         AND substr(request_id, 9, 1) = '-'
    103         AND substr(request_id, 14, 1) = '-'
    104         AND substr(request_id, 15, 1) = '7'
    105         AND substr(request_id, 19, 1) = '-'
    106         AND substr(request_id, 20, 1) IN ('8', '9', 'a', 'b')
    107         AND substr(request_id, 24, 1) = '-'
    108     ),
    109     operation_kind TEXT NOT NULL CHECK (operation_kind IN ('create', 'import', 'repair', 'remove')),
    110     account_public_key BLOB NOT NULL CHECK (length(account_public_key) = 32),
    111     binding_public_key BLOB NOT NULL CHECK (length(binding_public_key) = 32),
    112     expected_revision INTEGER CHECK (expected_revision IS NULL OR expected_revision >= 0),
    113     phase TEXT NOT NULL CHECK (phase IN (
    114         'intent_recorded', 'credential_written', 'metadata_committed', 'selection_committed',
    115         'compensation_pending', 'credential_deleted', 'metadata_deleted', 'finalized'
    116     )),
    117     terminal_outcome TEXT CHECK (
    118         terminal_outcome IS NULL OR terminal_outcome IN ('completed', 'cancelled', 'failed')
    119     ),
    120     prior_selected_public_key BLOB CHECK (
    121         prior_selected_public_key IS NULL OR length(prior_selected_public_key) = 32
    122     ),
    123     prior_binding_availability TEXT CHECK (
    124         prior_binding_availability IS NULL OR prior_binding_availability IN (
    125             'available', 'credential_missing', 'store_unavailable'
    126         )
    127     ),
    128     resulting_revision INTEGER CHECK (resulting_revision IS NULL OR resulting_revision >= 0),
    129     updated_at_unix_s INTEGER NOT NULL CHECK (updated_at_unix_s >= 0),
    130     diagnostic_code TEXT CHECK (diagnostic_code IS NULL OR diagnostic_code IN (
    131         'storage_unavailable', 'keyring_unavailable', 'credential_missing',
    132         'compensation_failed', 'conflict', 'expired'
    133     )),
    134     CHECK (account_public_key = binding_public_key),
    135     CHECK (
    136         (phase = 'finalized' AND terminal_outcome IS NOT NULL)
    137         OR
    138         (phase <> 'finalized' AND terminal_outcome IS NULL AND resulting_revision IS NULL)
    139     )
    140 ) STRICT"#;
    141 
    142 const CREATE_DURABLE_OPERATIONS_V2_SQL: &str = r#"CREATE TABLE durable_operations (
    143     request_id TEXT NOT NULL PRIMARY KEY CHECK (
    144         length(CAST(request_id AS BLOB)) = 36
    145         AND request_id = lower(request_id)
    146         AND request_id NOT GLOB '*[^0-9a-f-]*'
    147         AND substr(request_id, 9, 1) = '-'
    148         AND substr(request_id, 14, 1) = '-'
    149         AND substr(request_id, 15, 1) = '7'
    150         AND substr(request_id, 19, 1) = '-'
    151         AND substr(request_id, 20, 1) IN ('8', '9', 'a', 'b')
    152         AND substr(request_id, 24, 1) = '-'
    153     ),
    154     operation_kind TEXT NOT NULL CHECK (operation_kind IN ('create', 'import', 'repair', 'remove')),
    155     account_public_key BLOB NOT NULL CHECK (length(account_public_key) = 32),
    156     binding_public_key BLOB NOT NULL CHECK (length(binding_public_key) = 32),
    157     expected_revision INTEGER CHECK (expected_revision IS NULL OR expected_revision >= 0),
    158     phase TEXT NOT NULL CHECK (phase IN (
    159         'intent_recorded', 'credential_written', 'metadata_committed', 'selection_committed',
    160         'compensation_pending', 'credential_deleted', 'metadata_deleted', 'finalized'
    161     )),
    162     terminal_outcome TEXT CHECK (
    163         terminal_outcome IS NULL OR terminal_outcome IN ('completed', 'cancelled', 'failed')
    164     ),
    165     prior_selected_public_key BLOB CHECK (
    166         prior_selected_public_key IS NULL OR length(prior_selected_public_key) = 32
    167     ),
    168     prior_binding_availability TEXT CHECK (
    169         prior_binding_availability IS NULL OR prior_binding_availability IN (
    170             'available', 'credential_missing', 'store_unavailable'
    171         )
    172     ),
    173     resulting_revision INTEGER CHECK (resulting_revision IS NULL OR resulting_revision >= 0),
    174     updated_at_unix_s INTEGER NOT NULL CHECK (updated_at_unix_s >= 0),
    175     diagnostic_code TEXT CHECK (diagnostic_code IS NULL OR diagnostic_code IN (
    176         'storage_unavailable', 'keyring_unavailable', 'credential_missing',
    177         'compensation_failed', 'conflict', 'expired'
    178     )), completed_at_unix_s INTEGER CHECK (
    179     completed_at_unix_s IS NULL OR completed_at_unix_s >= 0
    180 ),
    181     CHECK (account_public_key = binding_public_key),
    182     CHECK (
    183         (phase = 'finalized' AND terminal_outcome IS NOT NULL)
    184         OR
    185         (phase <> 'finalized' AND terminal_outcome IS NULL AND resulting_revision IS NULL)
    186     )
    187 ) STRICT"#;
    188 
    189 const CREATE_DURABLE_OPERATIONS_RECEIPT_INSERT_GUARD_SQL: &str = r#"CREATE TRIGGER durable_operations_receipt_insert_guard
    190 BEFORE INSERT ON durable_operations
    191 WHEN NOT (
    192     (
    193         NEW.phase = 'finalized'
    194         AND NEW.terminal_outcome IS NOT NULL
    195         AND NEW.completed_at_unix_s IS NOT NULL
    196     )
    197     OR
    198     (
    199         NEW.phase <> 'finalized'
    200         AND NEW.terminal_outcome IS NULL
    201         AND NEW.resulting_revision IS NULL
    202         AND NEW.completed_at_unix_s IS NULL
    203     )
    204 )
    205 BEGIN
    206     SELECT RAISE(ABORT, 'durable operation receipt invariant');
    207 END"#;
    208 
    209 const CREATE_DURABLE_OPERATIONS_RECEIPT_UPDATE_GUARD_SQL: &str = r#"CREATE TRIGGER durable_operations_receipt_update_guard
    210 BEFORE UPDATE ON durable_operations
    211 WHEN NOT (
    212     (
    213         NEW.phase = 'finalized'
    214         AND NEW.terminal_outcome IS NOT NULL
    215         AND NEW.completed_at_unix_s IS NOT NULL
    216     )
    217     OR
    218     (
    219         NEW.phase <> 'finalized'
    220         AND NEW.terminal_outcome IS NULL
    221         AND NEW.resulting_revision IS NULL
    222         AND NEW.completed_at_unix_s IS NULL
    223     )
    224 )
    225 BEGIN
    226     SELECT RAISE(ABORT, 'durable operation receipt invariant');
    227 END"#;
    228 
    229 pub(crate) const MIGRATE_DURABLE_OPERATIONS_V2_SQL: &str = r#"ALTER TABLE durable_operations
    230 ADD COLUMN completed_at_unix_s INTEGER CHECK (
    231     completed_at_unix_s IS NULL OR completed_at_unix_s >= 0
    232 );
    233 UPDATE durable_operations
    234 SET completed_at_unix_s = updated_at_unix_s
    235 WHERE phase = 'finalized';
    236 CREATE TRIGGER durable_operations_receipt_insert_guard
    237 BEFORE INSERT ON durable_operations
    238 WHEN NOT (
    239     (
    240         NEW.phase = 'finalized'
    241         AND NEW.terminal_outcome IS NOT NULL
    242         AND NEW.completed_at_unix_s IS NOT NULL
    243     )
    244     OR
    245     (
    246         NEW.phase <> 'finalized'
    247         AND NEW.terminal_outcome IS NULL
    248         AND NEW.resulting_revision IS NULL
    249         AND NEW.completed_at_unix_s IS NULL
    250     )
    251 )
    252 BEGIN
    253     SELECT RAISE(ABORT, 'durable operation receipt invariant');
    254 END;
    255 CREATE TRIGGER durable_operations_receipt_update_guard
    256 BEFORE UPDATE ON durable_operations
    257 WHEN NOT (
    258     (
    259         NEW.phase = 'finalized'
    260         AND NEW.terminal_outcome IS NOT NULL
    261         AND NEW.completed_at_unix_s IS NOT NULL
    262     )
    263     OR
    264     (
    265         NEW.phase <> 'finalized'
    266         AND NEW.terminal_outcome IS NULL
    267         AND NEW.resulting_revision IS NULL
    268         AND NEW.completed_at_unix_s IS NULL
    269     )
    270 )
    271 BEGIN
    272     SELECT RAISE(ABORT, 'durable operation receipt invariant');
    273 END;
    274 UPDATE durable_operations SET request_id = request_id"#;
    275 
    276 pub(crate) const CREATE_INSTALLATION_IDENTITY_SQL: &str = r#"CREATE TABLE installation_identity (
    277     singleton INTEGER NOT NULL PRIMARY KEY CHECK (singleton = 1),
    278     installation_id BLOB NOT NULL CHECK (length(installation_id) = 16)
    279 ) STRICT"#;
    280 
    281 pub(crate) const MIGRATE_AVAILABILITY_EVIDENCE_V3_SQL: &str = r#"UPDATE account_preferences SET preference_value = preference_value;
    282 CREATE TABLE availability_versions (
    283     event_id BLOB NOT NULL PRIMARY KEY CHECK (length(event_id) = 32),
    284     author BLOB NOT NULL CHECK (length(author) = 32),
    285     kind INTEGER NOT NULL CHECK (kind = 30402),
    286     raw_d TEXT NOT NULL CHECK (length(CAST(raw_d AS BLOB)) BETWEEN 0 AND 4096),
    287     signed_at BLOB NOT NULL CHECK (length(signed_at) = 8),
    288     published_at BLOB CHECK (published_at IS NULL OR length(published_at) = 8),
    289     original_json TEXT NOT NULL CHECK (
    290         length(CAST(original_json AS BLOB)) BETWEEN 1 AND 262144
    291     ),
    292     admission_label TEXT NOT NULL CHECK (admission_label IN (
    293         'focused', 'excluded_focused', 'excluded_operational', 'excluded_generic',
    294         'excluded_ambiguous', 'projection_rejected'
    295     )),
    296     rejection_code TEXT CHECK (
    297         rejection_code IS NULL OR (
    298             length(CAST(rejection_code AS BLOB)) BETWEEN 1 AND 64
    299             AND rejection_code NOT GLOB '*[^ -~]*'
    300         )
    301     ),
    302     source TEXT NOT NULL CHECK (length(CAST(source AS BLOB)) BETWEEN 1 AND 2048),
    303     observed_at_unix_s INTEGER NOT NULL CHECK (observed_at_unix_s >= 0),
    304     payload_bytes INTEGER NOT NULL CHECK (
    305         payload_bytes = 92 + length(CAST(original_json AS BLOB))
    306             + length(CAST(raw_d AS BLOB)) + length(CAST(source AS BLOB))
    307             + length(CAST(admission_label AS BLOB))
    308             + coalesce(length(CAST(rejection_code AS BLOB)), 0)
    309     ),
    310     CHECK (
    311         (admission_label = 'focused' AND rejection_code IS NULL AND published_at IS NOT NULL)
    312         OR
    313         (admission_label IN (
    314             'excluded_focused', 'excluded_operational', 'excluded_generic', 'excluded_ambiguous'
    315         ) AND rejection_code IS NULL AND published_at IS NULL)
    316         OR
    317         (admission_label = 'projection_rejected' AND rejection_code IS NOT NULL AND published_at IS NULL)
    318     )
    319 ) STRICT;
    320 CREATE TABLE public_payload_usage (
    321     singleton INTEGER NOT NULL PRIMARY KEY CHECK (singleton = 1),
    322     version_count INTEGER NOT NULL CHECK (version_count BETWEEN 0 AND 4096),
    323     payload_bytes INTEGER NOT NULL CHECK (payload_bytes BETWEEN 0 AND 134217728)
    324 ) STRICT;
    325 INSERT INTO public_payload_usage (singleton, version_count, payload_bytes) VALUES (1, 0, 0)"#;
    326 
    327 pub(crate) const CREATE_INSTALLATION_IDENTITY_NO_UPDATE_SQL: &str = r#"CREATE TRIGGER installation_identity_no_update
    328 BEFORE UPDATE ON installation_identity
    329 BEGIN
    330     SELECT RAISE(ABORT, 'installation identity is immutable');
    331 END"#;
    332 
    333 pub(crate) const CREATE_INSTALLATION_IDENTITY_NO_DELETE_SQL: &str = r#"CREATE TRIGGER installation_identity_no_delete
    334 BEFORE DELETE ON installation_identity
    335 BEGIN
    336     SELECT RAISE(ABORT, 'installation identity is immutable');
    337 END"#;
    338 
    339 const INITIAL_SCHEMA_SQL: [&str; 9] = [
    340     CREATE_ACCOUNT_IDENTITIES_SQL,
    341     CREATE_LOCAL_SIGNER_BINDINGS_SQL,
    342     CREATE_RUNTIME_STATE_SQL,
    343     CREATE_PROFILE_CACHE_SQL,
    344     CREATE_ACCOUNT_PREFERENCES_SQL,
    345     CREATE_DURABLE_OPERATIONS_SQL,
    346     CREATE_INSTALLATION_IDENTITY_SQL,
    347     CREATE_INSTALLATION_IDENTITY_NO_UPDATE_SQL,
    348     CREATE_INSTALLATION_IDENTITY_NO_DELETE_SQL,
    349 ];
    350 
    351 const OBJECT_DIGESTS: [[u8; 32]; 9] = [
    352     [
    353         203, 193, 254, 189, 121, 130, 165, 156, 1, 155, 21, 35, 130, 72, 131, 44, 34, 217, 168,
    354         187, 96, 155, 40, 226, 113, 78, 22, 255, 8, 65, 23, 38,
    355     ],
    356     [
    357         204, 249, 174, 98, 165, 65, 184, 31, 254, 31, 101, 17, 139, 176, 170, 131, 88, 225, 158, 6,
    358         174, 77, 192, 131, 84, 153, 142, 200, 213, 235, 141, 82,
    359     ],
    360     [
    361         9, 60, 207, 79, 94, 50, 51, 84, 228, 163, 119, 152, 227, 137, 166, 31, 166, 235, 70, 79,
    362         228, 160, 229, 246, 4, 87, 11, 172, 36, 151, 102, 234,
    363     ],
    364     [
    365         17, 90, 168, 107, 5, 179, 134, 52, 25, 51, 228, 255, 236, 36, 157, 152, 26, 66, 108, 147,
    366         239, 116, 3, 99, 82, 220, 182, 236, 209, 136, 126, 97,
    367     ],
    368     [
    369         180, 232, 35, 143, 72, 174, 82, 223, 52, 122, 142, 211, 5, 167, 155, 75, 69, 223, 34, 117,
    370         131, 5, 132, 107, 175, 198, 215, 16, 71, 114, 4, 127,
    371     ],
    372     [
    373         135, 198, 48, 230, 122, 86, 86, 153, 66, 95, 22, 123, 24, 164, 49, 229, 246, 218, 210, 233,
    374         61, 182, 81, 194, 251, 121, 165, 203, 8, 29, 63, 27,
    375     ],
    376     [
    377         132, 111, 227, 84, 42, 121, 244, 99, 22, 255, 131, 104, 48, 33, 7, 146, 174, 120, 176, 103,
    378         37, 23, 171, 90, 90, 215, 142, 212, 32, 9, 250, 188,
    379     ],
    380     [
    381         142, 248, 11, 168, 116, 173, 238, 101, 167, 191, 95, 63, 180, 126, 229, 156, 164, 217, 108,
    382         71, 221, 145, 70, 169, 91, 117, 33, 93, 34, 250, 120, 151,
    383     ],
    384     [
    385         127, 153, 155, 84, 191, 170, 38, 18, 239, 225, 90, 123, 208, 172, 218, 99, 2, 212, 181,
    386         212, 194, 19, 99, 242, 225, 249, 202, 134, 204, 219, 200, 23,
    387     ],
    388 ];
    389 const VERSION_ONE_DIGEST: [u8; 32] = [
    390     61, 122, 56, 39, 178, 126, 179, 157, 145, 167, 19, 2, 172, 134, 213, 107, 151, 196, 212, 57,
    391     17, 112, 163, 67, 240, 140, 61, 62, 5, 101, 14, 71,
    392 ];
    393 const DURABLE_OPERATIONS_V2_DIGEST: [u8; 32] = [
    394     162, 24, 8, 218, 50, 32, 224, 252, 60, 229, 209, 160, 204, 46, 226, 37, 88, 102, 22, 2, 156,
    395     177, 28, 19, 235, 161, 26, 125, 65, 126, 59, 135,
    396 ];
    397 const DURABLE_OPERATIONS_RECEIPT_INSERT_GUARD_DIGEST: [u8; 32] = [
    398     130, 178, 229, 235, 158, 182, 51, 151, 133, 196, 115, 85, 8, 166, 169, 136, 62, 26, 211, 92,
    399     215, 57, 254, 158, 103, 196, 53, 39, 106, 201, 183, 14,
    400 ];
    401 const DURABLE_OPERATIONS_RECEIPT_UPDATE_GUARD_DIGEST: [u8; 32] = [
    402     48, 131, 220, 252, 243, 158, 221, 88, 8, 140, 207, 187, 34, 138, 145, 215, 92, 99, 32, 72, 254,
    403     25, 96, 241, 33, 172, 150, 186, 98, 114, 27, 142,
    404 ];
    405 const VERSION_TWO_DIGEST: [u8; 32] = [
    406     78, 151, 73, 238, 2, 15, 71, 52, 11, 111, 100, 95, 135, 11, 170, 138, 84, 105, 106, 177, 27, 7,
    407     134, 156, 53, 68, 22, 220, 116, 199, 147, 5,
    408 ];
    409 const DURABLE_OPERATIONS_V2_MIGRATION_CHECKSUM: MigrationChecksum =
    410     MigrationChecksum::from_bytes([
    411         107, 95, 237, 250, 255, 0, 44, 110, 142, 194, 92, 163, 84, 27, 96, 31, 210, 37, 151, 186,
    412         210, 83, 137, 114, 251, 20, 30, 31, 11, 136, 207, 168,
    413     ]);
    414 
    415 const AVAILABILITY_VERSIONS_DIGEST: [u8; 32] = [
    416     250, 239, 225, 0, 53, 126, 33, 150, 232, 105, 102, 35, 26, 221, 78, 156, 137, 219, 66, 240, 10,
    417     18, 96, 99, 102, 199, 219, 253, 8, 45, 171, 52,
    418 ];
    419 const PUBLIC_PAYLOAD_USAGE_DIGEST: [u8; 32] = [
    420     36, 83, 232, 46, 105, 160, 105, 237, 4, 189, 147, 119, 164, 56, 77, 154, 100, 122, 111, 62,
    421     246, 160, 0, 206, 10, 136, 76, 189, 81, 68, 10, 26,
    422 ];
    423 const VERSION_THREE_DIGEST: [u8; 32] = [
    424     53, 128, 250, 106, 143, 97, 51, 33, 195, 227, 254, 35, 19, 135, 136, 171, 229, 241, 204, 234,
    425     158, 180, 204, 19, 113, 95, 73, 250, 135, 241, 51, 88,
    426 ];
    427 const AVAILABILITY_EVIDENCE_V3_MIGRATION_CHECKSUM: MigrationChecksum =
    428     MigrationChecksum::from_bytes([
    429         254, 36, 47, 105, 27, 136, 68, 240, 103, 108, 27, 205, 97, 203, 221, 84, 234, 64, 5, 123,
    430         229, 227, 10, 199, 30, 127, 170, 170, 11, 2, 78, 81,
    431     ]);
    432 
    433 /// A sealed binding between one HarvestCircle runtime context and the governed state catalogs.
    434 ///
    435 /// External callers cannot forge alternate paths or catalogs:
    436 ///
    437 /// ```compile_fail
    438 /// use harvestcircle_storage::HarvestCircleStorageContract;
    439 ///
    440 /// let _ = HarvestCircleStorageContract {
    441 ///     paths: todo!(),
    442 ///     migrations: todo!(),
    443 ///     schema: todo!(),
    444 /// };
    445 /// ```
    446 #[derive(Clone, PartialEq, Eq)]
    447 pub struct HarvestCircleStorageContract {
    448     paths: ServiceSqlitePaths,
    449     migrations: MigrationCatalog,
    450     schema: SchemaCatalog,
    451 }
    452 
    453 impl HarvestCircleStorageContract {
    454     /// Binds the exact HarvestCircle service and desktop instance to canonical paths.
    455     pub fn from_runtime_context(
    456         context: &RuntimeContext,
    457     ) -> Result<Self, HarvestCircleStorageContractError> {
    458         if context.service().as_str() != HARVESTCIRCLE_SERVICE_ID
    459             || context.instance().as_str() != HARVESTCIRCLE_INSTANCE_ID
    460         {
    461             return Err(HarvestCircleStorageContractError::ContextIdentity);
    462         }
    463         let paths = ServiceSqlitePaths::from_runtime_context(context)
    464             .map_err(|_| HarvestCircleStorageContractError::CanonicalPaths)?;
    465         let migrations = harvestcircle_migration_catalog()?;
    466         let schema = schema_catalog_for(&migrations)?;
    467         Ok(Self {
    468             paths,
    469             migrations,
    470             schema,
    471         })
    472     }
    473 
    474     #[must_use]
    475     pub const fn paths(&self) -> &ServiceSqlitePaths {
    476         &self.paths
    477     }
    478 
    479     #[must_use]
    480     pub const fn migrations(&self) -> &MigrationCatalog {
    481         &self.migrations
    482     }
    483 
    484     #[must_use]
    485     pub const fn schema(&self) -> &SchemaCatalog {
    486         &self.schema
    487     }
    488 
    489     #[must_use]
    490     pub fn application_id(&self) -> ServiceSqliteApplicationId {
    491         ServiceSqliteApplicationId::new(HARVESTCIRCLE_APPLICATION_ID)
    492             .expect("HCR1 is a valid SQLite application ID")
    493     }
    494 
    495     #[must_use]
    496     pub const fn state_schema_version(&self) -> NonZeroU32 {
    497         NonZeroU32::new(HARVESTCIRCLE_STATE_SCHEMA_VERSION)
    498             .expect("HarvestCircle current schema version is nonzero")
    499     }
    500 }
    501 
    502 impl fmt::Debug for HarvestCircleStorageContract {
    503     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    504         formatter
    505             .debug_struct("HarvestCircleStorageContract")
    506             .field("service", &HARVESTCIRCLE_SERVICE_ID)
    507             .field("instance", &HARVESTCIRCLE_INSTANCE_ID)
    508             .field("paths", &"[redacted]")
    509             .field("schema_version", &HARVESTCIRCLE_STATE_SCHEMA_VERSION)
    510             .finish()
    511     }
    512 }
    513 
    514 /// Stable, path-free storage-contract construction failure.
    515 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    516 pub enum HarvestCircleStorageContractError {
    517     ContextIdentity,
    518     CanonicalPaths,
    519     MigrationCatalog,
    520     SchemaCatalog,
    521 }
    522 
    523 impl fmt::Display for HarvestCircleStorageContractError {
    524     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    525         formatter.write_str(match self {
    526             Self::ContextIdentity => "HarvestCircle storage context identity is invalid",
    527             Self::CanonicalPaths => "HarvestCircle storage paths are invalid",
    528             Self::MigrationCatalog => "HarvestCircle migration catalog is invalid",
    529             Self::SchemaCatalog => "HarvestCircle schema catalog is invalid",
    530         })
    531     }
    532 }
    533 
    534 impl Error for HarvestCircleStorageContractError {}
    535 
    536 #[must_use]
    537 pub(crate) const fn harvestcircle_initial_schema_sql() -> &'static [&'static str] {
    538     &INITIAL_SCHEMA_SQL
    539 }
    540 
    541 pub fn harvestcircle_migration_catalog()
    542 -> Result<MigrationCatalog, HarvestCircleStorageContractError> {
    543     let migration = MigrationDescriptor::sql(
    544         2,
    545         "bound_durable_operation_receipts",
    546         MIGRATE_DURABLE_OPERATIONS_V2_SQL,
    547         DURABLE_OPERATIONS_V2_MIGRATION_CHECKSUM,
    548     )
    549     .map_err(|_| HarvestCircleStorageContractError::MigrationCatalog)?;
    550     let evidence = MigrationDescriptor::sql(
    551         3,
    552         "add_verified_listing_evidence",
    553         MIGRATE_AVAILABILITY_EVIDENCE_V3_SQL,
    554         AVAILABILITY_EVIDENCE_V3_MIGRATION_CHECKSUM,
    555     )
    556     .map_err(|_| HarvestCircleStorageContractError::MigrationCatalog)?;
    557     MigrationCatalog::new([migration, evidence])
    558         .map_err(|_| HarvestCircleStorageContractError::MigrationCatalog)
    559 }
    560 
    561 pub fn harvestcircle_schema_catalog() -> Result<SchemaCatalog, HarvestCircleStorageContractError> {
    562     let migrations = harvestcircle_migration_catalog()?;
    563     schema_catalog_for(&migrations)
    564 }
    565 
    566 fn schema_catalog_for(
    567     migrations: &MigrationCatalog,
    568 ) -> Result<SchemaCatalog, HarvestCircleStorageContractError> {
    569     let version_one = SchemaVersionCatalog::new(
    570         HARVESTCIRCLE_INITIAL_STATE_SCHEMA_VERSION,
    571         schema_objects(CREATE_DURABLE_OPERATIONS_SQL)?,
    572         SchemaDigest::from_bytes(VERSION_ONE_DIGEST),
    573     )
    574     .map_err(schema_error)?;
    575     let version_two_objects = schema_objects(CREATE_DURABLE_OPERATIONS_V2_SQL)?;
    576     let version_two = SchemaVersionCatalog::new(
    577         2,
    578         version_two_objects,
    579         SchemaDigest::from_bytes(VERSION_TWO_DIGEST),
    580     )
    581     .map_err(schema_error)?;
    582     let version_three = SchemaVersionCatalog::new(
    583         3,
    584         schema_objects_v3()?,
    585         SchemaDigest::from_bytes(VERSION_THREE_DIGEST),
    586     )
    587     .map_err(schema_error)?;
    588     SchemaCatalog::new(migrations, [version_one, version_two, version_three]).map_err(schema_error)
    589 }
    590 
    591 fn schema_objects_v3() -> Result<Vec<SchemaObject>, HarvestCircleStorageContractError> {
    592     let mut objects = schema_objects(CREATE_DURABLE_OPERATIONS_V2_SQL)?;
    593     let mut statements = MIGRATE_AVAILABILITY_EVIDENCE_V3_SQL.split(";\n").skip(1);
    594     for (name, digest) in [
    595         ("availability_versions", AVAILABILITY_VERSIONS_DIGEST),
    596         ("public_payload_usage", PUBLIC_PAYLOAD_USAGE_DIGEST),
    597     ] {
    598         let sql = statements
    599             .next()
    600             .ok_or(HarvestCircleStorageContractError::SchemaCatalog)?;
    601         objects.push(
    602             SchemaObject::new(
    603                 SchemaObjectKind::Table,
    604                 name,
    605                 name,
    606                 sql,
    607                 SchemaDigest::from_bytes(digest),
    608             )
    609             .map_err(schema_error)?,
    610         );
    611     }
    612     Ok(objects)
    613 }
    614 
    615 fn schema_objects(
    616     durable_operations_sql: &'static str,
    617 ) -> Result<Vec<SchemaObject>, HarvestCircleStorageContractError> {
    618     let identities = [
    619         (
    620             SchemaObjectKind::Table,
    621             "account_identities",
    622             "account_identities",
    623         ),
    624         (
    625             SchemaObjectKind::Table,
    626             "local_signer_bindings",
    627             "local_signer_bindings",
    628         ),
    629         (SchemaObjectKind::Table, "runtime_state", "runtime_state"),
    630         (SchemaObjectKind::Table, "profile_cache", "profile_cache"),
    631         (
    632             SchemaObjectKind::Table,
    633             "account_preferences",
    634             "account_preferences",
    635         ),
    636         (
    637             SchemaObjectKind::Table,
    638             "durable_operations",
    639             "durable_operations",
    640         ),
    641         (
    642             SchemaObjectKind::Table,
    643             "installation_identity",
    644             "installation_identity",
    645         ),
    646         (
    647             SchemaObjectKind::Trigger,
    648             "installation_identity_no_update",
    649             "installation_identity",
    650         ),
    651         (
    652             SchemaObjectKind::Trigger,
    653             "installation_identity_no_delete",
    654             "installation_identity",
    655         ),
    656     ];
    657     let schema_sql = [
    658         CREATE_ACCOUNT_IDENTITIES_SQL,
    659         CREATE_LOCAL_SIGNER_BINDINGS_SQL,
    660         CREATE_RUNTIME_STATE_SQL,
    661         CREATE_PROFILE_CACHE_SQL,
    662         CREATE_ACCOUNT_PREFERENCES_SQL,
    663         durable_operations_sql,
    664         CREATE_INSTALLATION_IDENTITY_SQL,
    665         CREATE_INSTALLATION_IDENTITY_NO_UPDATE_SQL,
    666         CREATE_INSTALLATION_IDENTITY_NO_DELETE_SQL,
    667     ];
    668     let mut objects = identities
    669         .into_iter()
    670         .zip(schema_sql)
    671         .zip(OBJECT_DIGESTS)
    672         .map(|(((kind, name, table), sql), digest)| {
    673             let digest = if sql == CREATE_DURABLE_OPERATIONS_V2_SQL {
    674                 SchemaDigest::from_bytes(DURABLE_OPERATIONS_V2_DIGEST)
    675             } else {
    676                 SchemaDigest::from_bytes(digest)
    677             };
    678             SchemaObject::new(kind, name, table, sql, digest).map_err(schema_error)
    679         })
    680         .collect::<Result<Vec<_>, _>>()?;
    681     if durable_operations_sql == CREATE_DURABLE_OPERATIONS_V2_SQL {
    682         for (name, sql, digest) in [
    683             (
    684                 "durable_operations_receipt_insert_guard",
    685                 CREATE_DURABLE_OPERATIONS_RECEIPT_INSERT_GUARD_SQL,
    686                 DURABLE_OPERATIONS_RECEIPT_INSERT_GUARD_DIGEST,
    687             ),
    688             (
    689                 "durable_operations_receipt_update_guard",
    690                 CREATE_DURABLE_OPERATIONS_RECEIPT_UPDATE_GUARD_SQL,
    691                 DURABLE_OPERATIONS_RECEIPT_UPDATE_GUARD_DIGEST,
    692             ),
    693         ] {
    694             objects.push(
    695                 SchemaObject::new(
    696                     SchemaObjectKind::Trigger,
    697                     name,
    698                     "durable_operations",
    699                     sql,
    700                     SchemaDigest::from_bytes(digest),
    701                 )
    702                 .map_err(schema_error)?,
    703             );
    704         }
    705     }
    706     Ok(objects)
    707 }
    708 
    709 const fn schema_error(_: SchemaCatalogContractError) -> HarvestCircleStorageContractError {
    710     HarvestCircleStorageContractError::SchemaCatalog
    711 }
    712 
    713 #[cfg(test)]
    714 #[path = "availability_evidence_tests.rs"]
    715 mod availability_evidence_tests;
    716 
    717 #[cfg(test)]
    718 mod tests {
    719     use super::*;
    720     use radroots_runtime_paths::{
    721         InstanceId, RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver,
    722         RadrootsPlatform, RuntimeContextBootstrap, RuntimeContextSource, ServiceId,
    723     };
    724     use sqlx::{Connection, Row};
    725 
    726     fn context(service: &str, instance: &str) -> RuntimeContext {
    727         RuntimeContext::resolve(
    728             &RadrootsPathResolver::new(RadrootsPlatform::Macos, RadrootsHostEnvironment::default()),
    729             RuntimeContextBootstrap::new(
    730                 RadrootsPathProfile::RepoLocal,
    731                 Some(std::path::PathBuf::from("/tmp/harvestcircle-contract")),
    732                 RuntimeContextSource::BootstrapCli,
    733                 RuntimeContextSource::BootstrapCli,
    734             )
    735             .expect("bootstrap"),
    736             ServiceId::new(service).expect("service"),
    737             InstanceId::new(instance).expect("instance"),
    738         )
    739         .expect("context")
    740     }
    741 
    742     #[test]
    743     fn exact_context_paths_and_catalogs_are_sealed() {
    744         let contract = HarvestCircleStorageContract::from_runtime_context(&context(
    745             HARVESTCIRCLE_SERVICE_ID,
    746             HARVESTCIRCLE_INSTANCE_ID,
    747         ))
    748         .expect("contract");
    749         assert!(contract.paths().state_database().ends_with("state.sqlite"));
    750         assert!(contract.paths().state_lock().ends_with("state.lock"));
    751         assert_eq!(
    752             contract.application_id().get(),
    753             HARVESTCIRCLE_APPLICATION_ID
    754         );
    755         assert_eq!(contract.state_schema_version().get(), 3);
    756         assert_eq!(contract.migrations().current_version(), 3);
    757         assert_eq!(contract.migrations().descriptors().len(), 2);
    758         assert_eq!(contract.schema().versions().len(), 3);
    759         assert_eq!(harvestcircle_initial_schema_sql().len(), 9);
    760     }
    761 
    762     #[test]
    763     fn context_identity_and_public_diagnostics_fail_closed() {
    764         for invalid in [
    765             context("myc", "desktop"),
    766             context("harvestcircle", "primary"),
    767         ] {
    768             assert_eq!(
    769                 HarvestCircleStorageContract::from_runtime_context(&invalid),
    770                 Err(HarvestCircleStorageContractError::ContextIdentity)
    771             );
    772         }
    773         for error in [
    774             HarvestCircleStorageContractError::ContextIdentity,
    775             HarvestCircleStorageContractError::CanonicalPaths,
    776             HarvestCircleStorageContractError::MigrationCatalog,
    777             HarvestCircleStorageContractError::SchemaCatalog,
    778         ] {
    779             assert!(error.source().is_none());
    780             assert!(!error.to_string().contains("/tmp"));
    781         }
    782         let debug = format!(
    783             "{:?}",
    784             HarvestCircleStorageContract::from_runtime_context(&context(
    785                 HARVESTCIRCLE_SERVICE_ID,
    786                 HARVESTCIRCLE_INSTANCE_ID,
    787             ))
    788             .unwrap()
    789         );
    790         assert!(debug.contains("[redacted]"));
    791         assert!(!debug.contains("/tmp"));
    792     }
    793 
    794     #[test]
    795     fn machine_coordinates_match_the_typed_contract() {
    796         assert_eq!(
    797             harvestcircle_product::STORAGE_SERVICE_ID,
    798             HARVESTCIRCLE_SERVICE_ID
    799         );
    800         assert_eq!(
    801             harvestcircle_product::STORAGE_INSTANCE_ID,
    802             HARVESTCIRCLE_INSTANCE_ID
    803         );
    804         assert_eq!(
    805             harvestcircle_product::STORAGE_DATABASE_FILENAME,
    806             "state.sqlite"
    807         );
    808         assert_eq!(harvestcircle_product::STORAGE_LOCK_FILENAME, "state.lock");
    809         assert_eq!(
    810             harvestcircle_product::STORAGE_APPLICATION_ID,
    811             HARVESTCIRCLE_APPLICATION_ID.to_string()
    812         );
    813         assert_eq!(harvestcircle_product::STORAGE_APPLICATION_ID_TEXT, "HCR1");
    814         assert_eq!(
    815             harvestcircle_product::STORAGE_INITIAL_SCHEMA_VERSION,
    816             HARVESTCIRCLE_INITIAL_STATE_SCHEMA_VERSION.to_string()
    817         );
    818         assert_eq!(
    819             harvestcircle_product::LEGACY_DATABASE_FILENAME,
    820             "harvestcircle.sqlite3"
    821         );
    822         assert_eq!(
    823             harvestcircle_product::LEGACY_DATABASE_DISPOSITION,
    824             "untouched_and_unsupported"
    825         );
    826         assert_eq!(
    827             harvestcircle_product::PLATFORM_MACOS_ARCHITECTURE,
    828             "aarch64"
    829         );
    830         assert_eq!(harvestcircle_product::PLATFORM_LINUX_ARCHITECTURE, "x86_64");
    831         assert_eq!(
    832             harvestcircle_product::LIMIT_IDENTITIES,
    833             HARVESTCIRCLE_IDENTITY_CAPACITY.to_string()
    834         );
    835         assert_eq!(
    836             harvestcircle_product::LIMIT_UNFINISHED_DURABLE_OPERATIONS,
    837             HARVESTCIRCLE_UNFINISHED_DURABLE_OPERATION_CAPACITY.to_string()
    838         );
    839         assert_eq!(
    840             harvestcircle_product::LIMIT_PREFERENCE_VALUE_UTF8_BYTES,
    841             HARVESTCIRCLE_PREFERENCE_VALUE_UTF8_BYTES.to_string()
    842         );
    843         assert_eq!(
    844             harvestcircle_product::LIMIT_RELAY_ENDPOINTS,
    845             HARVESTCIRCLE_RELAY_ENDPOINT_CAPACITY.to_string()
    846         );
    847         assert_eq!(
    848             harvestcircle_product::LIMIT_RELAY_URL_BYTES,
    849             HARVESTCIRCLE_RELAY_URL_UTF8_BYTES.to_string()
    850         );
    851         assert_eq!(
    852             harvestcircle_product::LIMIT_EVENTS_PER_RELAY,
    853             HARVESTCIRCLE_EVENTS_PER_RELAY_CAPACITY.to_string()
    854         );
    855         assert_eq!(
    856             harvestcircle_product::LIMIT_EVENTS_TOTAL,
    857             HARVESTCIRCLE_EVENTS_TOTAL_CAPACITY.to_string()
    858         );
    859         assert_eq!(
    860             harvestcircle_product::LIMIT_OBSERVERS,
    861             HARVESTCIRCLE_OBSERVER_CAPACITY.to_string()
    862         );
    863         assert_eq!(
    864             harvestcircle_product::LIMIT_ACTOR_MAILBOX,
    865             HARVESTCIRCLE_ACTOR_MAILBOX_CAPACITY.to_string()
    866         );
    867         assert_eq!(
    868             harvestcircle_product::LIMIT_COMMAND_DEADLINE_MIN_MS,
    869             HARVESTCIRCLE_COMMAND_DEADLINE_MIN_MS.to_string()
    870         );
    871         assert_eq!(
    872             harvestcircle_product::LIMIT_COMMAND_DEADLINE_MAX_MS,
    873             HARVESTCIRCLE_COMMAND_DEADLINE_MAX_MS.to_string()
    874         );
    875         assert_eq!(
    876             harvestcircle_product::BACKUP_MEMBER_LIMIT,
    877             "caller_supplied_positive"
    878         );
    879     }
    880 
    881     #[tokio::test]
    882     async fn schema_sql_executes_as_one_fresh_strict_v1_inventory() {
    883         let mut connection = sqlx::SqliteConnection::connect(":memory:")
    884             .await
    885             .expect("memory database");
    886         sqlx::query("PRAGMA foreign_keys = ON")
    887             .execute(&mut connection)
    888             .await
    889             .expect("foreign keys");
    890         sqlx::query("PRAGMA trusted_schema = OFF")
    891             .execute(&mut connection)
    892             .await
    893             .expect("trusted schema");
    894         for statement in harvestcircle_initial_schema_sql() {
    895             sqlx::query(*statement)
    896                 .execute(&mut connection)
    897                 .await
    898                 .expect("schema statement");
    899         }
    900         let rows = sqlx::query(
    901             "SELECT type, name, tbl_name FROM sqlite_schema \
    902                  WHERE name NOT LIKE 'sqlite_%' ORDER BY type, name LIMIT 10",
    903         )
    904         .fetch_all(&mut connection)
    905         .await
    906         .expect("inventory rows");
    907         let inventory = rows
    908             .iter()
    909             .map(|row| {
    910                 (
    911                     row.get::<String, _>("type"),
    912                     row.get::<String, _>("name"),
    913                     row.get::<String, _>("tbl_name"),
    914                 )
    915             })
    916             .collect::<Vec<_>>();
    917         assert_eq!(inventory.len(), 9);
    918         assert_eq!(
    919             inventory
    920                 .iter()
    921                 .filter(|(kind, _, _)| kind == "table")
    922                 .count(),
    923             7
    924         );
    925         assert_eq!(
    926             inventory
    927                 .iter()
    928                 .filter(|(kind, _, _)| kind == "trigger")
    929                 .count(),
    930             2
    931         );
    932         assert!(inventory.iter().all(|(_, name, _)| {
    933             !matches!(
    934                 name.as_str(),
    935                 "application_schema" | "operation_journal" | "refinery_schema_history"
    936             )
    937         }));
    938     }
    939 
    940     #[tokio::test]
    941     async fn migration_v2_adds_the_exact_bounded_journal_schema() {
    942         let mut connection = sqlx::SqliteConnection::connect(":memory:")
    943             .await
    944             .expect("memory database");
    945         sqlx::query("PRAGMA foreign_keys = ON")
    946             .execute(&mut connection)
    947             .await
    948             .expect("foreign keys");
    949         sqlx::query("PRAGMA trusted_schema = OFF")
    950             .execute(&mut connection)
    951             .await
    952             .expect("trusted schema");
    953         for statement in harvestcircle_initial_schema_sql() {
    954             sqlx::query(*statement)
    955                 .execute(&mut connection)
    956                 .await
    957                 .expect("schema statement");
    958         }
    959         let identity = [7_u8; 32];
    960         sqlx::query(
    961             "INSERT INTO durable_operations (request_id, operation_kind, account_public_key, \
    962              binding_public_key, phase, terminal_outcome, updated_at_unix_s) \
    963              VALUES ('01890f3e-7b1c-7000-8000-000000000001', 'create', ?, ?, \
    964                      'finalized', 'completed', 10)",
    965         )
    966         .bind(identity.as_slice())
    967         .bind(identity.as_slice())
    968         .execute(&mut connection)
    969         .await
    970         .expect("terminal v1 row");
    971         sqlx::query(
    972             "INSERT INTO durable_operations (request_id, operation_kind, account_public_key, \
    973              binding_public_key, phase, updated_at_unix_s) \
    974              VALUES ('01890f3e-7b1c-7000-8000-000000000002', 'remove', ?, ?, \
    975                      'intent_recorded', 11)",
    976         )
    977         .bind(identity.as_slice())
    978         .bind(identity.as_slice())
    979         .execute(&mut connection)
    980         .await
    981         .expect("unfinished v1 row");
    982         sqlx::raw_sql(MIGRATE_DURABLE_OPERATIONS_V2_SQL)
    983             .execute(&mut connection)
    984             .await
    985             .expect("migration");
    986         let actual: String = sqlx::query_scalar(
    987             "SELECT sql FROM sqlite_schema WHERE type = 'table' AND name = 'durable_operations'",
    988         )
    989         .fetch_one(&mut connection)
    990         .await
    991         .expect("schema SQL");
    992         assert_eq!(actual, CREATE_DURABLE_OPERATIONS_V2_SQL);
    993         let rows = sqlx::query(
    994             "SELECT request_id, completed_at_unix_s FROM durable_operations ORDER BY request_id",
    995         )
    996         .fetch_all(&mut connection)
    997         .await
    998         .expect("migrated rows");
    999         assert_eq!(rows.len(), 2);
   1000         assert_eq!(
   1001             rows[0].get::<Option<i64>, _>("completed_at_unix_s"),
   1002             Some(10)
   1003         );
   1004         assert_eq!(rows[1].get::<Option<i64>, _>("completed_at_unix_s"), None);
   1005         assert!(
   1006             sqlx::query(
   1007                 "INSERT INTO durable_operations (request_id, operation_kind, account_public_key, \
   1008                  binding_public_key, phase, terminal_outcome, updated_at_unix_s) \
   1009                  VALUES ('01890f3e-7b1c-7000-8000-000000000004', 'create', ?, ?, \
   1010                          'finalized', 'completed', 13)",
   1011             )
   1012             .bind(identity.as_slice())
   1013             .bind(identity.as_slice())
   1014             .execute(&mut connection)
   1015             .await
   1016             .is_err(),
   1017             "insert guard must require terminal completion time"
   1018         );
   1019         assert!(
   1020             sqlx::query(
   1021                 "UPDATE durable_operations SET phase = 'finalized', \
   1022                  terminal_outcome = 'completed' WHERE request_id = \
   1023                  '01890f3e-7b1c-7000-8000-000000000002'",
   1024             )
   1025             .execute(&mut connection)
   1026             .await
   1027             .is_err(),
   1028             "update guard must require terminal completion time"
   1029         );
   1030 
   1031         let migration = MigrationChecksum::for_sql(MIGRATE_DURABLE_OPERATIONS_V2_SQL);
   1032         let objects = schema_objects(CREATE_DURABLE_OPERATIONS_V2_SQL).expect("objects");
   1033         let object = objects
   1034             .iter()
   1035             .find(|object| object.name() == "durable_operations")
   1036             .expect("durable operations")
   1037             .digest();
   1038         let snapshot = SchemaVersionCatalog::computed_digest(2, objects).expect("snapshot");
   1039         assert_eq!(migration, DURABLE_OPERATIONS_V2_MIGRATION_CHECKSUM);
   1040         assert_eq!(
   1041             object,
   1042             SchemaDigest::from_bytes(DURABLE_OPERATIONS_V2_DIGEST)
   1043         );
   1044         assert_eq!(snapshot, SchemaDigest::from_bytes(VERSION_TWO_DIGEST));
   1045     }
   1046 
   1047     #[tokio::test]
   1048     async fn migration_v2_rolls_back_without_partial_schema_on_invalid_v1_state() {
   1049         let mut connection = sqlx::SqliteConnection::connect(":memory:")
   1050             .await
   1051             .expect("memory database");
   1052         for statement in harvestcircle_initial_schema_sql() {
   1053             sqlx::query(*statement)
   1054                 .execute(&mut connection)
   1055                 .await
   1056                 .expect("schema statement");
   1057         }
   1058         sqlx::query("PRAGMA ignore_check_constraints = ON")
   1059             .execute(&mut connection)
   1060             .await
   1061             .expect("fixture policy");
   1062         let identity = [9_u8; 32];
   1063         sqlx::query(
   1064             "INSERT INTO durable_operations (request_id, operation_kind, account_public_key, \
   1065              binding_public_key, phase, updated_at_unix_s) \
   1066              VALUES ('01890f3e-7b1c-7000-8000-000000000003', 'create', ?, ?, 'finalized', 12)",
   1067         )
   1068         .bind(identity.as_slice())
   1069         .bind(identity.as_slice())
   1070         .execute(&mut connection)
   1071         .await
   1072         .expect("invalid v1 fixture");
   1073         sqlx::query("PRAGMA ignore_check_constraints = OFF")
   1074             .execute(&mut connection)
   1075             .await
   1076             .expect("restore policy");
   1077 
   1078         let mut transaction = connection.begin().await.expect("migration transaction");
   1079         assert!(
   1080             sqlx::raw_sql(MIGRATE_DURABLE_OPERATIONS_V2_SQL)
   1081                 .execute(&mut *transaction)
   1082                 .await
   1083                 .is_err()
   1084         );
   1085         transaction.rollback().await.expect("rollback");
   1086 
   1087         let columns: i64 = sqlx::query_scalar(
   1088             "SELECT count(*) FROM pragma_table_info('durable_operations') \
   1089              WHERE name = 'completed_at_unix_s'",
   1090         )
   1091         .fetch_one(&mut connection)
   1092         .await
   1093         .expect("column inventory");
   1094         let retained: i64 = sqlx::query_scalar("SELECT count(*) FROM durable_operations")
   1095             .fetch_one(&mut connection)
   1096             .await
   1097             .expect("retained row");
   1098         assert_eq!(columns, 0);
   1099         assert_eq!(retained, 1);
   1100     }
   1101 }