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