#![cfg(feature = "secondary-indexes")]
use core::num::NonZeroU32;
use core::ops::Bound::{Excluded, Included, Unbounded};
use pipecircuit::storage::query_workspace::{
QueryChunk, QueryWorkspace, QueryWorkspaceError, required_query_chunks,
};
use pipecircuit::storage::warp_table::{
Capacity, RangeIndexBounds, RangeIndexLookupError, RangeMembershipError, WarpTable,
WarpTableInsertWithIdOutcome, WarpTableRemoveOutcome, WarpTableRow, WorkspaceSelectionError,
};
use proptest::prelude::*;
#[derive(WarpTableRow)]
#[index(primary = u32)]
struct Unit {
#[index(range)]
x: i32,
#[index(range)]
y: u16,
#[index(secondary)]
team: u8,
}
fn table(capacity: u32) -> Result<WarpTable<Unit>, &'static str> {
WarpTable::try_new(Capacity::new(NonZeroU32::new(capacity).ok_or("capacity")?))
.map_err(|_| "table")
}
fn insert(
table: &mut WarpTable<Unit>,
id: u32,
x: i32,
y: u16,
team: u8,
) -> Result<(), &'static str> {
if matches!(
table.insert_with_id(id, Unit { x, y, team }),
WarpTableInsertWithIdOutcome::Inserted { .. }
) {
Ok(())
} else {
Err("insert")
}
}
#[test]
fn range_composition_reuses_chunks_across_edges_and_dense_relocation() -> Result<(), &'static str> {
for capacity in [1, 63, 64, 65, 4095, 4096, 4097, 65535, 65536, 65537, 100000] {
let mut table = table(capacity)?;
for id in 0..capacity {
insert(
&mut table,
id,
(id % 131) as i32 - 65,
(id % 19) as u16,
(id % 3) as u8,
)?;
}
assert!(matches!(
table.remove(0),
WarpTableRemoveOutcome::Removed { .. }
));
insert(&mut table, capacity, -91, 7, 1)?;
let mut chunks = core::iter::repeat_with(|| QueryChunk::EMPTY)
.take(required_query_chunks(capacity as usize).map_err(|_| "chunks")?)
.collect::<Vec<_>>();
let mut scratch = QueryChunk::EMPTY;
let mut workspace =
QueryWorkspace::new(capacity as usize, chunks.as_mut_slice(), &mut scratch)
.map_err(|_| "workspace")?;
for bounds in [
(Unbounded, Unbounded),
(Included(-17), Excluded(33)),
(Included(91), Included(91)),
(Excluded(0), Included(0)),
(Unbounded, Excluded(-18)),
(Excluded(20), Unbounded),
] {
let lookup = table
.query_memberships()
.find_x_in(bounds)
.map_err(|_| "range")?;
assert_eq!(lookup.bounds(), &RangeIndexBounds::from_range(bounds));
let selected = lookup
.in_workspace(workspace.membership().map_err(|_| "members")?)
.map_err(|_| "range workspace")?
.and(|q| q.find_team(1))
.map_err(|_| "and")?
.or(|q| q.find_y_in(3..=6))
.map_err(|_| "or")?
.and_not(|q| q.find_x_in(-9..=9))
.map_err(|_| "difference")?
.xor(|q| q.find_y_in(0..=1))
.map_err(|_| "xor")?;
let expected = table
.query()
.find_x_in(bounds)
.map_err(|_| "owned range")?
.and(|q| q.find_team(1))
.or(|q| q.find_y_in(3..=6))
.map_err(|_| "owned or")?
.and_not(|q| q.find_x_in(-9..=9))
.map_err(|_| "owned difference")?
.xor(|q| q.find_y_in(0..=1))
.map_err(|_| "owned xor")?;
assert_eq!(selected.len(), expected.len());
assert_eq!(
selected.rows().map(|r| *r.id).collect::<Vec<_>>(),
expected.rows().map(|r| *r.id).collect::<Vec<_>>()
);
let sparse = table
.query_memberships()
.find_team(2)
.in_workspace(workspace.membership().map_err(|_| "members")?)
.map_err(|_| "start")?
.and(|q| q.find_x_in(bounds))
.map_err(|_| "sparse range")?;
assert_eq!(
sparse.len(),
table
.query()
.find_team(2)
.and(|q| q.find_x_in(bounds))
.map_err(|_| "expected sparse")?
.len()
);
}
}
Ok(())
}
#[test]
fn refusals_leave_membership_reusable_and_preserve_exact_reasons() -> Result<(), &'static str> {
let mut table = table(8)?;
insert(&mut table, 1, 5, 3, 1)?;
let foreign = self::table(8)?;
let mut chunks = [QueryChunk::EMPTY];
let mut scratch = QueryChunk::EMPTY;
let mut workspace =
QueryWorkspace::new(8, &mut chunks, &mut scratch).map_err(|_| "workspace")?;
let selection = table
.query_memberships()
.find_team(1)
.in_workspace(workspace.membership().map_err(|_| "members")?)
.map_err(|_| "start")?;
assert!(
matches!(selection.and(|q| q.find_x_in((Included(9), Included(1)))), Err(RangeMembershipError::Lookup(RangeIndexLookupError::ReversedBounds { bounds })) if bounds == RangeIndexBounds::from_range((Included(9), Included(1))))
);
assert_eq!(workspace.membership().map_err(|_| "members")?.len(), 1);
let selection = table
.query_memberships()
.find_x_in(..)
.map_err(|_| "range")?
.in_workspace(workspace.membership().map_err(|_| "members")?)
.map_err(|_| "start")?;
assert!(matches!(
selection.or(|_| foreign.query_memberships().find_y_in(..)),
Err(RangeMembershipError::Selection(
WorkspaceSelectionError::DifferentTables
))
));
assert_eq!(
workspace
.membership()
.map_err(|_| "members")?
.slots()
.collect::<Vec<_>>(),
[0]
);
let mismatched_index =
pipecircuit::storage::warp_table::__private::RangeIndex::<i32>::try_new(7)
.map_err(|_| "index")?;
let selection = table
.query_memberships()
.find_team(1)
.in_workspace(workspace.membership().map_err(|_| "members")?)
.map_err(|_| "start")?;
assert!(matches!(
selection.and(|_| {
pipecircuit::storage::warp_table::__private::bind_range_membership_lookup(
&table,
&mismatched_index,
..,
)
}),
Err(RangeMembershipError::Selection(
WorkspaceSelectionError::Workspace(QueryWorkspaceError::DifferentCapacities {
left: 8,
right: 7
})
))
));
assert_eq!(
workspace
.membership()
.map_err(|_| "members")?
.slots()
.collect::<Vec<_>>(),
[0]
);
let mut workspace =
QueryWorkspace::new(7, &mut chunks, &mut scratch).map_err(|_| "workspace")?;
let mut members = workspace.membership().map_err(|_| "members")?;
let _inserted = members.insert(5).map_err(|_| "insert")?;
assert!(matches!(
table
.query_memberships()
.find_x_in(..)
.map_err(|_| "range")?
.in_workspace(members),
Err(RangeMembershipError::Selection(
WorkspaceSelectionError::Workspace(QueryWorkspaceError::DifferentCapacities {
left: 7,
right: 8
})
))
));
assert_eq!(
workspace
.membership()
.map_err(|_| "members")?
.slots()
.collect::<Vec<_>>(),
[5]
);
Ok(())
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(128))]
#[test]
fn mixed_composition_matches_row_predicates(
rows in prop::collection::vec((any::<i16>(), any::<u16>(), 0_u8..4), 0..256),
bounds in prop::array::uniform4(any::<i16>()),
y in any::<u16>(),
team in 0_u8..5,
) {
let mut table = table(256).map_err(TestCaseError::fail)?;
for (id, (x, y, team)) in rows.iter().copied().enumerate() {
insert(&mut table, id as u32, i32::from(x), y, team).map_err(TestCaseError::fail)?;
}
let mut chunks = [QueryChunk::EMPTY];
let mut scratch = QueryChunk::EMPTY;
let mut workspace = QueryWorkspace::new(256, &mut chunks, &mut scratch).map_err(|_| TestCaseError::fail("workspace"))?;
let [a,b,c,d] = bounds;
let first = i32::from(a.min(b))..=i32::from(a.max(b));
let second = i32::from(c.min(d))..=i32::from(c.max(d));
let selected = table.query_memberships().find_x_in(first.clone()).map_err(|_| TestCaseError::fail("range"))?
.in_workspace(workspace.membership().map_err(|_| TestCaseError::fail("members"))?).map_err(|_| TestCaseError::fail("start"))?
.xor(|q| q.find_team(team)).map_err(|_| TestCaseError::fail("xor"))?
.or(|q| q.find_y_in(..y)).map_err(|_| TestCaseError::fail("union"))?
.and(|q| q.find_x_in(second.clone())).map_err(|_| TestCaseError::fail("intersection"))?;
let expected = rows.iter().enumerate().filter(|(_, (x, ry, rt))| ((first.contains(&i32::from(*x)) != (*rt == team)) || *ry < y) && second.contains(&i32::from(*x))).map(|(id, _)| id as u32).collect::<Vec<_>>();
prop_assert_eq!(selected.len() as usize, expected.len());
prop_assert_eq!(selected.ids().collect::<Vec<_>>(), expected);
}
}