Skip to main content

cynos_binary/
schema_layout.rs

1//! Schema layout for binary encoding.
2//!
3//! Pre-computes offsets and sizes for efficient binary encoding/decoding.
4
5use super::BinaryDataType;
6use alloc::string::{String, ToString};
7use alloc::vec::Vec;
8use cynos_core::schema::Table;
9#[cfg(feature = "wasm")]
10use wasm_bindgen::prelude::*;
11
12/// Layout information for a single column
13#[derive(Debug, Clone)]
14pub struct ColumnLayout {
15    /// Column name
16    pub name: String,
17    /// Binary data type
18    pub data_type: BinaryDataType,
19    /// Fixed size in bytes
20    pub fixed_size: usize,
21    /// Whether this column is nullable
22    pub is_nullable: bool,
23    /// Offset within the row (after null_mask)
24    pub offset: usize,
25}
26
27/// Pre-computed layout for binary encoding/decoding
28#[cfg_attr(feature = "wasm", wasm_bindgen)]
29#[derive(Debug, Clone)]
30pub struct SchemaLayout {
31    columns: Vec<ColumnLayout>,
32    /// Total bytes per row (null_mask + all columns)
33    row_stride: usize,
34    /// Size of null_mask in bytes: ceil(cols/8)
35    null_mask_size: usize,
36}
37
38impl SchemaLayout {
39    /// Create a SchemaLayout directly from components
40    pub fn new(columns: Vec<ColumnLayout>, row_stride: usize, null_mask_size: usize) -> Self {
41        Self {
42            columns,
43            row_stride,
44            null_mask_size,
45        }
46    }
47
48    /// Create a SchemaLayout from a table schema
49    pub fn from_schema(schema: &Table) -> Self {
50        let columns: Vec<ColumnLayout> = schema
51            .columns()
52            .iter()
53            .scan(0usize, |offset, col| {
54                let data_type = BinaryDataType::from(col.data_type());
55                let fixed_size = data_type.fixed_size();
56                let layout = ColumnLayout {
57                    name: col.name().to_string(),
58                    data_type,
59                    fixed_size,
60                    is_nullable: col.is_nullable(),
61                    offset: *offset,
62                };
63                *offset += fixed_size;
64                Some(layout)
65            })
66            .collect();
67
68        let null_mask_size = (columns.len() + 7) / 8;
69        let data_size: usize = columns.iter().map(|c| c.fixed_size).sum();
70        let row_stride = null_mask_size + data_size;
71
72        Self {
73            columns,
74            row_stride,
75            null_mask_size,
76        }
77    }
78
79    /// Create a SchemaLayout by merging multiple table schemas (for JOIN results).
80    /// Columns are concatenated in order: left table columns, then right table columns, etc.
81    /// Right-side columns are marked nullable since LEFT JOIN can produce NULLs.
82    pub fn from_schemas(schemas: &[&Table]) -> Self {
83        let columns: Vec<ColumnLayout> = schemas
84            .iter()
85            .enumerate()
86            .flat_map(|(table_idx, schema)| {
87                schema.columns().iter().map(move |col| (table_idx, col))
88            })
89            .scan(0usize, |offset, (table_idx, col)| {
90                let data_type = BinaryDataType::from(col.data_type());
91                let fixed_size = data_type.fixed_size();
92                let layout = ColumnLayout {
93                    name: col.name().to_string(),
94                    data_type,
95                    fixed_size,
96                    // Right-side tables in a JOIN can have NULLs (LEFT JOIN)
97                    is_nullable: table_idx > 0 || col.is_nullable(),
98                    offset: *offset,
99                };
100                *offset += fixed_size;
101                Some(layout)
102            })
103            .collect();
104
105        let null_mask_size = (columns.len() + 7) / 8;
106        let data_size: usize = columns.iter().map(|c| c.fixed_size).sum();
107        let row_stride = null_mask_size + data_size;
108
109        Self {
110            columns,
111            row_stride,
112            null_mask_size,
113        }
114    }
115
116    /// Create a SchemaLayout from projected columns
117    pub fn from_projection(schema: &Table, column_names: &[String]) -> Self {
118        let columns: Vec<ColumnLayout> = column_names
119            .iter()
120            .scan(0usize, |offset, name| {
121                let col = schema.get_column(name)?;
122                let data_type = BinaryDataType::from(col.data_type());
123                let fixed_size = data_type.fixed_size();
124                let layout = ColumnLayout {
125                    name: col.name().to_string(),
126                    data_type,
127                    fixed_size,
128                    is_nullable: col.is_nullable(),
129                    offset: *offset,
130                };
131                *offset += fixed_size;
132                Some(layout)
133            })
134            .collect();
135
136        let null_mask_size = (columns.len() + 7) / 8;
137        let data_size: usize = columns.iter().map(|c| c.fixed_size).sum();
138        let row_stride = null_mask_size + data_size;
139
140        Self {
141            columns,
142            row_stride,
143            null_mask_size,
144        }
145    }
146
147    /// Get the columns
148    pub fn columns(&self) -> &[ColumnLayout] {
149        &self.columns
150    }
151
152    /// Get row stride (total bytes per row)
153    pub fn row_stride(&self) -> usize {
154        self.row_stride
155    }
156
157    /// Get null mask size in bytes
158    pub fn null_mask_size(&self) -> usize {
159        self.null_mask_size
160    }
161
162    /// Calculate required buffer size for N rows (header + fixed section only)
163    pub fn calculate_fixed_size(&self, row_count: usize) -> usize {
164        super::HEADER_SIZE + self.row_stride * row_count
165    }
166}
167
168// WASM bindings for JS access
169#[cfg(feature = "wasm")]
170#[wasm_bindgen]
171impl SchemaLayout {
172    /// Get the number of columns
173    #[wasm_bindgen(js_name = columnCount)]
174    pub fn column_count_js(&self) -> usize {
175        self.columns.len()
176    }
177
178    /// Get column name by index
179    #[wasm_bindgen(js_name = columnName)]
180    pub fn column_name_js(&self, idx: usize) -> Option<String> {
181        self.columns.get(idx).map(|c| c.name.clone())
182    }
183
184    /// Get column type by index (returns BinaryDataType as u8)
185    #[wasm_bindgen(js_name = columnType)]
186    pub fn column_type_js(&self, idx: usize) -> Option<u8> {
187        self.columns.get(idx).map(|c| c.data_type as u8)
188    }
189
190    /// Get column offset by index (offset within row, after null_mask)
191    #[wasm_bindgen(js_name = columnOffset)]
192    pub fn column_offset_js(&self, idx: usize) -> Option<usize> {
193        self.columns.get(idx).map(|c| c.offset)
194    }
195
196    /// Get column fixed size by index
197    #[wasm_bindgen(js_name = columnFixedSize)]
198    pub fn column_fixed_size_js(&self, idx: usize) -> Option<usize> {
199        self.columns.get(idx).map(|c| c.fixed_size)
200    }
201
202    /// Check if column is nullable
203    #[wasm_bindgen(js_name = columnNullable)]
204    pub fn column_nullable_js(&self, idx: usize) -> Option<bool> {
205        self.columns.get(idx).map(|c| c.is_nullable)
206    }
207
208    /// Get row stride (total bytes per row)
209    #[wasm_bindgen(js_name = rowStride)]
210    pub fn row_stride_js(&self) -> usize {
211        self.row_stride
212    }
213
214    /// Get null mask size in bytes
215    #[wasm_bindgen(js_name = nullMaskSize)]
216    pub fn null_mask_size_js(&self) -> usize {
217        self.null_mask_size
218    }
219}