lix 0.12.3

Embeddable version control for apps and AI agents.
Documentation
use std::ops::Bound;

use bytes::Bytes;
use lix::open_lix;
use lix::storage::{
    BeginScanOptions, CoreProjection, Key, KeyRange, ProjectedValue, PutBatch, PutEntry,
    ReadOptions, SpaceId, Storage, StorageRead, StorageSpace, StorageWrite, StoredValue,
    WriteOptions,
};

const HOT_ROW_SPACE: StorageSpace =
    StorageSpace::mutable(SpaceId(0x0004_001b), "hot_state.row.v21");
const SEQUENCE_IDENTITY: &[u8] = b"lix_deterministic_sequence_number";
const UNRELATED_IDENTITY: &[u8] = b"lix_unrelated_sequence_substitute";

pub async fn initialize_with_deterministic_mode<S>(storage: S)
where
    S: Storage + Clone + Send + Sync + 'static,
{
    let lix = open_lix()
        .with_storage(storage)
        .await
        .expect("repository should open");
    lix.execute(
        "INSERT INTO lix_key_value (key, value, lixcol_global, lixcol_untracked) \
             VALUES ('lix_deterministic_mode', CAST('{\"enabled\":true}' AS JSONB), true, true)",
        &[],
    )
    .await
    .expect("deterministic mode should enable without publishing a sequence row");
}

pub async fn assert_next_uuid<S>(storage: S, suffix: &str)
where
    S: Storage + Clone + Send + Sync + 'static,
{
    let lix = open_lix()
        .with_storage(storage)
        .await
        .expect("repository should reopen");
    let result = lix
        .execute("SELECT uuidv7() AS value", &[])
        .await
        .expect("valid deterministic authority should produce the next UUID");
    let value = result.rows()[0]
        .get::<String>("value")
        .expect("UUID should be text");
    assert_eq!(value, format!("01920000-0000-7000-8000-{suffix}"));
}

pub async fn replace_selected_sequence_member_with_unrelated<S>(storage: &S)
where
    S: Storage + Send + Sync,
{
    let read = storage
        .begin_read(ReadOptions::default())
        .await
        .expect("selected HOT generation should read");
    let range = KeyRange {
        lower: Bound::Unbounded,
        upper: Bound::Unbounded,
    };
    let mut sequence_entries = Vec::new();
    let mut cursor = read
        .begin_scan(
            HOT_ROW_SPACE,
            range,
            BeginScanOptions {
                projection: CoreProjection::FullValue,
                ..BeginScanOptions::default()
            },
        )
        .await
        .expect("HOT member scan should begin");
    loop {
        let (page, page_has_more) = cursor
            .next_page(lix::storage::MAX_SCAN_PAGE_ROWS)
            .await
            .expect("HOT members should scan")
            .into_parts();
        sequence_entries.extend(
            page.into_iter()
                .filter(|entry| contains_subslice(entry.key.0.as_ref(), SEQUENCE_IDENTITY)),
        );
        if !page_has_more {
            break;
        }
    }
    drop(cursor);
    drop(read);
    assert_eq!(
        sequence_entries.len(),
        1,
        "fixture should have one selected sequence HOT member"
    );
    let sequence_entry = sequence_entries.pop().expect("one sequence entry");
    let ProjectedValue::FullValue(value) = sequence_entry.value else {
        panic!("sequence corruption fixture must read the full row value");
    };
    let replacement_key = Key(Bytes::from(replace_subslice(
        sequence_entry.key.0.as_ref(),
        SEQUENCE_IDENTITY,
        UNRELATED_IDENTITY,
    )));

    let mut write = storage
        .begin_write(WriteOptions::default())
        .await
        .expect("physical corruption write should open");
    write
        .delete_many(HOT_ROW_SPACE, std::slice::from_ref(&sequence_entry.key))
        .await
        .expect("selected sequence member deletion should stage");
    write
        .put_many(
            HOT_ROW_SPACE,
            PutBatch {
                entries: vec![PutEntry {
                    key: replacement_key,
                    value: StoredValue { bytes: value },
                }],
            },
        )
        .await
        .expect("unrelated same-count member should stage");
    write
        .commit()
        .await
        .expect("same-count sequence substitution should commit");
}

pub async fn assert_missing_sequence_member_fails_closed<S>(storage: S)
where
    S: Storage + Clone + Send + Sync + 'static,
{
    let lix = open_lix()
        .with_storage(storage)
        .await
        .expect("member-corrupt repository should open structurally");
    let error = lix
        .execute("SELECT uuidv7()", &[])
        .await
        .expect_err("missing selected sequence member must fail closed");
    assert!(
        error.message.contains("identity digest") && error.message.contains("canonical members"),
        "unexpected member-closure error: {error:?}"
    );
}

fn replace_subslice(haystack: &[u8], needle: &[u8], replacement: &[u8]) -> Vec<u8> {
    assert_eq!(needle.len(), replacement.len());
    let position = haystack
        .windows(needle.len())
        .position(|candidate| candidate == needle)
        .expect("sequence identity should be present in its physical key");
    assert!(
        haystack[position + needle.len()..]
            .windows(needle.len())
            .all(|candidate| candidate != needle),
        "sequence identity should occur exactly once in its physical key"
    );
    let mut replaced = haystack.to_vec();
    replaced[position..position + needle.len()].copy_from_slice(replacement);
    replaced
}

fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
    haystack
        .windows(needle.len())
        .any(|candidate| candidate == needle)
}