Skip to main content

servers/otlp/
coerce.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use api::v1::ColumnDataType;
16use api::v1::value::ValueData;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum TraceCoerceError {
20    Unsupported,
21}
22
23// For now we support the following coercions:
24// - Int64 to Float64
25// - Int64 to String
26// - Float64 to String
27// - Boolean to String
28// The following coercions are supported with parse, which could fail:
29// If fails, we will return TraceCoerceError::Unsupported.
30// - String to Int64
31// - String to Float64
32// - String to Boolean
33//
34// Lossless signed-to-unsigned integer casts. These let the built-in data
35// models move from unsigned to signed integers while existing unsigned tables
36// keep accepting new signed ingest without an `ALTER TABLE`: an existing
37// UInt64/UInt32 column coerces an incoming Int64/Int32 request into the
38// existing type:
39// - Int64 to UInt64  (e.g. trace `duration_nano` on the v1 path): checked,
40//   so negative values are rejected rather than silently wrapping. Counts and
41//   durations are non-negative by construction; a negative request is a
42//   malformed value (e.g. a span whose end precedes its start).
43// - Int32 to UInt32  (e.g. log `trace_flags`): bit-preserving, because
44//   `trace_flags` is a bit field whose high bits may legitimately be set
45//   (e.g. the W3C sampled flag); bit patterns must round-trip exactly.
46
47/// The signed→unsigned integer coercions that let the built-in data models
48/// move from unsigned to signed integers while existing unsigned tables keep
49/// accepting new signed ingest without an `ALTER TABLE` (see the pair
50/// descriptions in the module-level comment above). Kept as one predicate so
51/// the trace and log ingest paths share the same supported pair set and
52/// cannot drift.
53pub fn is_supported_signed_to_unsigned_coercion(
54    request_type: ColumnDataType,
55    target_type: ColumnDataType,
56) -> bool {
57    matches!(
58        (request_type, target_type),
59        (ColumnDataType::Int64, ColumnDataType::Uint64)
60            | (ColumnDataType::Int32, ColumnDataType::Uint32)
61    )
62}
63
64pub fn is_supported_trace_coercion(
65    request_type: ColumnDataType,
66    target_type: ColumnDataType,
67) -> bool {
68    matches!(
69        (request_type, target_type),
70        (ColumnDataType::Int64, ColumnDataType::Float64)
71            | (ColumnDataType::Int64, ColumnDataType::String)
72            | (ColumnDataType::Float64, ColumnDataType::String)
73            | (ColumnDataType::Boolean, ColumnDataType::String)
74            | (ColumnDataType::String, ColumnDataType::Int64)
75            | (ColumnDataType::String, ColumnDataType::Float64)
76            | (ColumnDataType::String, ColumnDataType::Boolean)
77    ) || is_supported_signed_to_unsigned_coercion(request_type, target_type)
78}
79
80pub fn coerce_value_data(
81    value: &Option<ValueData>,
82    target: ColumnDataType,
83    request_type: ColumnDataType,
84) -> Result<Option<ValueData>, TraceCoerceError> {
85    let Some(v) = value else {
86        return Ok(None);
87    };
88
89    let Some(value) = coerce_non_null_value(target, request_type, v) else {
90        return Err(TraceCoerceError::Unsupported);
91    };
92    Ok(Some(value))
93}
94
95pub fn coerce_non_null_value(
96    target: ColumnDataType,
97    request_type: ColumnDataType,
98    value: &ValueData,
99) -> Option<ValueData> {
100    match (request_type, target, value) {
101        (ColumnDataType::Int64, ColumnDataType::Float64, ValueData::I64Value(n)) => {
102            Some(ValueData::F64Value(*n as f64))
103        }
104        (ColumnDataType::Int64, ColumnDataType::String, ValueData::I64Value(n)) => {
105            Some(ValueData::StringValue(n.to_string()))
106        }
107        (ColumnDataType::Float64, ColumnDataType::String, ValueData::F64Value(n)) => {
108            Some(ValueData::StringValue(n.to_string()))
109        }
110        (ColumnDataType::Boolean, ColumnDataType::String, ValueData::BoolValue(b)) => {
111            Some(ValueData::StringValue(b.to_string()))
112        }
113        (ColumnDataType::String, ColumnDataType::Int64, ValueData::StringValue(s)) => {
114            s.parse::<i64>().ok().map(ValueData::I64Value)
115        }
116        (ColumnDataType::String, ColumnDataType::Float64, ValueData::StringValue(s)) => {
117            s.parse::<f64>().ok().map(ValueData::F64Value)
118        }
119        (ColumnDataType::String, ColumnDataType::Boolean, ValueData::StringValue(s)) => {
120            s.parse::<bool>().ok().map(ValueData::BoolValue)
121        }
122        // Checked signed -> unsigned cast for built-in fields moving to signed
123        // types (durations, counts are always non-negative). Negative values
124        // are rejected instead of wrapping so the coercion stays lossless.
125        (ColumnDataType::Int64, ColumnDataType::Uint64, ValueData::I64Value(n)) => {
126            u64::try_from(*n).ok().map(ValueData::U64Value)
127        }
128        // Bit-preserving cast for the `trace_flags` bit field: high bits may
129        // legitimately be set, and the bit pattern must survive the round-trip
130        // into an existing UInt32 column unchanged.
131        (ColumnDataType::Int32, ColumnDataType::Uint32, ValueData::I32Value(n)) => {
132            Some(ValueData::U32Value(*n as u32))
133        }
134        _ => None,
135    }
136}
137
138pub fn trace_value_datatype(value: &ValueData) -> Option<ColumnDataType> {
139    match value {
140        ValueData::StringValue(_) => Some(ColumnDataType::String),
141        ValueData::BoolValue(_) => Some(ColumnDataType::Boolean),
142        ValueData::I64Value(_) => Some(ColumnDataType::Int64),
143        ValueData::F64Value(_) => Some(ColumnDataType::Float64),
144        ValueData::BinaryValue(_) => Some(ColumnDataType::Binary),
145        _ => None,
146    }
147}
148
149/// Resolves the final datatype for a new trace column when there is no existing
150/// table schema to override the request-local observations.
151pub fn resolve_new_trace_column_type(
152    observed_types: impl IntoIterator<Item = ColumnDataType>,
153) -> Result<Option<ColumnDataType>, TraceCoerceError> {
154    let mut observed = Vec::new();
155    for datatype in observed_types {
156        if !observed.contains(&datatype) {
157            observed.push(datatype);
158        }
159    }
160
161    if observed.is_empty() {
162        return Ok(None);
163    }
164    if observed.len() == 1 {
165        return Ok(observed.first().copied());
166    }
167
168    [
169        ColumnDataType::Boolean,
170        ColumnDataType::Int64,
171        ColumnDataType::Float64,
172        ColumnDataType::String,
173    ]
174    .into_iter()
175    .find(|target| {
176        observed.contains(target)
177            && observed
178                .iter()
179                .all(|source| source == target || is_supported_trace_coercion(*source, *target))
180    })
181    .map(Some)
182    .ok_or(TraceCoerceError::Unsupported)
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_coerce_int64_to_float64() {
191        let result = coerce_value_data(
192            &Some(ValueData::I64Value(42)),
193            ColumnDataType::Float64,
194            ColumnDataType::Int64,
195        );
196        assert_eq!(result, Ok(Some(ValueData::F64Value(42.0))));
197    }
198
199    #[test]
200    fn test_coerce_string_to_int64() {
201        let result = coerce_value_data(
202            &Some(ValueData::StringValue("123".to_string())),
203            ColumnDataType::Int64,
204            ColumnDataType::String,
205        );
206        assert_eq!(result, Ok(Some(ValueData::I64Value(123))));
207    }
208
209    #[test]
210    fn test_coerce_int64_to_string() {
211        let result = coerce_value_data(
212            &Some(ValueData::I64Value(123)),
213            ColumnDataType::String,
214            ColumnDataType::Int64,
215        );
216        assert_eq!(result, Ok(Some(ValueData::StringValue("123".to_string()))));
217    }
218
219    #[test]
220    fn test_coerce_string_to_float64() {
221        let result = coerce_value_data(
222            &Some(ValueData::StringValue("1.5".to_string())),
223            ColumnDataType::Float64,
224            ColumnDataType::String,
225        );
226        assert_eq!(result, Ok(Some(ValueData::F64Value(1.5))));
227    }
228
229    #[test]
230    fn test_coerce_float64_to_string() {
231        let result = coerce_value_data(
232            &Some(ValueData::F64Value(1.5)),
233            ColumnDataType::String,
234            ColumnDataType::Float64,
235        );
236        assert_eq!(result, Ok(Some(ValueData::StringValue("1.5".to_string()))));
237    }
238
239    #[test]
240    fn test_coerce_string_to_boolean() {
241        let result = coerce_value_data(
242            &Some(ValueData::StringValue("true".to_string())),
243            ColumnDataType::Boolean,
244            ColumnDataType::String,
245        );
246        assert_eq!(result, Ok(Some(ValueData::BoolValue(true))));
247
248        let result = coerce_value_data(
249            &Some(ValueData::StringValue("false".to_string())),
250            ColumnDataType::Boolean,
251            ColumnDataType::String,
252        );
253        assert_eq!(result, Ok(Some(ValueData::BoolValue(false))));
254    }
255
256    #[test]
257    fn test_coerce_boolean_to_string() {
258        let result = coerce_value_data(
259            &Some(ValueData::BoolValue(true)),
260            ColumnDataType::String,
261            ColumnDataType::Boolean,
262        );
263        assert_eq!(result, Ok(Some(ValueData::StringValue("true".to_string()))));
264    }
265
266    #[test]
267    fn test_coerce_unparsable_string() {
268        let result = coerce_value_data(
269            &Some(ValueData::StringValue("not_a_number".to_string())),
270            ColumnDataType::Int64,
271            ColumnDataType::String,
272        );
273        assert_eq!(result, Err(TraceCoerceError::Unsupported));
274    }
275
276    #[test]
277    fn test_coerce_float64_to_int64_not_supported() {
278        let result = coerce_value_data(
279            &Some(ValueData::F64Value(1.5)),
280            ColumnDataType::Int64,
281            ColumnDataType::Float64,
282        );
283        assert_eq!(result, Err(TraceCoerceError::Unsupported));
284    }
285
286    #[test]
287    fn test_coerce_int64_to_uint64() {
288        // Non-negative durations coerce losslessly into an existing UInt64
289        // column, so built-in fields moving to signed keep accepting writes.
290        let result = coerce_value_data(
291            &Some(ValueData::I64Value(123)),
292            ColumnDataType::Uint64,
293            ColumnDataType::Int64,
294        );
295        assert_eq!(result, Ok(Some(ValueData::U64Value(123))));
296    }
297
298    #[test]
299    fn test_coerce_negative_int64_to_uint64_rejected() {
300        // Negative values must not wrap into the unsigned column: the
301        // signed -> unsigned coercion is only lossless for non-negative
302        // inputs, so anything else is rejected instead of silently wrapping
303        // to a huge number.
304        let result = coerce_value_data(
305            &Some(ValueData::I64Value(-1)),
306            ColumnDataType::Uint64,
307            ColumnDataType::Int64,
308        );
309        assert_eq!(result, Err(TraceCoerceError::Unsupported));
310    }
311
312    #[test]
313    fn test_coerce_int32_to_uint32() {
314        let result = coerce_value_data(
315            &Some(ValueData::I32Value(7)),
316            ColumnDataType::Uint32,
317            ColumnDataType::Int32,
318        );
319        assert_eq!(result, Ok(Some(ValueData::U32Value(7))));
320    }
321
322    #[test]
323    fn test_coerce_uint_to_int_not_supported() {
324        // Only the signed -> unsigned direction is supported (the direction
325        // the no-ALTER transition needs); the reverse would be lossy for
326        // values above the signed range and is intentionally rejected.
327        let result = coerce_value_data(
328            &Some(ValueData::U64Value(9)),
329            ColumnDataType::Int64,
330            ColumnDataType::Uint64,
331        );
332        assert_eq!(result, Err(TraceCoerceError::Unsupported));
333    }
334
335    #[test]
336    fn test_coerce_none_value() {
337        let result = coerce_value_data(&None, ColumnDataType::Float64, ColumnDataType::Int64);
338        assert_eq!(result, Ok(None));
339    }
340
341    #[test]
342    fn test_is_supported_trace_coercion() {
343        assert!(is_supported_trace_coercion(
344            ColumnDataType::Int64,
345            ColumnDataType::Float64
346        ));
347        assert!(is_supported_trace_coercion(
348            ColumnDataType::Int64,
349            ColumnDataType::String
350        ));
351        assert!(is_supported_trace_coercion(
352            ColumnDataType::Float64,
353            ColumnDataType::String
354        ));
355        assert!(is_supported_trace_coercion(
356            ColumnDataType::Boolean,
357            ColumnDataType::String
358        ));
359        assert!(is_supported_trace_coercion(
360            ColumnDataType::String,
361            ColumnDataType::Int64
362        ));
363        assert!(is_supported_trace_coercion(
364            ColumnDataType::String,
365            ColumnDataType::Float64
366        ));
367        assert!(is_supported_trace_coercion(
368            ColumnDataType::String,
369            ColumnDataType::Boolean
370        ));
371        assert!(!is_supported_trace_coercion(
372            ColumnDataType::Binary,
373            ColumnDataType::Json
374        ));
375        // Signed -> unsigned casts are supported (built-in no-ALTER transition).
376        assert!(is_supported_trace_coercion(
377            ColumnDataType::Int64,
378            ColumnDataType::Uint64
379        ));
380        assert!(is_supported_trace_coercion(
381            ColumnDataType::Int32,
382            ColumnDataType::Uint32
383        ));
384        // The reverse direction is intentionally not supported (lossy).
385        assert!(!is_supported_trace_coercion(
386            ColumnDataType::Uint64,
387            ColumnDataType::Int64
388        ));
389    }
390
391    #[test]
392    fn test_trace_value_datatype() {
393        assert_eq!(
394            trace_value_datatype(&ValueData::StringValue("x".to_string())),
395            Some(ColumnDataType::String)
396        );
397        assert_eq!(
398            trace_value_datatype(&ValueData::BoolValue(true)),
399            Some(ColumnDataType::Boolean)
400        );
401        assert_eq!(
402            trace_value_datatype(&ValueData::I64Value(1)),
403            Some(ColumnDataType::Int64)
404        );
405        assert_eq!(
406            trace_value_datatype(&ValueData::F64Value(1.0)),
407            Some(ColumnDataType::Float64)
408        );
409        assert_eq!(
410            trace_value_datatype(&ValueData::BinaryValue(vec![1_u8])),
411            Some(ColumnDataType::Binary)
412        );
413    }
414
415    #[test]
416    fn test_resolve_new_trace_column_type() {
417        assert_eq!(
418            resolve_new_trace_column_type([ColumnDataType::Int64]),
419            Ok(Some(ColumnDataType::Int64))
420        );
421        assert_eq!(
422            resolve_new_trace_column_type([ColumnDataType::String, ColumnDataType::Int64]),
423            Ok(Some(ColumnDataType::Int64))
424        );
425        assert_eq!(
426            resolve_new_trace_column_type([ColumnDataType::String, ColumnDataType::Float64]),
427            Ok(Some(ColumnDataType::Float64))
428        );
429        assert_eq!(
430            resolve_new_trace_column_type([ColumnDataType::String, ColumnDataType::Boolean]),
431            Ok(Some(ColumnDataType::Boolean))
432        );
433        assert_eq!(
434            resolve_new_trace_column_type([ColumnDataType::Int64, ColumnDataType::Float64]),
435            Ok(Some(ColumnDataType::Float64))
436        );
437        assert_eq!(
438            resolve_new_trace_column_type([
439                ColumnDataType::String,
440                ColumnDataType::Int64,
441                ColumnDataType::Float64,
442            ]),
443            Ok(Some(ColumnDataType::Float64))
444        );
445        assert_eq!(
446            resolve_new_trace_column_type([
447                ColumnDataType::Float64,
448                ColumnDataType::String,
449                ColumnDataType::Int64,
450            ]),
451            Ok(Some(ColumnDataType::Float64))
452        );
453    }
454}