Skip to main content

runmat_runtime/data/
mod.rs

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/// The persisted backing values of a data-array payload.
57///
58/// JSON arrays written before integer storage was introduced are decoded as
59/// `F64`; new writes use the tagged representation below so every integer
60/// class can round-trip without passing through a floating point value.
61#[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    /// Returns at most `limit` values converted to the numeric preview format.
213    ///
214    /// Data-file previews cross the WASM boundary as JavaScript numbers. Keep
215    /// that established representation while only converting the requested
216    /// prefix: a fallback full-payload read may contain substantially more
217    /// values than the preview is allowed to return.
218    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}