diff --git a/Cargo.lock b/Cargo.lock index 820f1fb5..3fb93796 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -286,12 +286,33 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "bumpalo" version = "3.16.0" @@ -405,16 +426,38 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + [[package]] name = "flatbuffers" version = "24.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8add37afff2d4ffa83bc748a70b4b1370984f6980768554182424ef71447c35f" dependencies = [ - "bitflags", + "bitflags 1.3.2", "rustc_version", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "getrandom" version = "0.2.15" @@ -474,6 +517,7 @@ dependencies = [ "async-trait", "itertools", "pretty_assertions", + "proptest", "rand", ] @@ -593,6 +637,12 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "log" version = "0.4.22" @@ -713,6 +763,32 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.6.0", + "lazy_static", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.36" @@ -752,6 +828,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + [[package]] name = "regex" version = "1.10.5" @@ -790,6 +875,31 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.18" @@ -851,6 +961,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fcd239983515c23a32fb82099f97d0b11b8c72f654ed659363a95c3dad7a53" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -860,6 +983,12 @@ dependencies = [ "crunchy", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -872,6 +1001,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -941,6 +1079,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index daff15f1..b445c2be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ debug = true [dev-dependencies] pretty_assertions = "1.4.0" +proptest = "1.0.0" diff --git a/Justfile b/Justfile index ff1afa29..eda217af 100644 --- a/Justfile +++ b/Justfile @@ -34,4 +34,4 @@ check-deps *args='': cargo deny --all-features check {{args}} # run all checks that CI actions will run -pre-commit: (compile-tests "--locked") build (format "--check") lint test check-deps +pre-commit $RUSTFLAGS="-D warnings -W unreachable-pub -W bare-trait-objects": (compile-tests "--locked") build (format "--check") lint test check-deps diff --git a/proptest-regressions/manifest.txt b/proptest-regressions/manifest.txt new file mode 100644 index 00000000..1c56e413 --- /dev/null +++ b/proptest-regressions/manifest.txt @@ -0,0 +1,7 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc c056e4188055e9eacbeebae5e0a1caf8ff2fde6a877784ec42c9c32af4b4a4ff # shrinks to v = [] diff --git a/src/lib.rs b/src/lib.rs index 74f689c4..620e5d27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,10 +22,12 @@ /// - A `Dataset` concrete type, implements the high level interface, using an Storage /// implementation and the data tables. pub mod dataset; +pub mod manifest; pub mod storage; pub mod structure; use async_trait::async_trait; +use manifest::ManifestsTable; use std::{ collections::{HashMap, HashSet}, fmt::Display, @@ -35,8 +37,9 @@ use std::{ }; use structure::StructureTable; +#[derive(Clone, Debug, Hash, PartialEq, Eq)] /// An ND index to an element in an array. -pub type ArrayIndices = Vec; +pub struct ArrayIndices(pub Vec); /// The shape of an array. /// 0 is a valid shape member @@ -136,9 +139,9 @@ impl TryFrom for ChunkKeyEncoding { fn try_from(value: u8) -> Result { match value { - c if { c == '/' as u8 } => Ok(ChunkKeyEncoding::Slash), - c if { c == '.' as u8 } => Ok(ChunkKeyEncoding::Dot), - c if { c == 'x' as u8 } => Ok(ChunkKeyEncoding::Default), + b'/' => Ok(ChunkKeyEncoding::Slash), + b'.' => Ok(ChunkKeyEncoding::Dot), + b'x' => Ok(ChunkKeyEncoding::Default), _ => Err("Invalid chunk key encoding character"), } } @@ -147,9 +150,9 @@ impl TryFrom for ChunkKeyEncoding { impl From for u8 { fn from(value: ChunkKeyEncoding) -> Self { match value { - ChunkKeyEncoding::Slash => '/' as u8, - ChunkKeyEncoding::Dot => '.' as u8, - ChunkKeyEncoding::Default => 'x' as u8, + ChunkKeyEncoding::Slash => b'/', + ChunkKeyEncoding::Dot => b'.', + ChunkKeyEncoding::Default => b'x', } } } @@ -236,7 +239,7 @@ enum UserAttributesStructure { struct ManifestExtents(Vec); #[derive(Debug, Clone, PartialEq, Eq)] -struct ManifestRef { +pub struct ManifestRef { object_id: ObjectId, location: TableRegion, flags: Flags, @@ -276,24 +279,28 @@ pub struct NodeStructure { node_data: NodeData, } +#[derive(Clone, Debug, PartialEq, Eq)] pub struct VirtualChunkRef { location: String, // FIXME: better type offset: u64, length: u64, } +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ChunkRef { id: ObjectId, // FIXME: better type offset: u64, length: u64, } +#[derive(Clone, Debug, PartialEq, Eq)] pub enum ChunkPayload { Inline(Vec), // FIXME: optimize copies Virtual(VirtualChunkRef), Ref(ChunkRef), } +#[derive(Clone, Debug, PartialEq, Eq)] pub struct ChunkInfo { node: NodeId, coord: ArrayIndices, @@ -303,9 +310,6 @@ pub struct ChunkInfo { // FIXME: this will hold the arrow file pub struct AttributesTable(); -// FIXME: this will hold the arrow file -pub struct ManifestsTable(); - pub enum AddNodeError { AlreadyExists, } @@ -325,7 +329,7 @@ pub enum StorageError { /// Different implementation can cache the files differently, or not at all. /// Implementations are free to assume files are never overwritten. #[async_trait] -trait Storage { +pub trait Storage { async fn fetch_structure( &self, id: &ObjectId, @@ -367,3 +371,17 @@ pub struct Dataset { // FIXME: issue with too many inline chunks kept in mem set_chunks: HashMap<(Path, ArrayIndices), ChunkPayload>, } + +impl Dataset { + pub fn new(storage: Box, structure_id: ObjectId) -> Self { + Dataset { + structure_id, + storage, + new_groups: HashSet::new(), + new_arrays: HashMap::new(), + updated_arrays: HashMap::new(), + updated_attributes: HashMap::new(), + set_chunks: HashMap::new(), + } + } +} diff --git a/src/manifest.rs b/src/manifest.rs new file mode 100644 index 00000000..1c6dd7c1 --- /dev/null +++ b/src/manifest.rs @@ -0,0 +1,377 @@ +use std::sync::Arc; + +use arrow::{ + array::{ + Array, AsArray, BinaryArray, FixedSizeBinaryArray, GenericBinaryBuilder, + RecordBatch, StringArray, UInt32Array, UInt64Array, + }, + datatypes::{Field, Schema, UInt32Type, UInt64Type}, +}; +use itertools::Itertools; + +use crate::{ + ArrayIndices, ChunkInfo, ChunkPayload, ChunkRef, ObjectId, TableOffset, TableRegion, + VirtualChunkRef, +}; + +pub struct ManifestsTable { + batch: RecordBatch, +} + +impl ManifestsTable { + pub fn get_chunk_info( + &self, + coords: &ArrayIndices, + region: &TableRegion, + ) -> Option { + // FIXME: make this fast, currently it's a linear search + // FIXME: return error type + let idx = self.get_chunk_info_index(coords, region)?; + self.get_row(idx) + } + + fn get_row(&self, row: TableOffset) -> Option { + if row as usize >= self.batch.num_rows() { + return None; + } + + let id_col = + self.batch.column_by_name("array_id")?.as_primitive_opt::()?; + let coords_col = self.batch.column_by_name("coords")?.as_binary_opt::()?; + let offset_col = + self.batch.column_by_name("offset")?.as_primitive_opt::()?; + let length_col = + self.batch.column_by_name("length")?.as_primitive_opt::()?; + let inline_col = + self.batch.column_by_name("inline_data")?.as_binary_opt::()?; + let chunk_id_col = + self.batch.column_by_name("chunk_id")?.as_fixed_size_binary_opt()?; + let virtual_path_col = + self.batch.column_by_name("virtual_path")?.as_string_opt::()?; + // FIXME: do something with extras + let _extra_col = self.batch.column_by_name("extra")?.as_string_opt::()?; + + // These arrays cannot contain null values, we don't need to check using `is_null` + let idx = row as usize; + let id = id_col.value(idx); + let coords = + ArrayIndices::unchecked_try_from_slice(coords_col.value(idx)).ok()?; + + if inline_col.is_valid(idx) { + // we have an inline chunk + let data = inline_col.value(idx).into(); + Some(ChunkInfo { + node: id, + coord: coords, + payload: ChunkPayload::Inline(data), + }) + } else { + let offset = offset_col.value(idx); + let length = length_col.value(idx); + + if virtual_path_col.is_valid(idx) { + // we have a virtual chunk + let location = virtual_path_col.value(idx).to_string(); + Some(ChunkInfo { + node: id, + coord: coords, + payload: ChunkPayload::Virtual(VirtualChunkRef { + location, + offset, + length, + }), + }) + } else { + // we have a materialized chunk + let chunk_id = chunk_id_col.value(idx).try_into().ok()?; + Some(ChunkInfo { + node: id, + coord: coords, + payload: ChunkPayload::Ref(ChunkRef { id: chunk_id, offset, length }), + }) + } + } + } + + fn get_chunk_info_index( + &self, + coords: &ArrayIndices, + TableRegion(from_row, to_row): &TableRegion, + ) -> Option { + if *to_row as usize > self.batch.num_rows() || from_row > to_row { + return None; + } + let arrray = self + .batch + .column_by_name("coords")? + .as_binary_opt::()? + .slice(*from_row as usize, (to_row - from_row) as usize); + let binary_coord: Vec = coords.into(); + let binary_coord = binary_coord.as_slice(); + let position: TableOffset = arrray + .iter() + .position(|coord| coord == Some(binary_coord))? + .try_into() + .ok()?; + Some(position + from_row) + } +} + +impl ArrayIndices { + // FIXME: better error type + pub fn try_from_slice(rank: usize, slice: &[u8]) -> Result { + if slice.len() != rank * 8 { + Err(format!("Invalid slice length {}, expecting {}", slice.len(), rank)) + } else { + ArrayIndices::unchecked_try_from_slice(slice) + } + } + + pub fn unchecked_try_from_slice(slice: &[u8]) -> Result { + let chunked = slice.iter().chunks(8); + let res = chunked.into_iter().map(|chunk| { + u64::from_be_bytes( + chunk + .copied() + .collect::>() + .as_slice() + .try_into() + .expect("Invalid slice size"), + ) + }); + Ok(ArrayIndices(res.collect())) + } +} + +impl From<&ArrayIndices> for Vec { + fn from(ArrayIndices(ref value): &ArrayIndices) -> Self { + value.iter().flat_map(|c| c.to_be_bytes()).collect() + } +} + +pub fn mk_manifests_table>(coll: T) -> ManifestsTable { + let mut array_ids = Vec::new(); + let mut coords = Vec::new(); + let mut chunk_ids = Vec::new(); + let mut inline_data = Vec::new(); + let mut virtual_paths = Vec::new(); + let mut offsets = Vec::new(); + let mut lengths = Vec::new(); + let mut extras: Vec> = Vec::new(); + + for chunk in coll { + array_ids.push(chunk.node); + coords.push(chunk.coord); + // FIXME: + extras.push(None); + + match chunk.payload { + ChunkPayload::Inline(data) => { + lengths.push(data.len() as u64); + inline_data.push(Some(data)); + chunk_ids.push(None); + virtual_paths.push(None); + offsets.push(None); + } + ChunkPayload::Ref(ChunkRef { id, offset, length }) => { + lengths.push(length); + inline_data.push(None); + chunk_ids.push(Some(id)); + virtual_paths.push(None); + offsets.push(Some(offset)); + } + ChunkPayload::Virtual(VirtualChunkRef { location, offset, length }) => { + lengths.push(length); + inline_data.push(None); + chunk_ids.push(None); + virtual_paths.push(Some(location)); + offsets.push(Some(offset)); + } + } + } + + let array_ids = mk_array_ids_array(array_ids); + let coords = mk_coords_array(coords); + let offsets = mk_offsets_array(offsets); + let lengths = mk_lengths_array(lengths); + let inline_data = mk_inline_data_array(inline_data); + let chunk_ids = mk_chunk_ids_array(chunk_ids); + let virtual_paths = mk_virtual_paths_array(virtual_paths); + let extras = mk_extras_array(extras); + + let columns: Vec> = vec![ + Arc::new(array_ids), + Arc::new(coords), + Arc::new(offsets), + Arc::new(lengths), + Arc::new(inline_data), + Arc::new(chunk_ids), + Arc::new(virtual_paths), + Arc::new(extras), + ]; + + let schema = Arc::new(Schema::new(vec![ + Field::new("array_id", arrow::datatypes::DataType::UInt32, false), + Field::new("coords", arrow::datatypes::DataType::Binary, false), + Field::new("offset", arrow::datatypes::DataType::UInt64, true), + Field::new("length", arrow::datatypes::DataType::UInt64, false), + Field::new("inline_data", arrow::datatypes::DataType::Binary, true), + Field::new( + "chunk_id", + arrow::datatypes::DataType::FixedSizeBinary(ObjectId::SIZE as i32), + true, + ), + Field::new("virtual_path", arrow::datatypes::DataType::Utf8, true), + Field::new("extra", arrow::datatypes::DataType::Utf8, true), + ])); + let batch = + RecordBatch::try_new(schema, columns).expect("Error creating record batch"); + ManifestsTable { batch } +} + +fn mk_offsets_array>>(coll: T) -> UInt64Array { + coll.into_iter().collect() +} + +fn mk_virtual_paths_array>>( + coll: T, +) -> StringArray { + coll.into_iter().collect() +} + +fn mk_array_ids_array>(coll: T) -> UInt32Array { + coll.into_iter().collect() +} + +fn mk_inline_data_array>>>(coll: T) -> BinaryArray { + BinaryArray::from_iter(coll) +} + +fn mk_lengths_array>(coll: T) -> UInt64Array { + coll.into_iter().collect() +} + +fn mk_extras_array>>(coll: T) -> StringArray { + // FIXME: implement extras + coll.into_iter().map(|_x| None as Option).collect() +} + +fn mk_coords_array>(coll: T) -> BinaryArray { + let mut builder = GenericBinaryBuilder::::new(); + for ref coords in coll { + let vec: Vec = coords.into(); + builder.append_value(vec.as_slice()); + } + builder.finish() +} + +fn mk_chunk_ids_array>>( + coll: T, +) -> FixedSizeBinaryArray { + let iter = coll.into_iter().map(|oid| oid.map(|oid| oid.0)); + FixedSizeBinaryArray::try_from_sparse_iter_with_size(iter, ObjectId::SIZE as i32) + .expect("Bad ObjectId size") +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use proptest::prelude::*; + + proptest! { + + #[test] + fn coordinate_encoding_roundtrip(v: Vec) { + let arr = ArrayIndices(v); + let as_vec: Vec = (&arr).into(); + let roundtrip = ArrayIndices::try_from_slice(arr.0.len(), as_vec.as_slice()).unwrap(); + assert_eq!(arr, roundtrip); + } + } + + #[test] + fn test_get_chunk_info() { + let c1a = ChunkInfo { + node: 1, + coord: ArrayIndices(vec![0, 0, 0]), + payload: ChunkPayload::Ref(ChunkRef { + id: ObjectId::random(), + offset: 0, + length: 128, + }), + }; + let c2a = ChunkInfo { + node: 1, + coord: ArrayIndices(vec![0, 0, 1]), + payload: ChunkPayload::Ref(ChunkRef { + id: ObjectId::random(), + offset: 42, + length: 43, + }), + }; + let c3a = ChunkInfo { + node: 1, + coord: ArrayIndices(vec![1, 0, 1]), + payload: ChunkPayload::Ref(ChunkRef { + id: ObjectId::random(), + offset: 9999, + length: 1, + }), + }; + + let c1b = ChunkInfo { + node: 2, + coord: ArrayIndices(vec![0, 0, 0]), + payload: ChunkPayload::Inline(vec![42, 43, 44]), + }; + + let c1c = ChunkInfo { + node: 2, + coord: ArrayIndices(vec![0, 0, 0]), + payload: ChunkPayload::Virtual(VirtualChunkRef { + location: "s3://foo.bar".to_string(), + offset: 99, + length: 100, + }), + }; + + let table = mk_manifests_table(vec![ + c1a.clone(), + c2a.clone(), + c3a.clone(), + c1b.clone(), + c1c.clone(), + ]); + let res = table.get_chunk_info(&ArrayIndices(vec![0, 0, 0]), &TableRegion(1, 3)); + assert_eq!(res.as_ref(), None); + let res = table.get_chunk_info(&ArrayIndices(vec![0, 0, 0]), &TableRegion(0, 3)); + assert_eq!(res.as_ref(), Some(&c1a)); + let res = table.get_chunk_info(&ArrayIndices(vec![0, 0, 0]), &TableRegion(0, 1)); + assert_eq!(res.as_ref(), Some(&c1a)); + + let res = table.get_chunk_info(&ArrayIndices(vec![0, 0, 1]), &TableRegion(2, 3)); + assert_eq!(res.as_ref(), None); + let res = table.get_chunk_info(&ArrayIndices(vec![0, 0, 1]), &TableRegion(0, 3)); + assert_eq!(res.as_ref(), Some(&c2a)); + let res = table.get_chunk_info(&ArrayIndices(vec![0, 0, 1]), &TableRegion(0, 2)); + assert_eq!(res.as_ref(), Some(&c2a)); + let res = table.get_chunk_info(&ArrayIndices(vec![0, 0, 1]), &TableRegion(1, 3)); + assert_eq!(res.as_ref(), Some(&c2a)); + + let res = table.get_chunk_info(&ArrayIndices(vec![1, 0, 1]), &TableRegion(4, 3)); + assert_eq!(res.as_ref(), None); + let res = table.get_chunk_info(&ArrayIndices(vec![1, 0, 1]), &TableRegion(0, 3)); + assert_eq!(res.as_ref(), Some(&c3a)); + let res = table.get_chunk_info(&ArrayIndices(vec![1, 0, 1]), &TableRegion(1, 3)); + assert_eq!(res.as_ref(), Some(&c3a)); + let res = table.get_chunk_info(&ArrayIndices(vec![1, 0, 1]), &TableRegion(2, 3)); + assert_eq!(res.as_ref(), Some(&c3a)); + + let res = table.get_chunk_info(&ArrayIndices(vec![0, 0, 0]), &TableRegion(3, 4)); + assert_eq!(res.as_ref(), Some(&c1b)); + + let res = table.get_chunk_info(&ArrayIndices(vec![0, 0, 0]), &TableRegion(4, 5)); + assert_eq!(res.as_ref(), Some(&c1c)); + } +}