laminar_connectors/schema/
resolution.rs1use arrow_schema::SchemaRef;
4use serde::Serialize;
5
6use crate::config::ConnectorConfig;
7use crate::error::ConnectorError;
8
9pub use laminar_core::schema_binding::{
10 NativeSchema, NativeSchemaArtifact, SchemaBinding, SchemaDirection, SchemaFieldMapping,
11 SchemaOrigin,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
16#[serde(rename_all = "snake_case")]
17pub enum SchemaDiscovery {
18 Explicit,
20 BuiltIn,
22 Metadata,
24 Query,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
30#[serde(rename_all = "snake_case")]
31pub enum SchemaPreparation {
32 None,
34 ExplicitTableCreation,
36 ExplicitRegistration,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
42pub struct SchemaCapabilities {
43 pub discovery: SchemaDiscovery,
45 pub metadata_formats: Vec<String>,
47 pub built_in_formats: Vec<String>,
49 pub query_derived: bool,
51 pub bounded_sampling: bool,
53 pub preparation: SchemaPreparation,
55}
56
57impl SchemaCapabilities {
58 pub(crate) fn validate_native_format(
59 &self,
60 config: &ConnectorConfig,
61 ) -> Result<(), ConnectorError> {
62 let native = match self.discovery {
63 SchemaDiscovery::BuiltIn => self.built_in_formats.is_empty(),
64 SchemaDiscovery::Metadata => self.metadata_formats.is_empty(),
65 SchemaDiscovery::Explicit | SchemaDiscovery::Query => false,
66 };
67 if native && config.get("format").is_some() {
68 return Err(ConnectorError::FeatureUnsupported(format!(
69 "{} uses a fixed native protocol; omit FORMAT instead of selecting a serialization codec",
70 config.connector_type()
71 )));
72 }
73 Ok(())
74 }
75
76 #[must_use]
79 pub fn declared(is_sink: bool) -> Self {
80 Self {
81 discovery: if is_sink {
82 SchemaDiscovery::Query
83 } else {
84 SchemaDiscovery::Explicit
85 },
86 metadata_formats: Vec::new(),
87 built_in_formats: Vec::new(),
88 query_derived: is_sink,
89 bounded_sampling: false,
90 preparation: SchemaPreparation::None,
91 }
92 }
93
94 #[must_use]
96 pub fn built_in() -> Self {
97 Self {
98 discovery: SchemaDiscovery::BuiltIn,
99 ..Self::declared(false)
100 }
101 }
102
103 #[must_use]
105 pub fn metadata(formats: &[&str], is_sink: bool, preparation: SchemaPreparation) -> Self {
106 Self {
107 discovery: SchemaDiscovery::Metadata,
108 metadata_formats: formats.iter().map(|format| (*format).into()).collect(),
109 preparation,
110 ..Self::declared(is_sink)
111 }
112 }
113}
114
115pub fn logical_binding(
120 config: &ConnectorConfig,
121 direction: SchemaDirection,
122 origin: SchemaOrigin,
123 schema: &SchemaRef,
124) -> Result<SchemaBinding, ConnectorError> {
125 SchemaBinding::logical(
126 config.connector_type(),
127 direction,
128 origin,
129 schema.as_ref().clone(),
130 )
131 .map_err(binding_error)
132}
133
134pub fn bind_external(
139 binding: &mut SchemaBinding,
140 schema: &SchemaRef,
141) -> Result<(), ConnectorError> {
142 binding
143 .bind_external(schema.as_ref().clone())
144 .map_err(binding_error)
145}
146
147#[must_use]
149#[allow(clippy::needless_pass_by_value)] pub fn binding_error(error: laminar_core::schema_binding::SchemaBindingError) -> ConnectorError {
151 ConnectorError::SchemaMismatch(error.to_string())
152}
153
154pub fn validate_prepared_writer(
161 config: &ConnectorConfig,
162 resolved: &SchemaBinding,
163 prepared: &SchemaBinding,
164) -> Result<(), ConnectorError> {
165 if prepared.connector != config.connector_type()
166 || prepared.direction != SchemaDirection::Sink
167 || prepared.logical != resolved.logical
168 || prepared.control_fields != resolved.control_fields
169 {
170 return Err(ConnectorError::SchemaMismatch(format!(
171 "{} sink preparation changed the resolved query schema or writer policy",
172 config.connector_type()
173 )));
174 }
175 prepared.canonical_bytes().map_err(binding_error)?;
176 Ok(())
177}
178
179pub fn fixed_binding(
184 config: &ConnectorConfig,
185 explicit: Option<SchemaRef>,
186 protocol: &SchemaRef,
187) -> Result<SchemaBinding, ConnectorError> {
188 let origin = if explicit.is_some() {
189 SchemaOrigin::Explicit
190 } else {
191 SchemaOrigin::BuiltIn
192 };
193 let logical = explicit.unwrap_or_else(|| std::sync::Arc::clone(protocol));
194 if logical.fields().len() != protocol.fields().len()
195 || logical
196 .fields()
197 .iter()
198 .zip(protocol.fields())
199 .any(|(logical, native)| logical.name() != native.name())
200 {
201 return Err(ConnectorError::FeatureUnsupported("fixed protocol fields must be declared in their complete protocol order; project in CREATE STREAM".into()));
202 }
203 let mut binding = logical_binding(config, SchemaDirection::Source, origin, &logical)?;
204 bind_external(&mut binding, protocol)?;
205 Ok(binding)
206}
207
208pub fn project_batch(
213 batch: &arrow_array::RecordBatch,
214 logical: &SchemaRef,
215) -> Result<arrow_array::RecordBatch, ConnectorError> {
216 let mut columns = Vec::with_capacity(logical.fields().len());
217 for field in logical.fields() {
218 let index = batch.schema().index_of(field.name()).map_err(|_| {
219 ConnectorError::SchemaMismatch(format!("native batch is missing '{}'", field.name()))
220 })?;
221 let column = batch.column(index);
222 if !laminar_core::schema_binding::same_logical_type(column.data_type(), field.data_type())
223 || (!field.is_nullable() && column.null_count() > 0)
224 {
225 return Err(ConnectorError::SchemaMismatch(format!(
226 "native field '{}' changed type or nullability",
227 field.name()
228 )));
229 }
230 let column = if column.data_type() == field.data_type() {
231 std::sync::Arc::clone(column)
232 } else {
233 arrow_cast::cast(column, field.data_type())
235 .map_err(|error| ConnectorError::SchemaMismatch(error.to_string()))?
236 };
237 columns.push(column);
238 }
239 arrow_array::RecordBatch::try_new(std::sync::Arc::clone(logical), columns)
240 .map_err(|error| ConnectorError::SchemaMismatch(error.to_string()))
241}