arrow_sql_server/write/record_batch/
validate.rs1use arrow_array::{Array, RecordBatch};
4use arrow_schema::{Field, Schema};
5
6use crate::{
7 Diagnostic, DiagnosticCode, DiagnosticSet, FieldRef, Result, SchemaMapping,
8 conversion::arrow_to_mssql::variable_width::arrow_type_compatible_with_mapping,
9};
10
11pub fn validate_arrow_schema_against_mappings(
22 schema: &Schema,
23 mappings: &[SchemaMapping],
24) -> Result<()> {
25 validate_schema_field_count_against_mappings(schema, mappings)?;
26
27 for (position, (field, mapping)) in schema.fields().iter().zip(mappings).enumerate() {
28 validate_schema_field_shape_against_mapping(position, field, mapping)?;
29 validate_schema_field_nullability_against_mapping(field, mapping)?;
30 }
31
32 Ok(())
33}
34
35fn validate_schema_shape_for_record_batch_encoding(
36 schema: &Schema,
37 mappings: &[SchemaMapping],
38) -> Result<()> {
39 validate_schema_field_count_against_mappings(schema, mappings)?;
40
41 for (position, (field, mapping)) in schema.fields().iter().zip(mappings).enumerate() {
42 validate_schema_field_shape_against_mapping(position, field, mapping)?;
43 }
44
45 Ok(())
46}
47
48fn validate_schema_field_count_against_mappings(
49 schema: &Schema,
50 mappings: &[SchemaMapping],
51) -> Result<()> {
52 if schema.fields().len() < mappings.len() {
53 let mapping = &mappings[schema.fields().len()];
54 return Err(value_conversion_error(mapping_diagnostic(
55 mapping,
56 DiagnosticCode::SchemaMismatch,
57 format!(
58 "planned column index {} is outside runtime schema with {} field(s)",
59 mapping.arrow().index(),
60 schema.fields().len()
61 ),
62 )));
63 }
64
65 if schema.fields().len() > mappings.len() {
66 return Err(value_conversion_error(Diagnostic::error(
67 DiagnosticCode::SchemaMismatch,
68 format!(
69 "runtime schema has {} field(s) but mappings contain {} field(s)",
70 schema.fields().len(),
71 mappings.len()
72 ),
73 )));
74 }
75
76 Ok(())
77}
78
79pub fn validate_record_batch_schema_against_mappings(
86 batch: &RecordBatch,
87 mappings: &[SchemaMapping],
88) -> Result<()> {
89 validate_arrow_schema_against_mappings(batch.schema().as_ref(), mappings)
90}
91
92pub(crate) fn validate_record_batch_encoding_shape(
93 batch: &RecordBatch,
94 mappings: &[SchemaMapping],
95) -> Result<()> {
96 validate_schema_shape_for_record_batch_encoding(batch.schema().as_ref(), mappings)?;
97
98 for (array, mapping) in batch.columns().iter().zip(mappings) {
101 validate_unchecked_column_array_against_mapping(array.as_ref(), mapping)?;
102 }
103
104 Ok(())
105}
106
107fn validate_schema_field_shape_against_mapping(
108 position: usize,
109 field: &Field,
110 mapping: &SchemaMapping,
111) -> Result<()> {
112 if mapping.arrow().index() != position {
113 return Err(value_conversion_error(mapping_diagnostic(
114 mapping,
115 DiagnosticCode::SchemaMismatch,
116 format!(
117 "mapping position {position} does not match planned Arrow field index {}",
118 mapping.arrow().index()
119 ),
120 )));
121 }
122
123 if field.name() != mapping.arrow().name() {
124 return Err(value_conversion_error(mapping_diagnostic(
125 mapping,
126 DiagnosticCode::SchemaMismatch,
127 format!(
128 "runtime Arrow field name {} does not match planned Arrow field name {}",
129 field.name(),
130 mapping.arrow().name()
131 ),
132 )));
133 }
134
135 if !arrow_type_compatible_with_mapping(field.data_type(), mapping) {
136 return Err(value_conversion_error(mapping_diagnostic(
137 mapping,
138 DiagnosticCode::SchemaMismatch,
139 format!(
140 "runtime Arrow type {} does not match planned Arrow type {}",
141 field.data_type(),
142 mapping.arrow().data_type()
143 ),
144 )));
145 }
146
147 Ok(())
148}
149
150fn validate_schema_field_nullability_against_mapping(
151 field: &Field,
152 mapping: &SchemaMapping,
153) -> Result<()> {
154 if field.is_nullable() != mapping.arrow().nullable() {
155 return Err(value_conversion_error(mapping_diagnostic(
156 mapping,
157 DiagnosticCode::SchemaMismatch,
158 format!(
159 "runtime Arrow nullability {} does not match planned Arrow nullability {}",
160 field.is_nullable(),
161 mapping.arrow().nullable()
162 ),
163 )));
164 }
165
166 Ok(())
167}
168
169fn validate_unchecked_column_array_against_mapping(
170 array: &dyn Array,
171 mapping: &SchemaMapping,
172) -> Result<()> {
173 if !arrow_type_compatible_with_mapping(array.data_type(), mapping) {
174 return Err(value_conversion_error(mapping_diagnostic(
175 mapping,
176 DiagnosticCode::SchemaMismatch,
177 format!(
178 "runtime Arrow type {} does not match planned Arrow type {}",
179 array.data_type(),
180 mapping.arrow().data_type()
181 ),
182 )));
183 }
184
185 Ok(())
186}
187
188fn mapping_diagnostic(
189 mapping: &SchemaMapping,
190 code: DiagnosticCode,
191 message: impl Into<String>,
192) -> Diagnostic {
193 Diagnostic::error(code, message).with_field(FieldRef::new(
194 mapping.arrow().index(),
195 mapping.arrow().name(),
196 ))
197}
198
199fn value_conversion_error(diagnostic: Diagnostic) -> crate::Error {
200 crate::Error::ValueConversion {
201 diagnostics: DiagnosticSet::from(vec![diagnostic]),
202 }
203}