laminar_connectors/schema/traits/mod.rs
1//! Capability traits for the connector schema framework.
2#![allow(clippy::disallowed_types)] // cold path: schema management
3
4use std::collections::HashMap;
5
6use super::error::SchemaResult;
7use super::types::RawRecord;
8use arrow_array::RecordBatch;
9use arrow_schema::{DataType, SchemaRef};
10
11// ── FormatDecoder ──────────────────────────────────────────────────
12
13/// Decodes raw bytes into Arrow `RecordBatch`es.
14///
15/// Unlike [`RecordDeserializer`](crate::serde::RecordDeserializer) which
16/// takes `&[u8]` slices, `FormatDecoder` works with [`RawRecord`]s that
17/// carry metadata, headers, and timestamps alongside the payload.
18pub trait FormatDecoder: Send + Sync {
19 /// Returns the Arrow schema produced by this decoder.
20 fn output_schema(&self) -> SchemaRef;
21
22 /// Decodes a batch of raw records into an Arrow `RecordBatch`.
23 ///
24 /// # Errors
25 ///
26 /// Returns [`SchemaError::DecodeError`](super::error::SchemaError::DecodeError)
27 /// if the input cannot be parsed.
28 fn decode_batch(&self, records: &[RawRecord]) -> SchemaResult<RecordBatch>;
29
30 /// Decodes a single raw record into an Arrow `RecordBatch` with one row.
31 ///
32 /// Default implementation delegates to [`decode_batch`](Self::decode_batch)
33 /// with a single-element slice.
34 ///
35 /// # Errors
36 ///
37 /// Returns [`SchemaError::DecodeError`](super::error::SchemaError::DecodeError)
38 /// if the input cannot be parsed.
39 fn decode_one(&self, record: &RawRecord) -> SchemaResult<RecordBatch> {
40 self.decode_batch(std::slice::from_ref(record))
41 }
42
43 /// Returns the name of the format this decoder handles (e.g., `"json"`).
44 fn format_name(&self) -> &'static str;
45}
46
47// ── FormatEncoder ──────────────────────────────────────────────────
48
49/// Encodes Arrow `RecordBatch`es into raw bytes.
50pub trait FormatEncoder: Send + Sync {
51 /// Returns the expected input schema.
52 fn input_schema(&self) -> SchemaRef;
53
54 /// Encodes a `RecordBatch` into a vector of byte records.
55 ///
56 /// Each element in the returned vector represents one serialized record.
57 ///
58 /// # Errors
59 ///
60 /// Returns a schema error if encoding fails.
61 fn encode_batch(&self, batch: &RecordBatch) -> SchemaResult<Vec<Vec<u8>>>;
62
63 /// Returns the name of the format this encoder produces (e.g., `"json"`).
64 fn format_name(&self) -> &'static str;
65}
66
67// ── Inference types ────────────────────────────────────────────────
68
69/// Configuration for schema inference.
70#[derive(Debug, Clone)]
71pub struct InferenceConfig {
72 /// Data format to use for inference.
73 pub format: String,
74
75 /// How to handle number type inference.
76 pub number_inference: NumberInference,
77
78 /// How to handle array type inference.
79 pub array_inference: ArrayInference,
80
81 /// Maximum number of samples to use.
82 pub max_samples: usize,
83
84 /// Minimum confidence threshold (0.0–1.0) for accepting an inferred type.
85 pub min_confidence: f64,
86
87 /// Type hints for specific fields.
88 pub type_hints: HashMap<String, DataType>,
89
90 /// Whether to treat empty strings as nulls.
91 pub empty_as_null: bool,
92}
93
94impl Default for InferenceConfig {
95 fn default() -> Self {
96 Self {
97 format: "json".to_string(),
98 number_inference: NumberInference::PreferLarger,
99 array_inference: ArrayInference::Utf8,
100 max_samples: 1000,
101 min_confidence: 0.8,
102 type_hints: HashMap::new(),
103 empty_as_null: false,
104 }
105 }
106}
107
108impl InferenceConfig {
109 /// Creates a new inference config for the given format.
110 #[must_use]
111 pub fn new(format: impl Into<String>) -> Self {
112 Self {
113 format: format.into(),
114 ..Self::default()
115 }
116 }
117
118 /// Sets the minimum confidence threshold.
119 #[must_use]
120 pub fn with_min_confidence(mut self, confidence: f64) -> Self {
121 self.min_confidence = confidence;
122 self
123 }
124
125 /// Sets the maximum number of samples.
126 #[must_use]
127 pub fn with_max_samples(mut self, n: usize) -> Self {
128 self.max_samples = n;
129 self
130 }
131
132 /// Adds a type hint for a specific field.
133 #[must_use]
134 pub fn with_type_hint(mut self, field: impl Into<String>, data_type: DataType) -> Self {
135 self.type_hints.insert(field.into(), data_type);
136 self
137 }
138
139 /// Enables treating empty strings as nulls.
140 #[must_use]
141 pub fn with_empty_as_null(mut self) -> Self {
142 self.empty_as_null = true;
143 self
144 }
145}
146
147/// How to infer numeric types.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum NumberInference {
150 /// Prefer the smallest type that fits (i32 before i64).
151 PreferSmallest,
152 /// Prefer larger types (always i64, always f64).
153 PreferLarger,
154}
155
156/// How to infer array/object types in JSON.
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum ArrayInference {
159 /// Store arrays/objects as JSON-encoded Utf8 strings.
160 Utf8,
161 /// Attempt to infer Arrow List / Struct types.
162 NativeArrow,
163}
164
165/// The result of schema inference.
166#[derive(Debug, Clone)]
167pub struct InferredSchema {
168 /// The inferred Arrow schema.
169 pub schema: SchemaRef,
170
171 /// Overall confidence score (0.0–1.0).
172 pub confidence: f64,
173
174 /// Number of samples that were analyzed.
175 pub sample_count: usize,
176
177 /// Per-field inference details.
178 pub field_details: Vec<FieldInferenceDetail>,
179
180 /// Warnings generated during inference.
181 pub warnings: Vec<InferenceWarning>,
182}
183
184/// Per-field detail from inference.
185#[derive(Debug, Clone)]
186pub struct FieldInferenceDetail {
187 /// The field name.
188 pub field_name: String,
189
190 /// The inferred Arrow data type.
191 pub inferred_type: DataType,
192
193 /// Confidence for this specific field (0.0–1.0).
194 pub confidence: f64,
195
196 /// Number of non-null samples seen for this field.
197 pub non_null_count: usize,
198
199 /// Total number of samples that included this field.
200 pub total_count: usize,
201
202 /// Whether a type hint was applied.
203 pub hint_applied: bool,
204}
205
206/// A warning generated during inference.
207#[derive(Debug, Clone)]
208pub struct InferenceWarning {
209 /// The field this warning relates to, if any.
210 pub field: Option<String>,
211
212 /// Warning message.
213 pub message: String,
214
215 /// Severity level.
216 pub severity: WarningSeverity,
217}
218
219pub use laminar_core::error_codes::WarningSeverity;
220
221/// Compatibility mode for schema evolution.
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub enum CompatibilityMode {
224 /// No compatibility checks.
225 None,
226 /// New schema can read old data.
227 Backward,
228 /// Old schema can read new data.
229 Forward,
230 /// Both backward and forward compatible.
231 Full,
232 /// Backward compatible with all previous versions.
233 BackwardTransitive,
234 /// Forward compatible with all previous versions.
235 ForwardTransitive,
236 /// Fully compatible with all previous versions.
237 FullTransitive,
238}
239
240/// A single schema change detected by schema diffing.
241#[derive(Debug, Clone, PartialEq)]
242pub enum SchemaChange {
243 /// A new column was added.
244 ColumnAdded {
245 /// Column name.
246 name: String,
247 /// The new column's data type.
248 data_type: DataType,
249 /// Whether the column is nullable.
250 nullable: bool,
251 },
252
253 /// An existing column was removed.
254 ColumnRemoved {
255 /// Column name.
256 name: String,
257 },
258
259 /// A column's data type changed.
260 TypeChanged {
261 /// Column name.
262 name: String,
263 /// Previous data type.
264 old_type: DataType,
265 /// New data type.
266 new_type: DataType,
267 },
268
269 /// A column's nullability changed.
270 NullabilityChanged {
271 /// Column name.
272 name: String,
273 /// Previous nullable flag.
274 was_nullable: bool,
275 /// New nullable flag.
276 now_nullable: bool,
277 },
278
279 /// A column was renamed.
280 ColumnRenamed {
281 /// Previous name.
282 old_name: String,
283 /// New name.
284 new_name: String,
285 },
286}
287
288/// The result of evaluating a set of schema changes.
289#[derive(Debug, Clone, PartialEq, Eq)]
290pub enum EvolutionVerdict {
291 /// All changes are compatible — evolution can proceed.
292 Compatible,
293
294 /// Changes require data migration but are feasible.
295 RequiresMigration,
296
297 /// Changes are incompatible — evolution is rejected.
298 Incompatible(String),
299}
300
301/// Describes how to project columns from the old schema to the new schema.
302#[derive(Debug, Clone)]
303pub struct ColumnProjection {
304 /// For each column in the new schema, the index in the old schema
305 /// (or `None` if the column is newly added and should be filled
306 /// with the default/null).
307 pub mappings: Vec<Option<usize>>,
308
309 /// The resulting schema after projection.
310 pub target_schema: SchemaRef,
311}
312
313// ── Object-safety assertions ───────────────────────────────────────
314
315// Compile-time checks that the codec traits are object-safe.
316const _: () = {
317 fn _assert_format_decoder_object_safe(_: &dyn FormatDecoder) {}
318 fn _assert_format_encoder_object_safe(_: &dyn FormatEncoder) {}
319};
320
321#[cfg(test)]
322mod tests;