1use std::cell::RefCell;
2use std::collections::{BTreeMap, HashMap};
3use std::future::Future;
4use std::path::{Path, PathBuf};
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use chrono::Utc;
8use runmat_builtins::{IntValue, IntegerStorage, ObjectInstance, Tensor, Value};
9use runmat_filesystem as fs;
10use runmat_filesystem::data_contract::{
11 DataChunkDescriptor, DataChunkUploadRequest, DataChunkUploadTarget,
12};
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15
16use crate::builtins::math::elementwise::integer_cast::IntegerTarget;
17use crate::{build_runtime_error, BuiltinResult, RuntimeError};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct DataManifest {
21 pub schema_version: u32,
22 pub format: String,
23 pub dataset_id: String,
24 pub name: Option<String>,
25 pub created_at: String,
26 pub updated_at: String,
27 pub arrays: BTreeMap<String, DataArrayMeta>,
28 pub attrs: BTreeMap<String, serde_json::Value>,
29 pub txn_sequence: u64,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct DataArrayMeta {
34 pub dtype: String,
35 pub shape: Vec<usize>,
36 pub chunk_shape: Vec<usize>,
37 #[serde(default = "default_array_order")]
38 pub order: String,
39 pub codec: String,
40 #[serde(default)]
41 pub chunk_index_path: Option<String>,
42 pub data_path: String,
43}
44
45fn default_array_order() -> String {
46 "column_major".to_string()
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct DataArrayPayload {
51 pub dtype: String,
52 pub shape: Vec<usize>,
53 pub values: DataArrayValues,
54}
55
56#[derive(Debug, Clone, PartialEq)]
62pub enum DataArrayValues {
63 F64(Vec<f64>),
64 I8(Vec<i8>),
65 I16(Vec<i16>),
66 I32(Vec<i32>),
67 I64(Vec<i64>),
68 U8(Vec<u8>),
69 U16(Vec<u16>),
70 U32(Vec<u32>),
71 U64(Vec<u64>),
72}
73
74#[derive(Serialize, Deserialize)]
75#[serde(tag = "encoding", content = "data", rename_all = "snake_case")]
76enum TaggedDataArrayValues {
77 F64(Vec<f64>),
78 I8(Vec<i8>),
79 I16(Vec<i16>),
80 I32(Vec<i32>),
81 I64(Vec<i64>),
82 U8(Vec<u8>),
83 U16(Vec<u16>),
84 U32(Vec<u32>),
85 U64(Vec<u64>),
86}
87
88#[derive(Serialize)]
89#[serde(tag = "encoding", content = "data", rename_all = "snake_case")]
90enum TaggedDataArrayValuesRef<'a> {
91 F64(&'a [f64]),
92 I8(&'a [i8]),
93 I16(&'a [i16]),
94 I32(&'a [i32]),
95 I64(&'a [i64]),
96 U8(&'a [u8]),
97 U16(&'a [u16]),
98 U32(&'a [u32]),
99 U64(&'a [u64]),
100}
101
102#[derive(Deserialize)]
103#[serde(untagged)]
104enum DataArrayValuesWire {
105 Tagged(TaggedDataArrayValues),
106 Legacy(Vec<f64>),
107}
108
109impl Serialize for DataArrayValues {
110 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
111 where
112 S: serde::Serializer,
113 {
114 let tagged = match self {
115 Self::F64(values) => TaggedDataArrayValuesRef::F64(values),
116 Self::I8(values) => TaggedDataArrayValuesRef::I8(values),
117 Self::I16(values) => TaggedDataArrayValuesRef::I16(values),
118 Self::I32(values) => TaggedDataArrayValuesRef::I32(values),
119 Self::I64(values) => TaggedDataArrayValuesRef::I64(values),
120 Self::U8(values) => TaggedDataArrayValuesRef::U8(values),
121 Self::U16(values) => TaggedDataArrayValuesRef::U16(values),
122 Self::U32(values) => TaggedDataArrayValuesRef::U32(values),
123 Self::U64(values) => TaggedDataArrayValuesRef::U64(values),
124 };
125 tagged.serialize(serializer)
126 }
127}
128
129impl<'de> Deserialize<'de> for DataArrayValues {
130 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
131 where
132 D: serde::Deserializer<'de>,
133 {
134 Ok(match DataArrayValuesWire::deserialize(deserializer)? {
135 DataArrayValuesWire::Legacy(values) => Self::F64(values),
136 DataArrayValuesWire::Tagged(tagged) => match tagged {
137 TaggedDataArrayValues::F64(values) => Self::F64(values),
138 TaggedDataArrayValues::I8(values) => Self::I8(values),
139 TaggedDataArrayValues::I16(values) => Self::I16(values),
140 TaggedDataArrayValues::I32(values) => Self::I32(values),
141 TaggedDataArrayValues::I64(values) => Self::I64(values),
142 TaggedDataArrayValues::U8(values) => Self::U8(values),
143 TaggedDataArrayValues::U16(values) => Self::U16(values),
144 TaggedDataArrayValues::U32(values) => Self::U32(values),
145 TaggedDataArrayValues::U64(values) => Self::U64(values),
146 },
147 })
148 }
149}
150
151impl DataArrayValues {
152 pub fn zeros(dtype: &str, len: usize) -> Self {
153 match integer_dtype(dtype) {
154 Some("int8") => Self::I8(vec![0; len]),
155 Some("int16") => Self::I16(vec![0; len]),
156 Some("int32") => Self::I32(vec![0; len]),
157 Some("int64") => Self::I64(vec![0; len]),
158 Some("uint8") => Self::U8(vec![0; len]),
159 Some("uint16") => Self::U16(vec![0; len]),
160 Some("uint32") => Self::U32(vec![0; len]),
161 Some("uint64") => Self::U64(vec![0; len]),
162 _ => Self::F64(vec![0.0; len]),
163 }
164 }
165
166 pub fn len(&self) -> usize {
167 match self {
168 Self::F64(values) => values.len(),
169 Self::I8(values) => values.len(),
170 Self::I16(values) => values.len(),
171 Self::I32(values) => values.len(),
172 Self::I64(values) => values.len(),
173 Self::U8(values) => values.len(),
174 Self::U16(values) => values.len(),
175 Self::U32(values) => values.len(),
176 Self::U64(values) => values.len(),
177 }
178 }
179
180 pub fn is_empty(&self) -> bool {
181 self.len() == 0
182 }
183
184 pub fn into_tensor(self, shape: Vec<usize>) -> Result<Tensor, String> {
185 match self {
186 Self::F64(values) => Tensor::new(values, shape),
187 Self::I8(values) => Tensor::new_integer(IntegerStorage::I8(values), shape),
188 Self::I16(values) => Tensor::new_integer(IntegerStorage::I16(values), shape),
189 Self::I32(values) => Tensor::new_integer(IntegerStorage::I32(values), shape),
190 Self::I64(values) => Tensor::new_integer(IntegerStorage::I64(values), shape),
191 Self::U8(values) => Tensor::new_integer(IntegerStorage::U8(values), shape),
192 Self::U16(values) => Tensor::new_integer(IntegerStorage::U16(values), shape),
193 Self::U32(values) => Tensor::new_integer(IntegerStorage::U32(values), shape),
194 Self::U64(values) => Tensor::new_integer(IntegerStorage::U64(values), shape),
195 }
196 }
197
198 pub fn to_f64_vec(&self) -> Vec<f64> {
199 match self {
200 Self::F64(values) => values.clone(),
201 Self::I8(values) => values.iter().map(|&value| value as f64).collect(),
202 Self::I16(values) => values.iter().map(|&value| value as f64).collect(),
203 Self::I32(values) => values.iter().map(|&value| value as f64).collect(),
204 Self::I64(values) => values.iter().map(|&value| value as f64).collect(),
205 Self::U8(values) => values.iter().map(|&value| value as f64).collect(),
206 Self::U16(values) => values.iter().map(|&value| value as f64).collect(),
207 Self::U32(values) => values.iter().map(|&value| value as f64).collect(),
208 Self::U64(values) => values.iter().map(|&value| value as f64).collect(),
209 }
210 }
211
212 pub fn preview_f64(&self, limit: usize) -> Vec<f64> {
219 match self {
220 Self::F64(values) => values.iter().take(limit).copied().collect(),
221 Self::I8(values) => values
222 .iter()
223 .take(limit)
224 .map(|&value| value as f64)
225 .collect(),
226 Self::I16(values) => values
227 .iter()
228 .take(limit)
229 .map(|&value| value as f64)
230 .collect(),
231 Self::I32(values) => values
232 .iter()
233 .take(limit)
234 .map(|&value| value as f64)
235 .collect(),
236 Self::I64(values) => values
237 .iter()
238 .take(limit)
239 .map(|&value| value as f64)
240 .collect(),
241 Self::U8(values) => values
242 .iter()
243 .take(limit)
244 .map(|&value| value as f64)
245 .collect(),
246 Self::U16(values) => values
247 .iter()
248 .take(limit)
249 .map(|&value| value as f64)
250 .collect(),
251 Self::U32(values) => values
252 .iter()
253 .take(limit)
254 .map(|&value| value as f64)
255 .collect(),
256 Self::U64(values) => values
257 .iter()
258 .take(limit)
259 .map(|&value| value as f64)
260 .collect(),
261 }
262 }
263
264 pub fn get(&self, index: usize) -> BuiltinResult<DataScalar> {
265 match self {
266 Self::F64(values) => values.get(index).copied().map(DataScalar::F64),
267 Self::I8(values) => values.get(index).copied().map(|v| DataScalar::I8(v)),
268 Self::I16(values) => values.get(index).copied().map(|v| DataScalar::I16(v)),
269 Self::I32(values) => values.get(index).copied().map(|v| DataScalar::I32(v)),
270 Self::I64(values) => values.get(index).copied().map(|v| DataScalar::I64(v)),
271 Self::U8(values) => values.get(index).copied().map(|v| DataScalar::U8(v)),
272 Self::U16(values) => values.get(index).copied().map(|v| DataScalar::U16(v)),
273 Self::U32(values) => values.get(index).copied().map(|v| DataScalar::U32(v)),
274 Self::U64(values) => values.get(index).copied().map(|v| DataScalar::U64(v)),
275 }
276 .ok_or_else(|| data_error(format!("data payload index {index} is out of bounds")))
277 }
278
279 pub fn push(&mut self, value: DataScalar) -> BuiltinResult<()> {
280 match (self, value) {
281 (Self::F64(values), DataScalar::F64(value)) => values.push(value),
282 (Self::I8(values), DataScalar::I8(value)) => values.push(value),
283 (Self::I16(values), DataScalar::I16(value)) => values.push(value),
284 (Self::I32(values), DataScalar::I32(value)) => values.push(value),
285 (Self::I64(values), DataScalar::I64(value)) => values.push(value),
286 (Self::U8(values), DataScalar::U8(value)) => values.push(value),
287 (Self::U16(values), DataScalar::U16(value)) => values.push(value),
288 (Self::U32(values), DataScalar::U32(value)) => values.push(value),
289 (Self::U64(values), DataScalar::U64(value)) => values.push(value),
290 _ => return Err(data_error("data payload storage class mismatch")),
291 }
292 Ok(())
293 }
294
295 pub fn set(&mut self, index: usize, value: DataScalar) -> BuiltinResult<()> {
296 match (self, value) {
297 (Self::F64(values), DataScalar::F64(value)) => set_at(values, index, value),
298 (Self::I8(values), DataScalar::I8(value)) => set_at(values, index, value),
299 (Self::I16(values), DataScalar::I16(value)) => set_at(values, index, value),
300 (Self::I32(values), DataScalar::I32(value)) => set_at(values, index, value),
301 (Self::I64(values), DataScalar::I64(value)) => set_at(values, index, value),
302 (Self::U8(values), DataScalar::U8(value)) => set_at(values, index, value),
303 (Self::U16(values), DataScalar::U16(value)) => set_at(values, index, value),
304 (Self::U32(values), DataScalar::U32(value)) => set_at(values, index, value),
305 (Self::U64(values), DataScalar::U64(value)) => set_at(values, index, value),
306 _ => return Err(data_error("data payload storage class mismatch")),
307 }?;
308 Ok(())
309 }
310
311 fn cast_to_dtype(self, dtype: &str) -> BuiltinResult<Self> {
312 let Some(target) = integer_target(dtype) else {
313 return Ok(Self::F64(self.to_f64_vec()));
314 };
315 let mut values = Vec::with_capacity(self.len());
316 for index in 0..self.len() {
317 let value = self.get(index)?;
318 values.push(match value {
319 DataScalar::F64(value) => target.cast_scalar(value),
320 value => target.cast_int(&value.to_int_value()),
321 });
322 }
323 Ok(Self::from_integer_storage(target.storage(values)))
324 }
325
326 fn from_integer_storage(storage: IntegerStorage) -> Self {
327 match storage {
328 IntegerStorage::I8(values) => Self::I8(values),
329 IntegerStorage::I16(values) => Self::I16(values),
330 IntegerStorage::I32(values) => Self::I32(values),
331 IntegerStorage::I64(values) => Self::I64(values),
332 IntegerStorage::U8(values) => Self::U8(values),
333 IntegerStorage::U16(values) => Self::U16(values),
334 IntegerStorage::U32(values) => Self::U32(values),
335 IntegerStorage::U64(values) => Self::U64(values),
336 }
337 }
338}
339
340#[derive(Debug, Clone, Copy)]
341pub enum DataScalar {
342 F64(f64),
343 I8(i8),
344 I16(i16),
345 I32(i32),
346 I64(i64),
347 U8(u8),
348 U16(u16),
349 U32(u32),
350 U64(u64),
351}
352
353impl DataScalar {
354 fn to_int_value(self) -> IntValue {
355 match self {
356 Self::F64(value) => IntValue::I64(value as i64),
357 Self::I8(value) => IntValue::I8(value),
358 Self::I16(value) => IntValue::I16(value),
359 Self::I32(value) => IntValue::I32(value),
360 Self::I64(value) => IntValue::I64(value),
361 Self::U8(value) => IntValue::U8(value),
362 Self::U16(value) => IntValue::U16(value),
363 Self::U32(value) => IntValue::U32(value),
364 Self::U64(value) => IntValue::U64(value),
365 }
366 }
367}
368
369fn set_at<T>(values: &mut [T], index: usize, value: T) -> BuiltinResult<()> {
370 let target = values
371 .get_mut(index)
372 .ok_or_else(|| data_error(format!("data payload index {index} is out of bounds")))?;
373 *target = value;
374 Ok(())
375}
376
377fn integer_dtype(dtype: &str) -> Option<&'static str> {
378 match dtype.to_ascii_lowercase().as_str() {
379 "int8" => Some("int8"),
380 "int16" => Some("int16"),
381 "int32" => Some("int32"),
382 "int64" => Some("int64"),
383 "uint8" => Some("uint8"),
384 "uint16" => Some("uint16"),
385 "uint32" => Some("uint32"),
386 "uint64" => Some("uint64"),
387 _ => None,
388 }
389}
390
391fn integer_target(dtype: &str) -> Option<IntegerTarget> {
392 match integer_dtype(dtype) {
393 Some("int8") => Some(IntegerTarget::I8),
394 Some("int16") => Some(IntegerTarget::I16),
395 Some("int32") => Some(IntegerTarget::I32),
396 Some("int64") => Some(IntegerTarget::I64),
397 Some("uint8") => Some(IntegerTarget::U8),
398 Some("uint16") => Some(IntegerTarget::U16),
399 Some("uint32") => Some(IntegerTarget::U32),
400 Some("uint64") => Some(IntegerTarget::U64),
401 _ => None,
402 }
403}
404
405impl DataArrayPayload {
406 pub fn zeros(dtype: String, shape: Vec<usize>) -> Self {
407 let values = DataArrayValues::zeros(&dtype, shape.iter().copied().product());
408 Self {
409 dtype,
410 shape,
411 values,
412 }
413 }
414
415 pub fn from_value(dtype: String, value: &Value) -> BuiltinResult<Self> {
416 let (shape, values) = data_values_from_value(value)?;
417 Ok(Self {
418 dtype: dtype.clone(),
419 shape,
420 values: values.cast_to_dtype(&dtype)?,
421 })
422 }
423
424 pub fn filled(dtype: String, shape: Vec<usize>, value: &Value) -> BuiltinResult<Self> {
425 let scalar = Self::from_value(dtype.clone(), value)?;
426 if scalar.values.len() != 1 {
427 return Err(data_error("expected numeric scalar"));
428 }
429 let scalar = scalar.values.get(0)?;
430 let len = shape.iter().copied().product();
431 let mut values = DataArrayValues::zeros(&dtype, len);
432 for index in 0..len {
433 values.set(index, scalar)?;
434 }
435 Ok(Self {
436 dtype,
437 shape,
438 values,
439 })
440 }
441
442 pub fn normalize_for_dtype(mut self, dtype: &str) -> BuiltinResult<Self> {
443 self.values = self.values.cast_to_dtype(dtype)?;
444 self.dtype = dtype.to_string();
445 Ok(self)
446 }
447
448 pub fn into_value(self) -> BuiltinResult<Value> {
449 self.values
450 .into_tensor(self.shape)
451 .map(Value::Tensor)
452 .map_err(|err| data_error(format!("invalid data payload: {err}")))
453 }
454}
455
456fn data_values_from_value(value: &Value) -> BuiltinResult<(Vec<usize>, DataArrayValues)> {
457 match value {
458 Value::Tensor(tensor) => {
459 let values = match tensor.integer_storage() {
460 Some(IntegerStorage::I8(values)) => DataArrayValues::I8(values.clone()),
461 Some(IntegerStorage::I16(values)) => DataArrayValues::I16(values.clone()),
462 Some(IntegerStorage::I32(values)) => DataArrayValues::I32(values.clone()),
463 Some(IntegerStorage::I64(values)) => DataArrayValues::I64(values.clone()),
464 Some(IntegerStorage::U8(values)) => DataArrayValues::U8(values.clone()),
465 Some(IntegerStorage::U16(values)) => DataArrayValues::U16(values.clone()),
466 Some(IntegerStorage::U32(values)) => DataArrayValues::U32(values.clone()),
467 Some(IntegerStorage::U64(values)) => DataArrayValues::U64(values.clone()),
468 None => DataArrayValues::F64(tensor.data.clone()),
469 };
470 Ok((tensor.shape.clone(), values))
471 }
472 Value::Num(value) => Ok((vec![1, 1], DataArrayValues::F64(vec![*value]))),
473 Value::Int(IntValue::I8(value)) => Ok((vec![1, 1], DataArrayValues::I8(vec![*value]))),
474 Value::Int(IntValue::I16(value)) => Ok((vec![1, 1], DataArrayValues::I16(vec![*value]))),
475 Value::Int(IntValue::I32(value)) => Ok((vec![1, 1], DataArrayValues::I32(vec![*value]))),
476 Value::Int(IntValue::I64(value)) => Ok((vec![1, 1], DataArrayValues::I64(vec![*value]))),
477 Value::Int(IntValue::U8(value)) => Ok((vec![1, 1], DataArrayValues::U8(vec![*value]))),
478 Value::Int(IntValue::U16(value)) => Ok((vec![1, 1], DataArrayValues::U16(vec![*value]))),
479 Value::Int(IntValue::U32(value)) => Ok((vec![1, 1], DataArrayValues::U32(vec![*value]))),
480 Value::Int(IntValue::U64(value)) => Ok((vec![1, 1], DataArrayValues::U64(vec![*value]))),
481 _ => Err(data_error(
482 "DataArray.write supports tensor or numeric scalar values",
483 )),
484 }
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct DataChunkIndex {
489 pub schema_version: u32,
490 pub array: String,
491 pub chunks: Vec<DataChunkIndexEntry>,
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize)]
495pub struct DataChunkIndexEntry {
496 pub key: String,
497 pub object_id: String,
498 pub hash: String,
499 pub bytes_raw: u64,
500 pub bytes_stored: u64,
501 #[serde(default)]
502 pub coords: Vec<usize>,
503 #[serde(default)]
504 pub shape: Vec<usize>,
505 pub data_path: String,
506}
507
508#[derive(Debug, Clone)]
509pub struct DataSchema {
510 pub arrays: BTreeMap<String, DataArrayMeta>,
511}
512
513#[derive(Debug, Clone)]
514pub struct PendingTxn {
515 pub dataset_path: String,
516 pub base_sequence: u64,
517 pub writes: Vec<PendingWrite>,
518 pub resizes: Vec<PendingResize>,
519 pub fills: Vec<PendingFill>,
520 pub create_arrays: Vec<PendingCreateArray>,
521 pub delete_arrays: Vec<String>,
522 pub attrs: BTreeMap<String, Value>,
523 pub status: TxnStatus,
524}
525
526#[derive(Debug, Clone)]
527pub struct PendingWrite {
528 pub array: String,
529 pub slice_spec: Option<Value>,
530 pub value: Value,
531}
532
533#[derive(Debug, Clone)]
534pub struct PendingResize {
535 pub array: String,
536 pub shape: Vec<usize>,
537}
538
539#[derive(Debug, Clone)]
540pub struct PendingFill {
541 pub array: String,
542 pub slice_spec: Option<Value>,
543 pub value: Value,
544}
545
546#[derive(Debug, Clone)]
547pub struct PendingCreateArray {
548 pub array: String,
549 pub meta: DataArrayMeta,
550}
551
552#[derive(Debug, Clone, PartialEq, Eq)]
553pub enum TxnStatus {
554 Open,
555 Committed,
556 Aborted,
557}
558
559thread_local! {
560 static FALLBACK_TX_REGISTRY: RefCell<HashMap<String, PendingTxn>> = RefCell::new(HashMap::new());
561}
562
563#[cfg(not(target_arch = "wasm32"))]
564tokio::task_local! {
565 static TASK_TX_REGISTRY: RefCell<HashMap<String, PendingTxn>>;
566}
567
568pub async fn with_tx_registry_scope<F>(future: F) -> F::Output
569where
570 F: Future,
571{
572 #[cfg(not(target_arch = "wasm32"))]
573 {
574 if TASK_TX_REGISTRY.try_with(|_| ()).is_ok() {
575 future.await
576 } else {
577 TASK_TX_REGISTRY
578 .scope(RefCell::new(HashMap::new()), future)
579 .await
580 }
581 }
582 #[cfg(target_arch = "wasm32")]
583 {
584 future.await
585 }
586}
587
588fn with_tx_registry<T>(f: impl FnOnce(&mut HashMap<String, PendingTxn>) -> T) -> BuiltinResult<T> {
589 #[cfg(not(target_arch = "wasm32"))]
590 {
591 if TASK_TX_REGISTRY.try_with(|_| ()).is_ok() {
592 return TASK_TX_REGISTRY.with(|registry| {
593 let mut registry = registry.try_borrow_mut().map_err(|_| {
594 data_error("data transaction registry is already mutably borrowed")
595 })?;
596 Ok(f(&mut registry))
597 });
598 }
599 }
600
601 FALLBACK_TX_REGISTRY.with(|registry| {
602 let mut registry = registry
603 .try_borrow_mut()
604 .map_err(|_| data_error("data transaction registry is already mutably borrowed"))?;
605 Ok(f(&mut registry))
606 })
607}
608
609pub fn data_error(message: impl Into<String>) -> RuntimeError {
610 build_runtime_error(message)
611 .with_identifier("RUNMAT:Data:Error")
612 .with_builtin("data")
613 .build()
614}
615
616fn data_error_with_identifier(
617 message: impl Into<String>,
618 identifier: &'static str,
619) -> RuntimeError {
620 build_runtime_error(message)
621 .with_identifier(identifier)
622 .with_builtin("data")
623 .build()
624}
625
626const DATA_MANIFEST_CONFLICT_IDENTIFIER: &str = "RunMat:data:ManifestConflict";
627const DATA_TRANSACTION_NOT_FOUND_IDENTIFIER: &str = "RunMat:data:TransactionNotFound";
628
629pub fn parse_string(value: &Value, context: &str) -> BuiltinResult<String> {
630 match value {
631 Value::String(s) => Ok(s.clone()),
632 Value::CharArray(chars) => chars
633 .row_string()
634 .ok_or_else(|| data_error(format!("{context}: expected character row vector"))),
635 _ => Err(data_error(format!("{context}: expected string value"))),
636 }
637}
638
639pub fn dataset_root(path: &str) -> PathBuf {
640 PathBuf::from(path)
641}
642
643pub fn manifest_path(root: &Path) -> PathBuf {
644 root.join("manifest.json")
645}
646
647pub fn arrays_root(root: &Path) -> PathBuf {
648 root.join("arrays")
649}
650
651pub async fn write_manifest_async(root: &Path, manifest: &DataManifest) -> BuiltinResult<()> {
652 fs::create_dir_all_async(root).await.map_err(|err| {
653 data_error(format!(
654 "failed to create dataset root '{}': {err}",
655 root.display()
656 ))
657 })?;
658 let path = manifest_path(root);
659 let bytes = serde_json::to_vec_pretty(manifest)
660 .map_err(|err| data_error(format!("failed to encode manifest json: {err}")))?;
661 fs::write_async(&path, &bytes).await.map_err(|err| {
662 data_error(format!(
663 "failed to write manifest '{}': {err}",
664 path.display()
665 ))
666 })?;
667 Ok(())
668}
669
670pub async fn read_manifest_async(root: &Path) -> BuiltinResult<DataManifest> {
671 let path = manifest_path(root);
672 let bytes = fs::read_async(&path).await.map_err(|err| {
673 data_error(format!(
674 "failed to read manifest '{}': {err}",
675 path.display()
676 ))
677 })?;
678 let manifest = serde_json::from_slice::<DataManifest>(&bytes).map_err(|err| {
679 data_error(format!(
680 "failed to parse manifest '{}': {err}",
681 path.display()
682 ))
683 })?;
684 Ok(manifest)
685}
686
687pub async fn write_array_payload_async(
688 root: &Path,
689 array: &str,
690 payload: &DataArrayPayload,
691 chunk_shape: &[usize],
692) -> BuiltinResult<(PathBuf, PathBuf)> {
693 let array_dir = arrays_root(root).join(array);
694 fs::create_dir_all_async(&array_dir).await.map_err(|err| {
695 data_error(format!(
696 "failed to create array dir '{}': {err}",
697 array_dir.display()
698 ))
699 })?;
700 let payload_path = array_dir.join("data.f64.json");
701 let bytes = serde_json::to_vec(payload)
702 .map_err(|err| data_error(format!("failed to encode array payload json: {err}")))?;
703 fs::write_async(&payload_path, &bytes)
704 .await
705 .map_err(|err| {
706 data_error(format!(
707 "failed to write payload '{}': {err}",
708 payload_path.display()
709 ))
710 })?;
711
712 let chunk_dir = array_dir.join("chunks");
713 fs::create_dir_all_async(&chunk_dir).await.map_err(|err| {
714 data_error(format!(
715 "failed to create chunk dir '{}': {err}",
716 chunk_dir.display()
717 ))
718 })?;
719
720 let mut index = DataChunkIndex {
721 schema_version: 1,
722 array: array.to_string(),
723 chunks: Vec::new(),
724 };
725 let mut upload_chunks = Vec::new();
726 let grid_shape = chunk_grid_shape(&payload.shape, chunk_shape);
727 let mut coords = vec![0usize; payload.shape.len()];
728 loop {
729 let chunk_start = chunk_start_for_coords(&coords, chunk_shape);
730 let chunk_extent = chunk_extent_for_start(&chunk_start, chunk_shape, &payload.shape);
731 let chunk_payload = DataArrayPayload {
732 dtype: payload.dtype.clone(),
733 shape: chunk_extent.clone(),
734 values: collect_chunk_values(payload, &chunk_start, &chunk_extent)?,
735 };
736 let key = chunk_key(&coords);
737 let object_id = format!("obj_{}", key.replace('.', "_"));
738 let chunk_bytes = serde_json::to_vec(&chunk_payload)
739 .map_err(|err| data_error(format!("failed to encode chunk payload: {err}")))?;
740 let data_path = chunk_dir.join(format!("{object_id}.json"));
741 fs::write_async(&data_path, &chunk_bytes)
742 .await
743 .map_err(|err| {
744 data_error(format!(
745 "failed to write chunk '{}': {err}",
746 data_path.display()
747 ))
748 })?;
749 let hash = sha256_hex(&chunk_bytes);
750 let rel_chunk_path = data_path
751 .strip_prefix(root)
752 .map_err(|err| data_error(format!("failed to compute chunk relative path: {err}")))?
753 .to_string_lossy()
754 .to_string();
755 index.chunks.push(DataChunkIndexEntry {
756 key: key.clone(),
757 object_id: object_id.clone(),
758 hash: hash.clone(),
759 bytes_raw: chunk_bytes.len() as u64,
760 bytes_stored: chunk_bytes.len() as u64,
761 coords: coords.clone(),
762 shape: chunk_extent,
763 data_path: rel_chunk_path,
764 });
765 upload_chunks.push((
766 DataChunkDescriptor {
767 key,
768 object_id,
769 hash,
770 bytes_raw: chunk_bytes.len() as u64,
771 bytes_stored: chunk_bytes.len() as u64,
772 },
773 chunk_bytes,
774 ));
775 if !advance_index(&mut coords, &grid_shape) {
776 break;
777 }
778 }
779
780 maybe_upload_chunks_async(root, array, upload_chunks).await?;
781
782 tracing::info!(
783 target: "runmat.data",
784 dataset = %root.display(),
785 array = array,
786 chunks = index.chunks.len(),
787 payload_bytes = bytes.len(),
788 "data chunk write planned"
789 );
790
791 let chunk_index_path = chunk_dir.join("index.json");
792 let chunk_index_bytes = serde_json::to_vec(&index)
793 .map_err(|err| data_error(format!("failed to encode chunk index json: {err}")))?;
794 fs::write_async(&chunk_index_path, &chunk_index_bytes)
795 .await
796 .map_err(|err| {
797 data_error(format!(
798 "failed to write chunk index '{}': {err}",
799 chunk_index_path.display()
800 ))
801 })?;
802 Ok((payload_path, chunk_index_path))
803}
804
805pub async fn read_array_payload_async(
806 root: &Path,
807 meta: &DataArrayMeta,
808) -> BuiltinResult<DataArrayPayload> {
809 if let Some(index_path) = &meta.chunk_index_path {
810 let path = root.join(index_path);
811 if fs::metadata_async(&path).await.is_ok() {
812 return read_array_payload_chunked_async(root, meta, &path).await;
813 }
814 }
815 let payload_path = root.join(&meta.data_path);
816 let bytes = fs::read_async(&payload_path).await.map_err(|err| {
817 data_error(format!(
818 "failed to read payload '{}': {err}",
819 payload_path.display()
820 ))
821 })?;
822 serde_json::from_slice::<DataArrayPayload>(&bytes)
823 .map_err(|err| {
824 data_error(format!(
825 "failed to parse payload '{}': {err}",
826 payload_path.display()
827 ))
828 })?
829 .normalize_for_dtype(&meta.dtype)
830}
831
832pub async fn read_array_slice_payload_async(
833 root: &Path,
834 meta: &DataArrayMeta,
835 start: &[usize],
836 shape: &[usize],
837) -> BuiltinResult<DataArrayPayload> {
838 let (slice_start, slice_shape) = normalize_slice_bounds(&meta.shape, start, shape)?;
839 if let Some(index_path) = &meta.chunk_index_path {
840 let path = root.join(index_path);
841 if fs::metadata_async(&path).await.is_ok() {
842 return read_array_payload_chunked_slice_async(
843 root,
844 meta,
845 &path,
846 &slice_start,
847 &slice_shape,
848 )
849 .await;
850 }
851 }
852 let full = read_array_payload_async(root, meta).await?;
853 extract_slice_payload(&full, &slice_start, &slice_shape)
854}
855
856async fn read_array_payload_chunked_slice_async(
857 root: &Path,
858 meta: &DataArrayMeta,
859 index_path: &Path,
860 slice_start: &[usize],
861 slice_shape: &[usize],
862) -> BuiltinResult<DataArrayPayload> {
863 let bytes = fs::read_async(index_path).await.map_err(|err| {
864 data_error(format!(
865 "failed to read chunk index '{}': {err}",
866 index_path.display()
867 ))
868 })?;
869 let index: DataChunkIndex = serde_json::from_slice(&bytes).map_err(|err| {
870 data_error(format!(
871 "failed to parse chunk index '{}': {err}",
872 index_path.display()
873 ))
874 })?;
875
876 let mut values = DataArrayValues::zeros(&meta.dtype, slice_shape.iter().copied().product());
877 for chunk in index.chunks {
878 let coords = chunk_coords_from_entry(&chunk, meta.shape.len())?;
879 let chunk_start = chunk_start_for_coords(&coords, &meta.chunk_shape);
880 let chunk_extent = if chunk.shape.is_empty() {
881 chunk_extent_for_start(&chunk_start, &meta.chunk_shape, &meta.shape)
882 } else {
883 chunk.shape.clone()
884 };
885 if !chunk_intersects_slice(&chunk_start, &chunk_extent, slice_start, slice_shape) {
886 continue;
887 }
888
889 let chunk_path = root.join(&chunk.data_path);
890 let bytes = fs::read_async(&chunk_path).await.map_err(|err| {
891 data_error(format!(
892 "failed to read chunk payload '{}': {err}",
893 chunk_path.display()
894 ))
895 })?;
896 let payload: DataArrayPayload = serde_json::from_slice::<DataArrayPayload>(&bytes)
897 .map_err(|err| {
898 data_error(format!(
899 "failed to parse chunk payload '{}': {err}",
900 chunk_path.display()
901 ))
902 })?
903 .normalize_for_dtype(&meta.dtype)?;
904 if payload.shape != chunk_extent {
905 return Err(data_error(format!(
906 "chunk payload shape mismatch for key '{}': {:?} != {:?}",
907 chunk.key, payload.shape, chunk_extent
908 )));
909 }
910
911 let mut local = vec![0usize; chunk_extent.len()];
912 loop {
913 let mut global = Vec::with_capacity(chunk_extent.len());
914 for dim in 0..chunk_extent.len() {
915 global.push(chunk_start[dim] + local[dim]);
916 }
917 if coordinate_in_slice(&global, slice_start, slice_shape) {
918 let src_linear = linear_index_column_major(&local, &chunk_extent)?;
919 let mut dst = Vec::with_capacity(slice_shape.len());
920 for dim in 0..slice_shape.len() {
921 dst.push(global[dim].saturating_sub(slice_start[dim]));
922 }
923 let dst_linear = linear_index_column_major(&dst, slice_shape)?;
924 values.set(dst_linear, payload.values.get(src_linear)?)?;
925 }
926 if !advance_index(&mut local, &chunk_extent) {
927 break;
928 }
929 }
930 }
931
932 Ok(DataArrayPayload {
933 dtype: meta.dtype.clone(),
934 shape: slice_shape.to_vec(),
935 values,
936 })
937}
938
939async fn read_array_payload_chunked_async(
940 root: &Path,
941 meta: &DataArrayMeta,
942 index_path: &Path,
943) -> BuiltinResult<DataArrayPayload> {
944 let bytes = fs::read_async(index_path).await.map_err(|err| {
945 data_error(format!(
946 "failed to read chunk index '{}': {err}",
947 index_path.display()
948 ))
949 })?;
950 let index: DataChunkIndex = serde_json::from_slice(&bytes).map_err(|err| {
951 data_error(format!(
952 "failed to parse chunk index '{}': {err}",
953 index_path.display()
954 ))
955 })?;
956 let mut values = DataArrayValues::zeros(&meta.dtype, meta.shape.iter().copied().product());
957 for chunk in index.chunks {
958 let chunk_path = root.join(&chunk.data_path);
959 let bytes = fs::read_async(&chunk_path).await.map_err(|err| {
960 data_error(format!(
961 "failed to read chunk payload '{}': {err}",
962 chunk_path.display()
963 ))
964 })?;
965 let payload: DataArrayPayload = serde_json::from_slice::<DataArrayPayload>(&bytes)
966 .map_err(|err| {
967 data_error(format!(
968 "failed to parse chunk payload '{}': {err}",
969 chunk_path.display()
970 ))
971 })?
972 .normalize_for_dtype(&meta.dtype)?;
973 let coords = chunk_coords_from_entry(&chunk, meta.shape.len())?;
974 let chunk_start = chunk_start_for_coords(&coords, &meta.chunk_shape);
975 let chunk_extent = if chunk.shape.is_empty() {
976 chunk_extent_for_start(&chunk_start, &meta.chunk_shape, &meta.shape)
977 } else {
978 chunk.shape.clone()
979 };
980 if payload.shape != chunk_extent {
981 return Err(data_error(format!(
982 "chunk payload shape mismatch for key '{}': {:?} != {:?}",
983 chunk.key, payload.shape, chunk_extent
984 )));
985 }
986 let mut local = vec![0usize; chunk_extent.len()];
987 loop {
988 let mut global = Vec::with_capacity(chunk_extent.len());
989 for dim in 0..chunk_extent.len() {
990 global.push(chunk_start[dim] + local[dim]);
991 }
992 let src_linear = linear_index_column_major(&local, &chunk_extent)?;
993 let dst_linear = linear_index_column_major(&global, &meta.shape)?;
994 values.set(dst_linear, payload.values.get(src_linear)?)?;
995 if !advance_index(&mut local, &chunk_extent) {
996 break;
997 }
998 }
999 }
1000 Ok(DataArrayPayload {
1001 dtype: meta.dtype.clone(),
1002 shape: meta.shape.clone(),
1003 values,
1004 })
1005}
1006
1007async fn maybe_upload_chunks_async(
1008 root: &Path,
1009 array: &str,
1010 chunks: Vec<(DataChunkDescriptor, Vec<u8>)>,
1011) -> BuiltinResult<()> {
1012 if chunks.is_empty() {
1013 return Ok(());
1014 }
1015 let request = DataChunkUploadRequest {
1016 dataset_path: root.to_string_lossy().to_string(),
1017 array: array.to_string(),
1018 chunks: chunks.iter().map(|(desc, _)| desc.clone()).collect(),
1019 };
1020 let targets = match fs::data_chunk_upload_targets_async(&request).await {
1021 Ok(targets) => targets,
1022 Err(err) if err.kind() == std::io::ErrorKind::Unsupported => return Ok(()),
1023 Err(err) => {
1024 return Err(data_error(format!(
1025 "failed to request data chunk upload targets: {err}"
1026 )))
1027 }
1028 };
1029 for (descriptor, bytes) in chunks {
1030 let target = find_chunk_target(&targets, &descriptor.key)?;
1031 fs::data_upload_chunk_async(target, &bytes)
1032 .await
1033 .map_err(|err| {
1034 data_error(format!(
1035 "failed to upload chunk '{}': {err}",
1036 descriptor.key
1037 ))
1038 })?;
1039 tracing::info!(
1040 target: "runmat.data",
1041 dataset = %root.display(),
1042 array = array,
1043 chunk_key = descriptor.key,
1044 bytes = bytes.len(),
1045 "data chunk uploaded"
1046 );
1047 }
1048 Ok(())
1049}
1050
1051fn find_chunk_target<'a>(
1052 targets: &'a [DataChunkUploadTarget],
1053 key: &str,
1054) -> BuiltinResult<&'a DataChunkUploadTarget> {
1055 targets
1056 .iter()
1057 .find(|target| target.key == key)
1058 .ok_or_else(|| data_error(format!("missing upload target for chunk '{key}'")))
1059}
1060
1061pub fn sha256_hex(bytes: &[u8]) -> String {
1062 let mut hasher = Sha256::new();
1063 hasher.update(bytes);
1064 let digest = hasher.finalize();
1065 format!("sha256:{:x}", digest)
1066}
1067
1068fn chunk_key(coords: &[usize]) -> String {
1069 coords
1070 .iter()
1071 .map(|v| v.to_string())
1072 .collect::<Vec<_>>()
1073 .join(".")
1074}
1075
1076fn chunk_grid_shape(shape: &[usize], chunk_shape: &[usize]) -> Vec<usize> {
1077 shape
1078 .iter()
1079 .enumerate()
1080 .map(|(idx, extent)| {
1081 let chunk = chunk_shape.get(idx).copied().unwrap_or(1).max(1);
1082 extent.div_ceil(chunk)
1083 })
1084 .collect()
1085}
1086
1087fn chunk_start_for_coords(coords: &[usize], chunk_shape: &[usize]) -> Vec<usize> {
1088 coords
1089 .iter()
1090 .enumerate()
1091 .map(|(idx, coord)| coord * chunk_shape.get(idx).copied().unwrap_or(1).max(1))
1092 .collect()
1093}
1094
1095fn chunk_extent_for_start(
1096 start: &[usize],
1097 chunk_shape: &[usize],
1098 full_shape: &[usize],
1099) -> Vec<usize> {
1100 start
1101 .iter()
1102 .enumerate()
1103 .map(|(idx, start)| {
1104 let chunk = chunk_shape.get(idx).copied().unwrap_or(1).max(1);
1105 let end = (*start + chunk).min(full_shape[idx]);
1106 end.saturating_sub(*start)
1107 })
1108 .collect()
1109}
1110
1111fn collect_chunk_values(
1112 payload: &DataArrayPayload,
1113 chunk_start: &[usize],
1114 chunk_extent: &[usize],
1115) -> BuiltinResult<DataArrayValues> {
1116 let mut local = vec![0usize; chunk_extent.len()];
1117 let mut values = DataArrayValues::zeros(&payload.dtype, 0);
1118 loop {
1119 let mut global = Vec::with_capacity(chunk_extent.len());
1120 for dim in 0..chunk_extent.len() {
1121 global.push(chunk_start[dim] + local[dim]);
1122 }
1123 let linear = linear_index_column_major(&global, &payload.shape)?;
1124 values.push(payload.values.get(linear)?)?;
1125 if !advance_index(&mut local, chunk_extent) {
1126 break;
1127 }
1128 }
1129 Ok(values)
1130}
1131
1132fn chunk_coords_from_entry(entry: &DataChunkIndexEntry, rank: usize) -> BuiltinResult<Vec<usize>> {
1133 if !entry.coords.is_empty() {
1134 if entry.coords.len() != rank {
1135 return Err(data_error(format!(
1136 "chunk coords rank mismatch for key '{}': expected {rank}, got {}",
1137 entry.key,
1138 entry.coords.len()
1139 )));
1140 }
1141 return Ok(entry.coords.clone());
1142 }
1143 let coords = entry
1144 .key
1145 .split('.')
1146 .map(|part| {
1147 part.parse::<usize>()
1148 .map_err(|_| data_error(format!("invalid chunk key '{}'", entry.key)))
1149 })
1150 .collect::<BuiltinResult<Vec<_>>>()?;
1151 if coords.len() != rank {
1152 return Err(data_error(format!(
1153 "chunk key rank mismatch for key '{}': expected {rank}, got {}",
1154 entry.key,
1155 coords.len()
1156 )));
1157 }
1158 Ok(coords)
1159}
1160
1161fn normalize_slice_bounds(
1162 full_shape: &[usize],
1163 start: &[usize],
1164 shape: &[usize],
1165) -> BuiltinResult<(Vec<usize>, Vec<usize>)> {
1166 if full_shape.is_empty() {
1167 return Ok((Vec::new(), Vec::new()));
1168 }
1169 let mut normalized_start = Vec::with_capacity(full_shape.len());
1170 let mut normalized_shape = Vec::with_capacity(full_shape.len());
1171 for (axis, axis_len) in full_shape.iter().copied().enumerate() {
1172 if axis_len == 0 {
1173 return Err(data_error("slice axis length must be greater than zero"));
1174 }
1175 let requested_start = start.get(axis).copied().unwrap_or(0);
1176 let clamped_start = requested_start.min(axis_len.saturating_sub(1));
1177 let requested_span = shape.get(axis).copied().unwrap_or(axis_len);
1178 let clamped_span = requested_span
1179 .max(1)
1180 .min(axis_len.saturating_sub(clamped_start));
1181 normalized_start.push(clamped_start);
1182 normalized_shape.push(clamped_span);
1183 }
1184 Ok((normalized_start, normalized_shape))
1185}
1186
1187fn coordinate_in_slice(global: &[usize], slice_start: &[usize], slice_shape: &[usize]) -> bool {
1188 for dim in 0..slice_shape.len() {
1189 let start = slice_start[dim];
1190 let end = start.saturating_add(slice_shape[dim]);
1191 let value = global[dim];
1192 if value < start || value >= end {
1193 return false;
1194 }
1195 }
1196 true
1197}
1198
1199fn chunk_intersects_slice(
1200 chunk_start: &[usize],
1201 chunk_extent: &[usize],
1202 slice_start: &[usize],
1203 slice_shape: &[usize],
1204) -> bool {
1205 for dim in 0..slice_shape.len() {
1206 let chunk_lo = chunk_start[dim];
1207 let chunk_hi = chunk_lo.saturating_add(chunk_extent[dim]);
1208 let slice_lo = slice_start[dim];
1209 let slice_hi = slice_lo.saturating_add(slice_shape[dim]);
1210 if chunk_hi <= slice_lo || slice_hi <= chunk_lo {
1211 return false;
1212 }
1213 }
1214 true
1215}
1216
1217fn extract_slice_payload(
1218 payload: &DataArrayPayload,
1219 start: &[usize],
1220 shape: &[usize],
1221) -> BuiltinResult<DataArrayPayload> {
1222 let mut values = DataArrayValues::zeros(&payload.dtype, 0);
1223 if shape.is_empty() {
1224 return Ok(DataArrayPayload {
1225 dtype: payload.dtype.clone(),
1226 shape: Vec::new(),
1227 values,
1228 });
1229 }
1230 let mut local = vec![0usize; shape.len()];
1231 loop {
1232 let mut global = Vec::with_capacity(shape.len());
1233 for dim in 0..shape.len() {
1234 global.push(start[dim] + local[dim]);
1235 }
1236 let linear = linear_index_column_major(&global, &payload.shape)?;
1237 values.push(payload.values.get(linear)?)?;
1238 if !advance_index(&mut local, shape) {
1239 break;
1240 }
1241 }
1242 Ok(DataArrayPayload {
1243 dtype: payload.dtype.clone(),
1244 shape: shape.to_vec(),
1245 values,
1246 })
1247}
1248
1249fn linear_index_column_major(index: &[usize], shape: &[usize]) -> BuiltinResult<usize> {
1250 if index.len() != shape.len() {
1251 return Err(data_error("chunk index rank mismatch"));
1252 }
1253 let mut stride = 1usize;
1254 let mut linear = 0usize;
1255 for (idx, extent) in index.iter().zip(shape.iter()) {
1256 if *idx >= *extent {
1257 return Err(data_error("chunk index out of bounds"));
1258 }
1259 linear += idx * stride;
1260 stride = stride.saturating_mul(*extent);
1261 }
1262 Ok(linear)
1263}
1264
1265fn advance_index(index: &mut [usize], shape: &[usize]) -> bool {
1266 if shape.is_empty() {
1267 return false;
1268 }
1269 for dim in 0..shape.len() {
1270 index[dim] += 1;
1271 if index[dim] < shape[dim] {
1272 return true;
1273 }
1274 index[dim] = 0;
1275 }
1276 false
1277}
1278
1279pub fn parse_schema(schema: &Value) -> BuiltinResult<DataSchema> {
1280 let Value::Struct(schema_struct) = schema else {
1281 return Err(data_error("data.create: schema must be a struct"));
1282 };
1283 let arrays_value = schema_struct
1284 .fields
1285 .get("arrays")
1286 .ok_or_else(|| data_error("data.create: schema missing 'arrays' field"))?;
1287 let Value::Struct(arrays_struct) = arrays_value else {
1288 return Err(data_error("data.create: schema.arrays must be a struct"));
1289 };
1290
1291 let mut arrays = BTreeMap::new();
1292 for (name, meta_value) in &arrays_struct.fields {
1293 let Value::Struct(meta_struct) = meta_value else {
1294 return Err(data_error(format!(
1295 "data.create: schema.arrays.{name} must be a struct"
1296 )));
1297 };
1298 let dtype = meta_struct
1299 .fields
1300 .get("dtype")
1301 .map(|v| parse_string(v, "data.create schema dtype"))
1302 .transpose()?
1303 .unwrap_or_else(|| "f64".to_string());
1304 let shape = meta_struct
1305 .fields
1306 .get("shape")
1307 .map(parse_usize_vector)
1308 .transpose()?
1309 .unwrap_or_else(|| vec![0, 0]);
1310 let chunk_shape = meta_struct
1311 .fields
1312 .get("chunk")
1313 .map(parse_usize_vector)
1314 .transpose()?
1315 .unwrap_or_else(|| default_chunk_shape(&shape));
1316 let codec = meta_struct
1317 .fields
1318 .get("codec")
1319 .map(|v| parse_string(v, "data.create schema codec"))
1320 .transpose()?
1321 .unwrap_or_else(|| "zstd".to_string());
1322 let data_path = format!("arrays/{name}/data.f64.json");
1323 let chunk_index_path = format!("arrays/{name}/chunks/index.json");
1324 arrays.insert(
1325 name.clone(),
1326 DataArrayMeta {
1327 dtype,
1328 shape,
1329 chunk_shape,
1330 order: default_array_order(),
1331 codec,
1332 chunk_index_path: Some(chunk_index_path),
1333 data_path,
1334 },
1335 );
1336 }
1337
1338 Ok(DataSchema { arrays })
1339}
1340
1341fn default_chunk_shape(shape: &[usize]) -> Vec<usize> {
1342 if shape.is_empty() {
1343 return vec![1024];
1344 }
1345 let mut out = shape.to_vec();
1346 if out.len() == 1 {
1347 out[0] = out[0].clamp(1, 65_536);
1348 return out;
1349 }
1350 out[0] = out[0].clamp(1, 256);
1351 out[1] = out[1].clamp(1, 256);
1352 for dim in out.iter_mut().skip(2) {
1353 *dim = (*dim).clamp(1, 8);
1354 }
1355 out
1356}
1357
1358fn parse_usize_vector(value: &Value) -> BuiltinResult<Vec<usize>> {
1359 match value {
1360 Value::Tensor(t) => tensor_to_usize_vector(t),
1361 Value::Num(n) => {
1362 if *n < 0.0 || !n.is_finite() {
1363 return Err(data_error(
1364 "data schema dimensions must be non-negative finite numbers",
1365 ));
1366 }
1367 Ok(vec![*n as usize])
1368 }
1369 Value::Int(i) => {
1370 let n = i.to_i64();
1371 if n < 0 {
1372 return Err(data_error("data schema dimensions must be non-negative"));
1373 }
1374 Ok(vec![n as usize])
1375 }
1376 _ => Err(data_error(
1377 "data schema dimension field must be numeric tensor/vector",
1378 )),
1379 }
1380}
1381
1382fn tensor_to_usize_vector(t: &Tensor) -> BuiltinResult<Vec<usize>> {
1383 let mut out = Vec::with_capacity(t.data.len());
1384 for value in &t.data {
1385 if !value.is_finite() || *value < 0.0 {
1386 return Err(data_error(
1387 "data schema dimensions must be non-negative finite numbers",
1388 ));
1389 }
1390 out.push(*value as usize);
1391 }
1392 Ok(out)
1393}
1394
1395pub fn dataset_object(path: &str, manifest: &DataManifest) -> Value {
1396 let mut obj = ObjectInstance::new("Dataset".to_string());
1397 obj.properties
1398 .insert("__data_path".to_string(), Value::String(path.to_string()));
1399 obj.properties.insert(
1400 "__data_id".to_string(),
1401 Value::String(manifest.dataset_id.clone()),
1402 );
1403 obj.properties.insert(
1404 "__data_version".to_string(),
1405 Value::String(manifest_version_token(manifest)),
1406 );
1407 Value::Object(obj)
1408}
1409
1410pub fn manifest_version_token(manifest: &DataManifest) -> String {
1411 format!("{}:{}", manifest.updated_at, manifest.txn_sequence)
1412}
1413
1414pub fn ensure_manifest_sequence(expected: u64, manifest: &DataManifest) -> BuiltinResult<()> {
1415 if manifest.txn_sequence != expected {
1416 tracing::warn!(
1417 target: "runmat.data",
1418 expected_sequence = expected,
1419 actual_sequence = manifest.txn_sequence,
1420 "manifest conflict detected"
1421 );
1422 return Err(data_error_with_identifier(
1423 "MANIFEST_CONFLICT: dataset changed since transaction begin",
1424 DATA_MANIFEST_CONFLICT_IDENTIFIER,
1425 ));
1426 }
1427 Ok(())
1428}
1429
1430pub fn array_object(dataset_path: &str, array_name: &str) -> Value {
1431 let mut obj = ObjectInstance::new("DataArray".to_string());
1432 obj.properties.insert(
1433 "__data_path".to_string(),
1434 Value::String(dataset_path.to_string()),
1435 );
1436 obj.properties.insert(
1437 "__array_name".to_string(),
1438 Value::String(array_name.to_string()),
1439 );
1440 Value::Object(obj)
1441}
1442
1443pub fn transaction_object(dataset_path: &str, tx_id: &str) -> Value {
1444 let mut obj = ObjectInstance::new("DataTransaction".to_string());
1445 obj.properties.insert(
1446 "__data_path".to_string(),
1447 Value::String(dataset_path.to_string()),
1448 );
1449 obj.properties
1450 .insert("__tx_id".to_string(), Value::String(tx_id.to_string()));
1451 Value::Object(obj)
1452}
1453
1454pub fn get_object_prop<'a>(obj: &'a ObjectInstance, key: &str) -> BuiltinResult<&'a Value> {
1455 obj.properties
1456 .get(key)
1457 .ok_or_else(|| data_error(format!("object missing internal property '{key}'")))
1458}
1459
1460pub fn now_rfc3339() -> String {
1461 Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
1462}
1463
1464pub fn new_dataset_id() -> String {
1465 static NEXT_DATASET_ID: AtomicU64 = AtomicU64::new(1);
1466 let seq = NEXT_DATASET_ID.fetch_add(1, Ordering::Relaxed);
1467 format!("ds_{}_{}", Utc::now().timestamp_millis(), seq)
1468}
1469
1470pub fn new_tx_id() -> String {
1471 static NEXT_TX_ID: AtomicU64 = AtomicU64::new(1);
1472 let seq = NEXT_TX_ID.fetch_add(1, Ordering::Relaxed);
1473 format!("tx_{}_{}", Utc::now().timestamp_millis(), seq)
1474}
1475
1476pub fn start_tx(dataset_path: String, base_sequence: u64) -> BuiltinResult<String> {
1477 let tx_id = new_tx_id();
1478 let pending = PendingTxn {
1479 dataset_path,
1480 base_sequence,
1481 writes: Vec::new(),
1482 resizes: Vec::new(),
1483 fills: Vec::new(),
1484 create_arrays: Vec::new(),
1485 delete_arrays: Vec::new(),
1486 attrs: BTreeMap::new(),
1487 status: TxnStatus::Open,
1488 };
1489 with_tx_registry(|registry| {
1490 registry.insert(tx_id.clone(), pending);
1491 })?;
1492 Ok(tx_id)
1493}
1494
1495pub fn with_tx_mut<T>(
1496 tx_id: &str,
1497 f: impl FnOnce(&mut PendingTxn) -> BuiltinResult<T>,
1498) -> BuiltinResult<T> {
1499 with_tx_registry(|registry| {
1500 let tx = registry.get_mut(tx_id).ok_or_else(|| {
1501 data_error_with_identifier(
1502 format!("transaction '{tx_id}' not found"),
1503 DATA_TRANSACTION_NOT_FOUND_IDENTIFIER,
1504 )
1505 })?;
1506 f(tx)
1507 })?
1508}
1509
1510pub fn with_tx<T>(
1511 tx_id: &str,
1512 f: impl FnOnce(&PendingTxn) -> BuiltinResult<T>,
1513) -> BuiltinResult<T> {
1514 #[cfg(not(target_arch = "wasm32"))]
1515 {
1516 if TASK_TX_REGISTRY.try_with(|_| ()).is_ok() {
1517 return TASK_TX_REGISTRY.with(|registry| {
1518 let registry = registry
1519 .try_borrow()
1520 .map_err(|_| data_error("data transaction registry is already borrowed"))?;
1521 let tx = registry.get(tx_id).ok_or_else(|| {
1522 data_error_with_identifier(
1523 format!("transaction '{tx_id}' not found"),
1524 DATA_TRANSACTION_NOT_FOUND_IDENTIFIER,
1525 )
1526 })?;
1527 f(tx)
1528 });
1529 }
1530 }
1531
1532 FALLBACK_TX_REGISTRY.with(|registry| {
1533 let registry = registry
1534 .try_borrow()
1535 .map_err(|_| data_error("data transaction registry is already borrowed"))?;
1536 let tx = registry.get(tx_id).ok_or_else(|| {
1537 data_error_with_identifier(
1538 format!("transaction '{tx_id}' not found"),
1539 DATA_TRANSACTION_NOT_FOUND_IDENTIFIER,
1540 )
1541 })?;
1542 f(tx)
1543 })
1544}
1545
1546pub fn remove_tx(tx_id: &str) -> BuiltinResult<()> {
1547 with_tx_registry(|registry| {
1548 let _ = registry.remove(tx_id);
1549 })
1550}
1551
1552#[cfg(test)]
1553mod tests {
1554 use super::*;
1555
1556 #[test]
1557 fn payload_roundtrips_every_native_integer_storage_class() {
1558 let cases = vec![
1559 DataArrayValues::I8(vec![i8::MIN, i8::MAX]),
1560 DataArrayValues::I16(vec![i16::MIN, i16::MAX]),
1561 DataArrayValues::I32(vec![i32::MIN, i32::MAX]),
1562 DataArrayValues::I64(vec![i64::MIN, i64::MAX]),
1563 DataArrayValues::U8(vec![0, u8::MAX]),
1564 DataArrayValues::U16(vec![0, u16::MAX]),
1565 DataArrayValues::U32(vec![0, u32::MAX]),
1566 DataArrayValues::U64(vec![0, u64::MAX]),
1567 ];
1568
1569 for values in cases {
1570 let dtype = match &values {
1571 DataArrayValues::I8(_) => "int8",
1572 DataArrayValues::I16(_) => "int16",
1573 DataArrayValues::I32(_) => "int32",
1574 DataArrayValues::I64(_) => "int64",
1575 DataArrayValues::U8(_) => "uint8",
1576 DataArrayValues::U16(_) => "uint16",
1577 DataArrayValues::U32(_) => "uint32",
1578 DataArrayValues::U64(_) => "uint64",
1579 DataArrayValues::F64(_) => unreachable!(),
1580 };
1581 let payload = DataArrayPayload {
1582 dtype: dtype.to_string(),
1583 shape: vec![1, 2],
1584 values: values.clone(),
1585 };
1586 let bytes = serde_json::to_vec(&payload).expect("encode typed payload");
1587 let decoded: DataArrayPayload = serde_json::from_slice(&bytes).expect("decode payload");
1588 assert_eq!(decoded.values, values, "{dtype} payload must remain exact");
1589 let Value::Tensor(tensor) = decoded.into_value().expect("tensor value") else {
1590 panic!("expected tensor");
1591 };
1592 assert_eq!(
1593 tensor.integer_storage().map(IntegerStorage::class_name),
1594 Some(dtype)
1595 );
1596 }
1597 }
1598
1599 #[test]
1600 fn payload_decodes_legacy_f64_arrays_and_normalizes_declared_integer_dtypes() {
1601 let legacy = br#"{"dtype":"uint64","shape":[1,2],"values":[1,2]}"#;
1602 let payload: DataArrayPayload =
1603 serde_json::from_slice(legacy).expect("decode legacy payload");
1604 assert_eq!(payload.values, DataArrayValues::F64(vec![1.0, 2.0]));
1605
1606 let payload = payload
1607 .normalize_for_dtype("uint64")
1608 .expect("normalize legacy payload");
1609 assert_eq!(payload.values, DataArrayValues::U64(vec![1, 2]));
1610 }
1611
1612 #[test]
1613 fn preview_conversion_is_bounded_for_typed_integer_payloads() {
1614 let values = DataArrayValues::I16(vec![-2, 0, 3, 7]);
1615
1616 assert_eq!(values.preview_f64(3), vec![-2.0, 0.0, 3.0]);
1617 assert!(values.preview_f64(0).is_empty());
1618 }
1619
1620 #[test]
1621 fn payload_construction_preserves_uint64_tensor_extrema() {
1622 let input =
1623 Tensor::new_integer(IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]), vec![1, 2])
1624 .expect("uint64 tensor");
1625 let payload = DataArrayPayload::from_value("uint64".to_string(), &Value::Tensor(input))
1626 .expect("payload");
1627 assert_eq!(
1628 payload.values,
1629 DataArrayValues::U64(vec![1_u64 << 63, u64::MAX])
1630 );
1631 }
1632
1633 #[test]
1634 fn ensure_manifest_sequence_accepts_matching_sequence() {
1635 let manifest = DataManifest {
1636 schema_version: 1,
1637 format: "runmat-data".to_string(),
1638 dataset_id: "ds_test".to_string(),
1639 name: Some("test".to_string()),
1640 created_at: "2026-03-01T00:00:00Z".to_string(),
1641 updated_at: "2026-03-01T00:00:00Z".to_string(),
1642 arrays: BTreeMap::new(),
1643 attrs: BTreeMap::new(),
1644 txn_sequence: 5,
1645 };
1646 ensure_manifest_sequence(5, &manifest).expect("expected sequence match");
1647 }
1648
1649 #[test]
1650 fn ensure_manifest_sequence_rejects_conflict() {
1651 let manifest = DataManifest {
1652 schema_version: 1,
1653 format: "runmat-data".to_string(),
1654 dataset_id: "ds_test".to_string(),
1655 name: Some("test".to_string()),
1656 created_at: "2026-03-01T00:00:00Z".to_string(),
1657 updated_at: "2026-03-01T00:00:00Z".to_string(),
1658 arrays: BTreeMap::new(),
1659 attrs: BTreeMap::new(),
1660 txn_sequence: 6,
1661 };
1662 let err = ensure_manifest_sequence(5, &manifest).expect_err("expected conflict error");
1663 assert_eq!(
1664 err.identifier(),
1665 Some(DATA_MANIFEST_CONFLICT_IDENTIFIER),
1666 "manifest conflicts should expose a stable identifier"
1667 );
1668 }
1669
1670 #[test]
1671 fn transaction_registry_roundtrip() {
1672 let tx_id = start_tx("/datasets/test.data".to_string(), 7).expect("start tx");
1673 let status = with_tx(&tx_id, |tx| Ok(tx.status.clone())).expect("tx lookup");
1674 assert_eq!(status, TxnStatus::Open);
1675 remove_tx(&tx_id).expect("remove tx");
1676 let err = with_tx(&tx_id, |_| Ok(())).expect_err("expected missing tx");
1677 assert_eq!(
1678 err.identifier(),
1679 Some(DATA_TRANSACTION_NOT_FOUND_IDENTIFIER),
1680 "missing transaction lookups should expose a stable identifier"
1681 );
1682 }
1683
1684 #[cfg(not(target_arch = "wasm32"))]
1685 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1686 async fn transaction_registry_scope_survives_await() {
1687 with_tx_registry_scope(async {
1688 let tx_id = start_tx("/datasets/task-local.data".to_string(), 11).expect("start tx");
1689 tokio::task::yield_now().await;
1690 let status = with_tx(&tx_id, |tx| Ok(tx.status.clone())).expect("tx lookup");
1691 assert_eq!(status, TxnStatus::Open);
1692 remove_tx(&tx_id).expect("remove tx");
1693 let err = with_tx(&tx_id, |_| Ok(())).expect_err("expected missing tx");
1694 assert_eq!(
1695 err.identifier(),
1696 Some(DATA_TRANSACTION_NOT_FOUND_IDENTIFIER)
1697 );
1698 })
1699 .await;
1700 }
1701
1702 #[test]
1703 fn sha256_hash_format_matches_expected_prefix() {
1704 let hash = sha256_hex(b"runmat");
1705 assert!(hash.starts_with("sha256:"));
1706 assert_eq!(hash.len(), "sha256:".len() + 64);
1707 }
1708}