1use std::collections::BTreeMap;
16
17use ahash::{HashMap, HashMapExt};
18use api::helper::{ColumnDataTypeWrapper, timestamp_unit};
19use api::v1::column_data_type_extension::TypeExt;
20use api::v1::column_def::options_from_column_schema;
21use api::v1::value::ValueData;
22use api::v1::{
23 ColumnDataType, ColumnDataTypeExtension, ColumnOptions, ColumnSchema, JsonTypeExtension, Row,
24 RowInsertRequest, Rows, SemanticType, Value as GreptimeValue,
25};
26use bytes::Bytes;
27use common_telemetry::warn;
28use common_time::Timestamp;
29use common_time::timestamp::TimeUnit;
30use jsonb::{Number as JsonbNumber, Value as JsonbValue};
31use opentelemetry_proto::tonic::collector::logs::v1::ExportLogsServiceRequest;
32use opentelemetry_proto::tonic::common::v1::{AnyValue, InstrumentationScope, KeyValue, any_value};
33use opentelemetry_proto::tonic::logs::v1::{LogRecord, ResourceLogs, ScopeLogs};
34use pipeline::{
35 ContextReq, GreptimePipelineParams, PipelineContext, PipelineWay, SchemaInfo, SelectInfo,
36};
37use session::context::QueryContextRef;
38use snafu::ensure;
39use vrl::prelude::NotNan;
40use vrl::value::{KeyString, Value as VrlValue};
41
42use crate::error::{
43 Error, IncompatibleSchemaSnafu, InvalidParameterSnafu, NotSupportedSnafu, Result,
44 UnsupportedJsonDataTypeForTagSnafu,
45};
46use crate::http::event::PipelineIngestRequest;
47use crate::otlp::coerce::{coerce_value_data, is_supported_signed_to_unsigned_coercion};
48use crate::otlp::trace::attributes::OtlpAnyValue;
49use crate::otlp::utils::{bytes_to_hex_string, key_value_to_jsonb};
50use crate::pipeline::run_pipeline;
51use crate::query_handler::PipelineHandlerRef;
52
53pub const LOG_TABLE_NAME: &str = "opentelemetry_logs";
54
55pub async fn to_grpc_insert_requests(
63 request: ExportLogsServiceRequest,
64 pipeline: PipelineWay,
65 pipeline_params: GreptimePipelineParams,
66 table_name: String,
67 query_ctx: &QueryContextRef,
68 pipeline_handler: PipelineHandlerRef,
69) -> Result<ContextReq> {
70 match pipeline {
71 PipelineWay::OtlpLogDirect(select_info) => {
72 let table = pipeline_handler
73 .get_table(&table_name, query_ctx)
74 .await
75 .map_err(Error::from)?;
76 let existing_schema = table
77 .as_deref()
78 .map(ExistingLogSchema::try_from_table)
79 .transpose()?;
80 let rows = parse_export_logs_service_request_to_rows(
81 request,
82 select_info,
83 existing_schema.as_ref(),
84 &table_name,
85 )?;
86 let insert_request = RowInsertRequest {
87 rows: Some(rows),
88 table_name,
89 };
90
91 Ok(ContextReq::default_opt_with_reqs(vec![insert_request]))
92 }
93 PipelineWay::Pipeline(pipeline_def) => {
94 let array = parse_export_logs_service_request(request);
95
96 let pipeline_ctx =
97 PipelineContext::new(&pipeline_def, &pipeline_params, query_ctx.channel());
98 run_pipeline(
99 &pipeline_handler,
100 &pipeline_ctx,
101 PipelineIngestRequest {
102 table: table_name,
103 values: array,
104 },
105 query_ctx,
106 true,
107 )
108 .await
109 }
110 _ => NotSupportedSnafu {
111 feat: "Unsupported pipeline for logs",
112 }
113 .fail(),
114 }
115}
116
117fn scope_to_pipeline_value(scope: Option<InstrumentationScope>) -> (VrlValue, VrlValue, VrlValue) {
118 scope
119 .map(|x| {
120 (
121 VrlValue::Object(key_value_to_map(x.attributes)),
122 VrlValue::Bytes(x.version.into()),
123 VrlValue::Bytes(x.name.into()),
124 )
125 })
126 .unwrap_or((VrlValue::Null, VrlValue::Null, VrlValue::Null))
127}
128
129fn scope_to_jsonb(
130 scope: Option<InstrumentationScope>,
131) -> (JsonbValue<'static>, Option<String>, Option<String>) {
132 scope
133 .map(|x| {
134 (
135 key_value_to_jsonb(x.attributes),
136 Some(x.version),
137 Some(x.name),
138 )
139 })
140 .unwrap_or((JsonbValue::Null, None, None))
141}
142
143fn log_to_pipeline_value(
144 log: LogRecord,
145 resource_schema_url: VrlValue,
146 resource_attr: VrlValue,
147 scope_schema_url: VrlValue,
148 scope_name: VrlValue,
149 scope_version: VrlValue,
150 scope_attrs: VrlValue,
151) -> VrlValue {
152 let log_attrs = VrlValue::Object(key_value_to_map(log.attributes));
153 let mut map = BTreeMap::new();
154 map.insert(
155 "Timestamp".into(),
156 VrlValue::Integer(log.time_unix_nano as i64),
157 );
158 map.insert(
159 "ObservedTimestamp".into(),
160 VrlValue::Integer(log.observed_time_unix_nano as i64),
161 );
162
163 map.insert(
165 "TraceId".into(),
166 VrlValue::Bytes(bytes_to_hex_string(&log.trace_id).into()),
167 );
168 map.insert(
169 "SpanId".into(),
170 VrlValue::Bytes(bytes_to_hex_string(&log.span_id).into()),
171 );
172 map.insert("TraceFlags".into(), VrlValue::Integer(log.flags as i64));
173 map.insert(
174 "SeverityText".into(),
175 VrlValue::Bytes(log.severity_text.into()),
176 );
177 map.insert(
178 "SeverityNumber".into(),
179 VrlValue::Integer(log.severity_number as i64),
180 );
181 map.insert(
183 "Body".into(),
184 log.body
185 .as_ref()
186 .map(|x| VrlValue::Bytes(log_body_to_string(x).into()))
187 .unwrap_or(VrlValue::Null),
188 );
189 map.insert("ResourceSchemaUrl".into(), resource_schema_url);
190
191 map.insert("ResourceAttributes".into(), resource_attr);
192 map.insert("ScopeSchemaUrl".into(), scope_schema_url);
193 map.insert("ScopeName".into(), scope_name);
194 map.insert("ScopeVersion".into(), scope_version);
195 map.insert("ScopeAttributes".into(), scope_attrs);
196 map.insert("LogAttributes".into(), log_attrs);
197 VrlValue::Object(map)
198}
199
200fn build_otlp_logs_identity_schema() -> Vec<ColumnSchema> {
201 [
202 (
203 "timestamp",
204 ColumnDataType::TimestampNanosecond,
205 SemanticType::Timestamp,
206 None,
207 None,
208 ),
209 (
210 "trace_id",
211 ColumnDataType::String,
212 SemanticType::Field,
213 None,
214 None,
215 ),
216 (
217 "span_id",
218 ColumnDataType::String,
219 SemanticType::Field,
220 None,
221 None,
222 ),
223 (
224 "severity_text",
225 ColumnDataType::String,
226 SemanticType::Field,
227 None,
228 None,
229 ),
230 (
231 "severity_number",
232 ColumnDataType::Int32,
233 SemanticType::Field,
234 None,
235 None,
236 ),
237 (
238 "body",
239 ColumnDataType::String,
240 SemanticType::Field,
241 None,
242 Some(ColumnOptions {
243 options: std::collections::HashMap::from([(
244 "fulltext".to_string(),
245 r#"{"enable":true}"#.to_string(),
246 )]),
247 }),
248 ),
249 (
250 "log_attributes",
251 ColumnDataType::Binary,
252 SemanticType::Field,
253 Some(ColumnDataTypeExtension {
254 type_ext: Some(TypeExt::JsonType(JsonTypeExtension::JsonBinary.into())),
255 }),
256 None,
257 ),
258 (
259 "trace_flags",
260 ColumnDataType::Int32,
261 SemanticType::Field,
262 None,
263 None,
264 ),
265 (
266 "scope_name",
267 ColumnDataType::String,
268 SemanticType::Tag,
269 None,
270 None,
271 ),
272 (
273 "scope_version",
274 ColumnDataType::String,
275 SemanticType::Field,
276 None,
277 None,
278 ),
279 (
280 "scope_attributes",
281 ColumnDataType::Binary,
282 SemanticType::Field,
283 Some(ColumnDataTypeExtension {
284 type_ext: Some(TypeExt::JsonType(JsonTypeExtension::JsonBinary.into())),
285 }),
286 None,
287 ),
288 (
289 "scope_schema_url",
290 ColumnDataType::String,
291 SemanticType::Field,
292 None,
293 None,
294 ),
295 (
296 "resource_attributes",
297 ColumnDataType::Binary,
298 SemanticType::Field,
299 Some(ColumnDataTypeExtension {
300 type_ext: Some(TypeExt::JsonType(JsonTypeExtension::JsonBinary.into())),
301 }),
302 None,
303 ),
304 (
305 "resource_schema_url",
306 ColumnDataType::String,
307 SemanticType::Field,
308 None,
309 None,
310 ),
311 ]
312 .into_iter()
313 .map(
314 |(field_name, column_type, semantic_type, datatype_extension, options)| ColumnSchema {
315 column_name: field_name.to_string(),
316 datatype: column_type as i32,
317 semantic_type: semantic_type as i32,
318 datatype_extension,
319 options,
320 },
321 )
322 .collect::<Vec<ColumnSchema>>()
323}
324
325#[derive(Clone)]
326struct ExistingLogColumn {
327 schema: ColumnSchema,
328 datatype: ColumnDataType,
329}
330
331impl ExistingLogColumn {
332 fn schema_for_request_type(&self, request_type: ColumnDataType) -> ColumnSchema {
333 let mut schema = self.schema.clone();
334 if request_type == ColumnDataType::Binary && self.is_json_binary() {
335 schema.datatype = ColumnDataType::Binary as i32;
336 }
337 schema
338 }
339
340 fn is_json_binary(&self) -> bool {
341 self.datatype == ColumnDataType::Json
342 && matches!(
343 self.schema
344 .datatype_extension
345 .as_ref()
346 .and_then(|datatype_extension| datatype_extension.type_ext.as_ref()),
347 Some(TypeExt::JsonType(json_type))
348 if *json_type == JsonTypeExtension::JsonBinary as i32
349 )
350 }
351}
352
353#[derive(Default)]
354struct ExistingLogSchema {
355 columns: HashMap<String, ExistingLogColumn>,
356}
357
358impl ExistingLogSchema {
359 fn try_from_table(table: &table::Table) -> Result<Self> {
360 let table_info = table.table_info();
361 Self::try_from_schema_parts(
362 table.schema_ref().column_schemas(),
363 &table_info.meta.primary_key_indices,
364 )
365 }
366
367 fn try_from_schema_parts(
368 column_schemas: &[datatypes::schema::ColumnSchema],
369 primary_key_indices: &[usize],
370 ) -> Result<Self> {
371 let mut columns = HashMap::with_capacity(column_schemas.len());
372
373 for (index, column_schema) in column_schemas.iter().enumerate() {
374 let (datatype, datatype_extension) =
375 ColumnDataTypeWrapper::try_from(column_schema.data_type.clone())
376 .map(|wrapper| wrapper.into_parts())
377 .map_err(Error::from)?;
378 let semantic_type = if column_schema.is_time_index() {
379 SemanticType::Timestamp
380 } else if primary_key_indices.contains(&index) {
381 SemanticType::Tag
382 } else {
383 SemanticType::Field
384 };
385 let schema = ColumnSchema {
386 column_name: column_schema.name.clone(),
387 datatype: datatype as i32,
388 semantic_type: semantic_type as i32,
389 datatype_extension,
390 options: options_from_column_schema(column_schema),
391 };
392 columns.insert(
393 schema.column_name.clone(),
394 ExistingLogColumn { schema, datatype },
395 );
396 }
397
398 Ok(Self { columns })
399 }
400
401 fn get(&self, column_name: &str) -> Option<&ExistingLogColumn> {
402 self.columns.get(column_name)
403 }
404}
405
406fn build_otlp_build_in_row(
407 log: LogRecord,
408 parse_ctx: &mut ParseContext,
409) -> (Row, JsonbValue<'static>) {
410 let log_attr = key_value_to_jsonb(log.attributes);
411 let ts = if log.time_unix_nano != 0 {
412 log.time_unix_nano
413 } else {
414 log.observed_time_unix_nano
415 };
416
417 let row = vec![
418 GreptimeValue {
419 value_data: Some(ValueData::TimestampNanosecondValue(ts as i64)),
420 },
421 GreptimeValue {
422 value_data: Some(ValueData::StringValue(bytes_to_hex_string(&log.trace_id))),
423 },
424 GreptimeValue {
425 value_data: Some(ValueData::StringValue(bytes_to_hex_string(&log.span_id))),
426 },
427 GreptimeValue {
428 value_data: Some(ValueData::StringValue(log.severity_text)),
429 },
430 GreptimeValue {
431 value_data: Some(ValueData::I32Value(log.severity_number)),
432 },
433 GreptimeValue {
434 value_data: log
435 .body
436 .as_ref()
437 .map(|x| ValueData::StringValue(log_body_to_string(x))),
438 },
439 GreptimeValue {
440 value_data: Some(ValueData::BinaryValue(log_attr.to_vec())),
441 },
442 GreptimeValue {
443 value_data: Some(ValueData::I32Value(log.flags as i32)),
444 },
445 GreptimeValue {
446 value_data: parse_ctx.scope_name.clone().map(ValueData::StringValue),
447 },
448 GreptimeValue {
449 value_data: parse_ctx.scope_version.clone().map(ValueData::StringValue),
450 },
451 GreptimeValue {
452 value_data: Some(ValueData::BinaryValue(parse_ctx.scope_attrs.to_vec())),
453 },
454 GreptimeValue {
455 value_data: Some(ValueData::StringValue(parse_ctx.scope_url.clone())),
456 },
457 GreptimeValue {
458 value_data: Some(ValueData::BinaryValue(parse_ctx.resource_attr.to_vec())),
459 },
460 GreptimeValue {
461 value_data: Some(ValueData::StringValue(parse_ctx.resource_url.clone())),
462 },
463 ];
464 (Row { values: row }, log_attr)
465}
466
467fn extract_field_from_attr_and_combine_schema(
468 select_info: &SelectInfo,
469 select_schema: &mut SchemaInfo,
470 attrs: &jsonb::Value,
471 existing_schema: Option<&ExistingLogSchema>,
472 table_name: &str,
473) -> Result<Vec<GreptimeValue>> {
474 let mut extracted_values = vec![GreptimeValue::default(); select_schema.schema.len()];
477
478 for key in select_info.keys.iter() {
479 let Some(value) = attrs.get_by_name_ignore_case(key).cloned() else {
480 continue;
481 };
482 let Some((schema, value)) =
483 decide_column_schema_and_convert_value(key, value, existing_schema, table_name)?
484 else {
485 continue;
486 };
487
488 if let Some(index) = select_schema.index.get(key) {
489 let column_schema = &select_schema.schema[*index];
490 let column_schema: ColumnSchema = column_schema.clone().try_into()?;
491 ensure!(
493 column_schema.datatype == schema.datatype,
494 IncompatibleSchemaSnafu {
495 column_name: key,
496 datatype: column_schema.datatype().as_str_name(),
497 expected: column_schema.datatype,
498 actual: schema.datatype,
499 }
500 );
501 extracted_values[*index] = value;
502 } else {
503 select_schema.schema.push(schema.into());
504 select_schema
505 .index
506 .insert(key.clone(), select_schema.schema.len() - 1);
507 extracted_values.push(value);
508 }
509 }
510
511 Ok(extracted_values)
512}
513
514fn decide_column_schema_and_convert_value(
515 column_name: &str,
516 value: JsonbValue,
517 existing_schema: Option<&ExistingLogSchema>,
518 table_name: &str,
519) -> Result<Option<(ColumnSchema, GreptimeValue)>> {
520 if let Some(existing_column) = existing_schema.and_then(|schema| schema.get(column_name)) {
521 return decide_existing_column_schema_and_convert_value(
522 column_name,
523 value,
524 existing_column,
525 table_name,
526 );
527 }
528
529 let column_info = match value {
530 JsonbValue::String(s) => Ok(Some((
531 GreptimeValue {
532 value_data: Some(ValueData::StringValue(s.into())),
533 },
534 ColumnDataType::String,
535 SemanticType::Tag,
536 None,
537 ))),
538 JsonbValue::Number(n) => match n {
539 JsonbNumber::Int64(i) => Ok(Some((
540 GreptimeValue {
541 value_data: Some(ValueData::I64Value(i)),
542 },
543 ColumnDataType::Int64,
544 SemanticType::Tag,
545 None,
546 ))),
547 JsonbNumber::Float64(_) => UnsupportedJsonDataTypeForTagSnafu {
548 ty: "FLOAT".to_string(),
549 key: column_name,
550 }
551 .fail(),
552 JsonbNumber::Decimal64(_) | JsonbNumber::Decimal128(_) | JsonbNumber::Decimal256(_) => {
553 UnsupportedJsonDataTypeForTagSnafu {
554 ty: "DECIMAL".to_string(),
555 key: column_name,
556 }
557 .fail()
558 }
559 JsonbNumber::UInt64(u) => {
560 let value = jsonb_uint64_to_log_value(u, column_name)?;
561 Ok(Some((
562 GreptimeValue {
563 value_data: Some(ValueData::I64Value(value)),
564 },
565 ColumnDataType::Int64,
566 SemanticType::Tag,
567 None,
568 )))
569 }
570 },
571 JsonbValue::Bool(b) => Ok(Some((
572 GreptimeValue {
573 value_data: Some(ValueData::BoolValue(b)),
574 },
575 ColumnDataType::Boolean,
576 SemanticType::Tag,
577 None,
578 ))),
579 JsonbValue::Array(_) | JsonbValue::Object(_) => UnsupportedJsonDataTypeForTagSnafu {
580 ty: "Json".to_string(),
581 key: column_name,
582 }
583 .fail(),
584 JsonbValue::Binary(_) => UnsupportedJsonDataTypeForTagSnafu {
585 ty: "Binary".to_string(),
586 key: column_name,
587 }
588 .fail(),
589 JsonbValue::Date(_) => UnsupportedJsonDataTypeForTagSnafu {
590 ty: "Date".to_string(),
591 key: column_name,
592 }
593 .fail(),
594 JsonbValue::Timestamp(_) => UnsupportedJsonDataTypeForTagSnafu {
595 ty: "Timestamp".to_string(),
596 key: column_name,
597 }
598 .fail(),
599 JsonbValue::TimestampTz(_) => UnsupportedJsonDataTypeForTagSnafu {
600 ty: "TimestampTz".to_string(),
601 key: column_name,
602 }
603 .fail(),
604 JsonbValue::Interval(_) => UnsupportedJsonDataTypeForTagSnafu {
605 ty: "Interval".to_string(),
606 key: column_name,
607 }
608 .fail(),
609 JsonbValue::Null => Ok(None),
610 };
611 column_info.map(|c| {
612 c.map(|(value, column_type, semantic_type, datatype_extension)| {
613 (
614 ColumnSchema {
615 column_name: column_name.to_string(),
616 datatype: column_type as i32,
617 semantic_type: semantic_type as i32,
618 datatype_extension,
619 options: None,
620 },
621 value,
622 )
623 })
624 })
625}
626
627fn decide_existing_column_schema_and_convert_value(
628 column_name: &str,
629 value: JsonbValue,
630 existing_column: &ExistingLogColumn,
631 table_name: &str,
632) -> Result<Option<(ColumnSchema, GreptimeValue)>> {
633 let Some((value_data, request_type)) = jsonb_value_to_log_value_data(column_name, value, true)?
634 else {
635 return Ok(None);
636 };
637 let value_data = coerce_log_value_data(
638 Some(value_data),
639 existing_column.datatype,
640 existing_column.schema.semantic_type(),
641 request_type,
642 existing_column.is_json_binary(),
643 column_name,
644 table_name,
645 )?;
646
647 Ok(Some((
648 existing_column.schema.clone(),
649 GreptimeValue { value_data },
650 )))
651}
652
653fn jsonb_uint64_to_log_value(u: u64, column_name: &str) -> Result<i64> {
660 i64::try_from(u).map_err(|_| InvalidParameterSnafu {
661 reason: format!(
662 "uint64 value {u} in column '{column_name}' exceeds the i64 range supported by built-in log columns"
663 ),
664 }
665 .build())
666}
667
668fn jsonb_value_to_log_value_data(
669 column_name: &str,
670 value: JsonbValue,
671 allow_float: bool,
672) -> Result<Option<(ValueData, ColumnDataType)>> {
673 match value {
674 JsonbValue::String(s) => Ok(Some((
675 ValueData::StringValue(s.into()),
676 ColumnDataType::String,
677 ))),
678 JsonbValue::Number(n) => match n {
679 JsonbNumber::Int64(i) => Ok(Some((ValueData::I64Value(i), ColumnDataType::Int64))),
680 JsonbNumber::Float64(f) if allow_float => {
681 Ok(Some((ValueData::F64Value(f), ColumnDataType::Float64)))
682 }
683 JsonbNumber::Float64(_) => UnsupportedJsonDataTypeForTagSnafu {
684 ty: "FLOAT".to_string(),
685 key: column_name,
686 }
687 .fail(),
688 JsonbNumber::Decimal64(_) | JsonbNumber::Decimal128(_) | JsonbNumber::Decimal256(_) => {
689 UnsupportedJsonDataTypeForTagSnafu {
690 ty: "DECIMAL".to_string(),
691 key: column_name,
692 }
693 .fail()
694 }
695 JsonbNumber::UInt64(u) => Ok(Some((
696 ValueData::I64Value(jsonb_uint64_to_log_value(u, column_name)?),
697 ColumnDataType::Int64,
698 ))),
699 },
700 JsonbValue::Bool(b) => Ok(Some((ValueData::BoolValue(b), ColumnDataType::Boolean))),
701 JsonbValue::Array(_) | JsonbValue::Object(_) => UnsupportedJsonDataTypeForTagSnafu {
702 ty: "Json".to_string(),
703 key: column_name,
704 }
705 .fail(),
706 JsonbValue::Binary(_) => UnsupportedJsonDataTypeForTagSnafu {
707 ty: "Binary".to_string(),
708 key: column_name,
709 }
710 .fail(),
711 JsonbValue::Date(_) => UnsupportedJsonDataTypeForTagSnafu {
712 ty: "Date".to_string(),
713 key: column_name,
714 }
715 .fail(),
716 JsonbValue::Timestamp(_) => UnsupportedJsonDataTypeForTagSnafu {
717 ty: "Timestamp".to_string(),
718 key: column_name,
719 }
720 .fail(),
721 JsonbValue::TimestampTz(_) => UnsupportedJsonDataTypeForTagSnafu {
722 ty: "TimestampTz".to_string(),
723 key: column_name,
724 }
725 .fail(),
726 JsonbValue::Interval(_) => UnsupportedJsonDataTypeForTagSnafu {
727 ty: "Interval".to_string(),
728 key: column_name,
729 }
730 .fail(),
731 JsonbValue::Null => Ok(None),
732 }
733}
734
735fn align_rows_with_existing_schema(
736 schemas: &mut [ColumnSchema],
737 rows: &mut [Row],
738 existing_schema: Option<&ExistingLogSchema>,
739 table_name: &str,
740) -> Result<()> {
741 let Some(existing_schema) = existing_schema else {
742 return Ok(());
743 };
744
745 for (column_idx, schema) in schemas.iter_mut().enumerate() {
746 let request_type = schema.datatype();
747 let Some(existing_column) = existing_schema.get(&schema.column_name) else {
748 if schema.semantic_type() == SemanticType::Tag {
751 schema.semantic_type = SemanticType::Field as i32;
752 }
753 continue;
754 };
755
756 let target_type = existing_column.datatype;
757 let semantic_type = existing_column.schema.semantic_type();
758 let target_is_json_binary = existing_column.is_json_binary();
759 for row in rows.iter_mut() {
760 let Some(value) = row.values.get_mut(column_idx) else {
761 continue;
762 };
763 value.value_data = coerce_log_value_data(
764 value.value_data.take(),
765 target_type,
766 semantic_type,
767 request_type,
768 target_is_json_binary,
769 &schema.column_name,
770 table_name,
771 )?;
772 }
773 *schema = existing_column.schema_for_request_type(request_type);
774 }
775
776 Ok(())
777}
778
779fn coerce_log_value_data(
780 value_data: Option<ValueData>,
781 target_type: ColumnDataType,
782 _semantic_type: SemanticType,
783 request_type: ColumnDataType,
784 target_is_json_binary: bool,
785 column_name: &str,
786 table_name: &str,
787) -> Result<Option<ValueData>> {
788 let Some(value_data) = value_data else {
789 return Ok(None);
790 };
791
792 if request_type == target_type {
793 return Ok(Some(value_data));
794 }
795
796 if request_type == ColumnDataType::Binary && target_is_json_binary {
797 return Ok(Some(value_data));
798 }
799
800 if is_timestamp_type(request_type)
801 && let Some(target_unit) = timestamp_unit(target_type)
802 {
803 return align_timestamp_value(value_data, target_unit, column_name, table_name).map(Some);
804 }
805
806 if target_type == ColumnDataType::String {
815 if let Ok(value_data) =
816 coerce_value_data(&Some(value_data.clone()), target_type, request_type)
817 {
818 return Ok(value_data);
819 }
820 if let Some(value_data) = stringify_scalar_value(value_data) {
821 return Ok(Some(value_data));
822 }
823 } else if is_supported_signed_to_unsigned_coercion(request_type, target_type)
824 && let Ok(Some(value_data)) =
825 coerce_value_data(&Some(value_data), target_type, request_type)
826 {
827 return Ok(Some(value_data));
828 }
829
830 InvalidParameterSnafu {
831 reason: format!(
832 "failed to align log column '{}' in table '{}' from {:?} to {:?}",
833 column_name, table_name, request_type, target_type
834 ),
835 }
836 .fail()
837}
838
839fn stringify_scalar_value(value_data: ValueData) -> Option<ValueData> {
840 let value = match value_data {
841 ValueData::StringValue(value) => value,
842 ValueData::BoolValue(value) => value.to_string(),
843 ValueData::I8Value(value) => value.to_string(),
844 ValueData::I16Value(value) => value.to_string(),
845 ValueData::I32Value(value) => value.to_string(),
846 ValueData::I64Value(value) => value.to_string(),
847 ValueData::U8Value(value) => value.to_string(),
848 ValueData::U16Value(value) => value.to_string(),
849 ValueData::U32Value(value) => value.to_string(),
850 ValueData::U64Value(value) => value.to_string(),
851 ValueData::F32Value(value) => value.to_string(),
852 ValueData::F64Value(value) => value.to_string(),
853 _ => return None,
854 };
855 Some(ValueData::StringValue(value))
856}
857
858fn align_timestamp_value(
859 value_data: ValueData,
860 target_unit: TimeUnit,
861 column_name: &str,
862 table_name: &str,
863) -> Result<ValueData> {
864 let timestamp = match value_data {
865 ValueData::TimestampSecondValue(value) => Timestamp::new_second(value),
866 ValueData::TimestampMillisecondValue(value) => Timestamp::new_millisecond(value),
867 ValueData::TimestampMicrosecondValue(value) => Timestamp::new_microsecond(value),
868 ValueData::TimestampNanosecondValue(value) => Timestamp::new_nanosecond(value),
869 value_data => {
870 return InvalidParameterSnafu {
871 reason: format!(
872 "failed to align log column '{}' in table '{}' from non-timestamp value {:?}",
873 column_name, table_name, value_data
874 ),
875 }
876 .fail();
877 }
878 };
879 let timestamp = timestamp.convert_to(target_unit).ok_or_else(|| {
880 InvalidParameterSnafu {
881 reason: format!(
882 "failed to align log column '{}' in table '{}' to timestamp unit {}",
883 column_name, table_name, target_unit
884 ),
885 }
886 .build()
887 })?;
888
889 Ok(match target_unit {
890 TimeUnit::Second => ValueData::TimestampSecondValue(timestamp.value()),
891 TimeUnit::Millisecond => ValueData::TimestampMillisecondValue(timestamp.value()),
892 TimeUnit::Microsecond => ValueData::TimestampMicrosecondValue(timestamp.value()),
893 TimeUnit::Nanosecond => ValueData::TimestampNanosecondValue(timestamp.value()),
894 })
895}
896
897fn is_timestamp_type(datatype: ColumnDataType) -> bool {
898 timestamp_unit(datatype).is_some()
899}
900
901fn parse_export_logs_service_request_to_rows(
902 request: ExportLogsServiceRequest,
903 select_info: Box<SelectInfo>,
904 existing_schema: Option<&ExistingLogSchema>,
905 table_name: &str,
906) -> Result<Rows> {
907 let mut schemas = build_otlp_logs_identity_schema();
908
909 let mut parse_ctx = ParseContext::new(select_info, existing_schema, table_name);
910 let mut rows = parse_resource(&mut parse_ctx, request.resource_logs)?;
911
912 schemas.extend(parse_ctx.select_schema.column_schemas()?);
913 align_rows_with_existing_schema(&mut schemas, &mut rows, existing_schema, table_name)?;
914
915 rows.iter_mut().for_each(|row| {
916 row.values.resize(schemas.len(), GreptimeValue::default());
917 });
918
919 Ok(Rows {
920 schema: schemas,
921 rows,
922 })
923}
924
925fn parse_resource(
926 parse_ctx: &mut ParseContext,
927 resource_logs_vec: Vec<ResourceLogs>,
928) -> Result<Vec<Row>> {
929 let total_len = resource_logs_vec
930 .iter()
931 .flat_map(|r| r.scope_logs.iter())
932 .map(|s| s.log_records.len())
933 .sum();
934
935 let mut results = Vec::with_capacity(total_len);
936
937 for r in resource_logs_vec {
938 parse_ctx.resource_attr = r
939 .resource
940 .map(|resource| key_value_to_jsonb(resource.attributes))
941 .unwrap_or(JsonbValue::Null);
942
943 parse_ctx.resource_url = r.schema_url;
944
945 parse_ctx.resource_uplift_values = extract_field_from_attr_and_combine_schema(
946 &parse_ctx.select_info,
947 &mut parse_ctx.select_schema,
948 &parse_ctx.resource_attr,
949 parse_ctx.existing_schema,
950 parse_ctx.table_name,
951 )?;
952
953 let rows = parse_scope(r.scope_logs, parse_ctx)?;
954 results.extend(rows);
955 }
956 Ok(results)
957}
958
959struct ParseContext<'a> {
960 select_info: Box<SelectInfo>,
962 existing_schema: Option<&'a ExistingLogSchema>,
963 table_name: &'a str,
964 select_schema: SchemaInfo,
967
968 resource_uplift_values: Vec<GreptimeValue>,
970 scope_uplift_values: Vec<GreptimeValue>,
971
972 resource_url: String,
974 resource_attr: JsonbValue<'a>,
975 scope_name: Option<String>,
976 scope_version: Option<String>,
977 scope_url: String,
978 scope_attrs: JsonbValue<'a>,
979}
980
981impl<'a> ParseContext<'a> {
982 pub fn new(
983 select_info: Box<SelectInfo>,
984 existing_schema: Option<&'a ExistingLogSchema>,
985 table_name: &'a str,
986 ) -> ParseContext<'a> {
987 let len = select_info.keys.len();
988 ParseContext {
989 select_info,
990 existing_schema,
991 table_name,
992 select_schema: SchemaInfo::with_capacity(len),
993 resource_uplift_values: vec![],
994 scope_uplift_values: vec![],
995 resource_url: String::new(),
996 resource_attr: JsonbValue::Null,
997 scope_name: None,
998 scope_version: None,
999 scope_url: String::new(),
1000 scope_attrs: JsonbValue::Null,
1001 }
1002 }
1003}
1004
1005fn parse_scope(scopes_log_vec: Vec<ScopeLogs>, parse_ctx: &mut ParseContext) -> Result<Vec<Row>> {
1006 let len = scopes_log_vec.iter().map(|l| l.log_records.len()).sum();
1007 let mut results = Vec::with_capacity(len);
1008
1009 for scope_logs in scopes_log_vec {
1010 let (scope_attrs, scope_version, scope_name) = scope_to_jsonb(scope_logs.scope);
1011 parse_ctx.scope_name = scope_name;
1012 parse_ctx.scope_version = scope_version;
1013 parse_ctx.scope_url = scope_logs.schema_url;
1014 parse_ctx.scope_attrs = scope_attrs;
1015
1016 parse_ctx.scope_uplift_values = extract_field_from_attr_and_combine_schema(
1017 &parse_ctx.select_info,
1018 &mut parse_ctx.select_schema,
1019 &parse_ctx.scope_attrs,
1020 parse_ctx.existing_schema,
1021 parse_ctx.table_name,
1022 )?;
1023
1024 let rows = parse_log(scope_logs.log_records, parse_ctx)?;
1025 results.extend(rows);
1026 }
1027 Ok(results)
1028}
1029
1030fn parse_log(log_records: Vec<LogRecord>, parse_ctx: &mut ParseContext) -> Result<Vec<Row>> {
1031 let mut result = Vec::with_capacity(log_records.len());
1032
1033 for log in log_records {
1034 let (mut row, log_attr) = build_otlp_build_in_row(log, parse_ctx);
1035
1036 let log_values = extract_field_from_attr_and_combine_schema(
1037 &parse_ctx.select_info,
1038 &mut parse_ctx.select_schema,
1039 &log_attr,
1040 parse_ctx.existing_schema,
1041 parse_ctx.table_name,
1042 )?;
1043
1044 let extracted_values = merge_values(
1045 log_values,
1046 &parse_ctx.scope_uplift_values,
1047 &parse_ctx.resource_uplift_values,
1048 );
1049
1050 row.values.extend(extracted_values);
1051
1052 result.push(row);
1053 }
1054 Ok(result)
1055}
1056
1057fn merge_values(
1058 log: Vec<GreptimeValue>,
1059 scope: &[GreptimeValue],
1060 resource: &[GreptimeValue],
1061) -> Vec<GreptimeValue> {
1062 log.into_iter()
1063 .enumerate()
1064 .map(|(i, value)| GreptimeValue {
1065 value_data: value
1066 .value_data
1067 .or_else(|| scope.get(i).and_then(|x| x.value_data.clone()))
1068 .or_else(|| resource.get(i).and_then(|x| x.value_data.clone())),
1069 })
1070 .collect()
1071}
1072
1073fn parse_export_logs_service_request(request: ExportLogsServiceRequest) -> Vec<VrlValue> {
1076 let mut result = Vec::new();
1077 for r in request.resource_logs {
1078 let resource_attr = r
1079 .resource
1080 .map(|x| VrlValue::Object(key_value_to_map(x.attributes)))
1081 .unwrap_or(VrlValue::Null);
1082 let resource_schema_url = VrlValue::Bytes(r.schema_url.into());
1083 for scope_logs in r.scope_logs {
1084 let (scope_attrs, scope_version, scope_name) =
1085 scope_to_pipeline_value(scope_logs.scope);
1086 let scope_schema_url = VrlValue::Bytes(scope_logs.schema_url.into());
1087 for log in scope_logs.log_records {
1088 let value = log_to_pipeline_value(
1089 log,
1090 resource_schema_url.clone(),
1091 resource_attr.clone(),
1092 scope_schema_url.clone(),
1093 scope_name.clone(),
1094 scope_version.clone(),
1095 scope_attrs.clone(),
1096 );
1097 result.push(value);
1098 }
1099 }
1100 }
1101 result
1102}
1103
1104fn any_value_to_vrl_value(value: any_value::Value) -> VrlValue {
1106 match value {
1107 any_value::Value::StringValue(s) => VrlValue::Bytes(s.into()),
1108 any_value::Value::IntValue(i) => VrlValue::Integer(i),
1109 any_value::Value::DoubleValue(d) => VrlValue::Float(NotNan::new(d).unwrap()),
1110 any_value::Value::BoolValue(b) => VrlValue::Boolean(b),
1111 any_value::Value::ArrayValue(array_value) => {
1112 let values = array_value
1113 .values
1114 .into_iter()
1115 .filter_map(|v| v.value.map(any_value_to_vrl_value))
1116 .collect();
1117 VrlValue::Array(values)
1118 }
1119 any_value::Value::KvlistValue(key_value_list) => {
1120 VrlValue::Object(key_value_to_map(key_value_list.values))
1121 }
1122 any_value::Value::BytesValue(items) => VrlValue::Bytes(Bytes::from(items)),
1123 any_value::Value::StringValueStrindex(_) => {
1128 warn!(
1129 "encountered a profiling-only `StringValueStrindex` value in a non-profiling signal; ignoring"
1130 );
1131 VrlValue::Null
1132 }
1133 }
1134}
1135
1136fn key_value_to_map(key_values: Vec<KeyValue>) -> BTreeMap<KeyString, VrlValue> {
1138 let mut map = BTreeMap::new();
1139 for kv in key_values {
1140 let value = match kv.value {
1141 Some(value) => match value.value {
1142 Some(value) => any_value_to_vrl_value(value),
1143 None => VrlValue::Null,
1144 },
1145 None => VrlValue::Null,
1146 };
1147 map.insert(kv.key.into(), value);
1148 }
1149 map
1150}
1151
1152fn log_body_to_string(body: &AnyValue) -> String {
1153 let otlp_value = OtlpAnyValue::from(body);
1154 otlp_value.to_string()
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159 use datatypes::prelude::ConcreteDataType;
1160 use datatypes::schema::ColumnSchema as DatatypesColumnSchema;
1161 use opentelemetry_proto::tonic::common::v1::any_value::Value as OtlpValue;
1162
1163 use super::*;
1164
1165 fn time_column(datatype: ConcreteDataType) -> DatatypesColumnSchema {
1166 DatatypesColumnSchema::new("timestamp", datatype, false).with_time_index(true)
1167 }
1168
1169 fn column(name: &str, datatype: ConcreteDataType) -> DatatypesColumnSchema {
1170 DatatypesColumnSchema::new(name, datatype, true)
1171 }
1172
1173 fn existing_schema(
1174 columns: Vec<DatatypesColumnSchema>,
1175 primary_key_indices: &[usize],
1176 ) -> ExistingLogSchema {
1177 ExistingLogSchema::try_from_schema_parts(&columns, primary_key_indices).unwrap()
1178 }
1179
1180 fn existing_uint32_trace_flags_schema() -> ExistingLogSchema {
1181 existing_schema(
1182 vec![
1183 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1184 column("trace_flags", ConcreteDataType::uint32_datatype()),
1185 ],
1186 &[],
1187 )
1188 }
1189
1190 fn kv(key: &str, value: OtlpValue) -> KeyValue {
1191 KeyValue {
1192 key: key.to_string(),
1193 value: Some(AnyValue { value: Some(value) }),
1194 ..Default::default()
1195 }
1196 }
1197
1198 fn request_with_log_attrs(attrs: Vec<KeyValue>) -> ExportLogsServiceRequest {
1199 request_with_log_attrs_and_flags(attrs, 0)
1200 }
1201
1202 fn request_with_log_attrs_and_flags(
1203 attrs: Vec<KeyValue>,
1204 flags: u32,
1205 ) -> ExportLogsServiceRequest {
1206 ExportLogsServiceRequest {
1207 resource_logs: vec![ResourceLogs {
1208 scope_logs: vec![ScopeLogs {
1209 log_records: vec![LogRecord {
1210 time_unix_nano: 1_234_000_000,
1211 trace_id: vec![1; 16],
1212 flags,
1213 attributes: attrs,
1214 ..Default::default()
1215 }],
1216 ..Default::default()
1217 }],
1218 ..Default::default()
1219 }],
1220 }
1221 }
1222
1223 fn parse_with_select(
1224 request: ExportLogsServiceRequest,
1225 select: &str,
1226 existing_schema: Option<&ExistingLogSchema>,
1227 ) -> Result<Rows> {
1228 parse_export_logs_service_request_to_rows(
1229 request,
1230 Box::new(SelectInfo::from(select.to_string())),
1231 existing_schema,
1232 "test_logs",
1233 )
1234 }
1235
1236 fn column_index(rows: &Rows, name: &str) -> usize {
1237 rows.schema
1238 .iter()
1239 .position(|schema| schema.column_name == name)
1240 .unwrap()
1241 }
1242
1243 #[test]
1244 fn test_no_existing_table_preserves_direct_schema() {
1245 let rows = parse_with_select(request_with_log_attrs(vec![]), "", None).unwrap();
1246
1247 assert_eq!(rows.schema[0].column_name, "timestamp");
1248 assert_eq!(
1249 rows.schema[0].datatype,
1250 ColumnDataType::TimestampNanosecond as i32
1251 );
1252 assert_eq!(rows.schema[0].semantic_type, SemanticType::Timestamp as i32);
1253 let scope_name_idx = column_index(&rows, "scope_name");
1254 assert_eq!(
1255 rows.schema[scope_name_idx].semantic_type,
1256 SemanticType::Tag as i32
1257 );
1258 }
1259
1260 #[test]
1261 fn test_fresh_table_trace_flags_is_int32() {
1262 let rows = parse_with_select(request_with_log_attrs(vec![]), "", None).unwrap();
1266 let idx = column_index(&rows, "trace_flags");
1267
1268 assert_eq!(rows.schema[idx].datatype, ColumnDataType::Int32 as i32);
1269 assert_eq!(
1270 rows.rows[0].values[idx].value_data,
1271 Some(ValueData::I32Value(0))
1272 );
1273 }
1274
1275 #[test]
1276 fn test_existing_uint32_trace_flags_keeps_type_and_coerces_int32_request() {
1277 let existing = existing_uint32_trace_flags_schema();
1280
1281 let rows = parse_with_select(request_with_log_attrs(vec![]), "", Some(&existing)).unwrap();
1282 let idx = column_index(&rows, "trace_flags");
1283
1284 assert_eq!(rows.schema[idx].datatype, ColumnDataType::Uint32 as i32);
1285 assert_eq!(
1286 rows.rows[0].values[idx].value_data,
1287 Some(ValueData::U32Value(0))
1288 );
1289 }
1290
1291 #[test]
1292 fn test_existing_uint32_trace_flags_preserves_nonzero_flags_value() {
1293 let existing = existing_uint32_trace_flags_schema();
1298
1299 let rows = parse_with_select(
1300 request_with_log_attrs_and_flags(vec![], 256),
1301 "",
1302 Some(&existing),
1303 )
1304 .unwrap();
1305 let idx = column_index(&rows, "trace_flags");
1306
1307 assert_eq!(rows.schema[idx].datatype, ColumnDataType::Uint32 as i32);
1308 assert_eq!(
1309 rows.rows[0].values[idx].value_data,
1310 Some(ValueData::U32Value(256))
1311 );
1312 }
1313
1314 #[test]
1315 fn test_existing_uint32_trace_flags_preserves_high_bit_flags_value() {
1316 let existing = existing_uint32_trace_flags_schema();
1321
1322 let rows = parse_with_select(
1323 request_with_log_attrs_and_flags(vec![], 0x8000_0000),
1324 "",
1325 Some(&existing),
1326 )
1327 .unwrap();
1328 let idx = column_index(&rows, "trace_flags");
1329
1330 assert_eq!(rows.schema[idx].datatype, ColumnDataType::Uint32 as i32);
1331 assert_eq!(
1332 rows.rows[0].values[idx].value_data,
1333 Some(ValueData::U32Value(0x8000_0000))
1334 );
1335 }
1336
1337 #[test]
1338 fn test_existing_primary_key_updates_builtin_column_semantic_type() {
1339 let existing = existing_schema(
1340 vec![
1341 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1342 column("trace_id", ConcreteDataType::string_datatype()),
1343 ],
1344 &[1],
1345 );
1346
1347 let rows = parse_with_select(request_with_log_attrs(vec![]), "", Some(&existing)).unwrap();
1348 let trace_id_idx = column_index(&rows, "trace_id");
1349
1350 assert_eq!(
1351 rows.schema[trace_id_idx].semantic_type,
1352 SemanticType::Tag as i32
1353 );
1354 }
1355
1356 #[test]
1357 fn test_existing_string_primary_key_stringifies_selected_scalar_values() {
1358 let existing = existing_schema(
1359 vec![
1360 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1361 column("host", ConcreteDataType::string_datatype()),
1362 ],
1363 &[1],
1364 );
1365 let rows = parse_with_select(
1366 request_with_log_attrs(vec![kv("host", OtlpValue::IntValue(42))]),
1367 "host",
1368 Some(&existing),
1369 )
1370 .unwrap();
1371 let host_idx = column_index(&rows, "host");
1372
1373 assert_eq!(
1374 rows.schema[host_idx].datatype,
1375 ColumnDataType::String as i32
1376 );
1377 assert_eq!(
1378 rows.schema[host_idx].semantic_type,
1379 SemanticType::Tag as i32
1380 );
1381 assert_eq!(
1382 rows.rows[0].values[host_idx].value_data,
1383 Some(ValueData::StringValue("42".to_string()))
1384 );
1385 }
1386
1387 #[test]
1388 fn test_existing_string_field_stringifies_selected_scalar_values() {
1389 let existing = existing_schema(
1390 vec![
1391 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1392 column("host", ConcreteDataType::string_datatype()),
1393 ],
1394 &[],
1395 );
1396 let rows = parse_with_select(
1397 request_with_log_attrs(vec![kv("host", OtlpValue::IntValue(42))]),
1398 "host",
1399 Some(&existing),
1400 )
1401 .unwrap();
1402 let host_idx = column_index(&rows, "host");
1403
1404 assert_eq!(
1405 rows.schema[host_idx].datatype,
1406 ColumnDataType::String as i32
1407 );
1408 assert_eq!(
1409 rows.schema[host_idx].semantic_type,
1410 SemanticType::Field as i32
1411 );
1412 assert_eq!(
1413 rows.rows[0].values[host_idx].value_data,
1414 Some(ValueData::StringValue("42".to_string()))
1415 );
1416 }
1417
1418 #[test]
1419 fn test_existing_non_string_primary_key_rejects_incompatible_selected_value() {
1420 let existing = existing_schema(
1421 vec![
1422 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1423 column("host", ConcreteDataType::int64_datatype()),
1424 ],
1425 &[1],
1426 );
1427 let err = parse_with_select(
1428 request_with_log_attrs(vec![kv(
1429 "host",
1430 OtlpValue::StringValue("node-a".to_string()),
1431 )]),
1432 "host",
1433 Some(&existing),
1434 )
1435 .unwrap_err();
1436
1437 assert!(
1438 err.to_string()
1439 .contains("failed to align log column 'host'")
1440 );
1441 }
1442
1443 #[test]
1444 fn test_existing_timestamp_unit_is_respected() {
1445 let existing = existing_schema(
1446 vec![time_column(
1447 ConcreteDataType::timestamp_millisecond_datatype(),
1448 )],
1449 &[],
1450 );
1451 let rows = parse_with_select(request_with_log_attrs(vec![]), "", Some(&existing)).unwrap();
1452
1453 assert_eq!(
1454 rows.schema[0].datatype,
1455 ColumnDataType::TimestampMillisecond as i32
1456 );
1457 assert_eq!(
1458 rows.rows[0].values[0].value_data,
1459 Some(ValueData::TimestampMillisecondValue(1234))
1460 );
1461 }
1462
1463 #[test]
1464 fn test_missing_existing_primary_key_is_not_generated() {
1465 let existing = existing_schema(
1466 vec![
1467 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1468 column("host", ConcreteDataType::string_datatype()),
1469 ],
1470 &[1],
1471 );
1472 let rows = parse_with_select(request_with_log_attrs(vec![]), "", Some(&existing)).unwrap();
1473
1474 assert!(
1475 !rows
1476 .schema
1477 .iter()
1478 .any(|schema| schema.column_name == "host")
1479 );
1480 }
1481
1482 #[test]
1483 fn test_existing_table_keeps_new_generated_columns_as_fields() {
1484 let existing = existing_schema(
1485 vec![
1486 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1487 column("trace_id", ConcreteDataType::string_datatype()),
1488 ],
1489 &[1],
1490 );
1491 let rows = parse_with_select(
1492 request_with_log_attrs(vec![kv(
1493 "host",
1494 OtlpValue::StringValue("node-a".to_string()),
1495 )]),
1496 "host",
1497 Some(&existing),
1498 )
1499 .unwrap();
1500 let host_idx = column_index(&rows, "host");
1501 let scope_name_idx = column_index(&rows, "scope_name");
1502
1503 assert_eq!(
1504 rows.schema[host_idx].semantic_type,
1505 SemanticType::Field as i32
1506 );
1507 assert_eq!(
1508 rows.schema[scope_name_idx].semantic_type,
1509 SemanticType::Field as i32
1510 );
1511 }
1512
1513 #[test]
1514 fn test_existing_uint64_column_keeps_type_and_coerces_int64_request() {
1515 let existing = existing_schema(
1519 vec![
1520 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1521 column("counter", ConcreteDataType::uint64_datatype()),
1522 ],
1523 &[],
1524 );
1525
1526 let rows = parse_with_select(
1527 request_with_log_attrs(vec![kv("counter", OtlpValue::IntValue(42))]),
1528 "counter",
1529 Some(&existing),
1530 )
1531 .unwrap();
1532 let idx = column_index(&rows, "counter");
1533
1534 assert_eq!(rows.schema[idx].datatype, ColumnDataType::Uint64 as i32);
1535 assert_eq!(
1536 rows.rows[0].values[idx].value_data,
1537 Some(ValueData::U64Value(42))
1538 );
1539 }
1540
1541 #[test]
1542 fn test_jsonb_extended_log_values_rejected() {
1543 for (value, expected_type) in [
1544 (
1545 JsonbValue::Number(JsonbNumber::Decimal64(jsonb::Decimal64 {
1546 value: 123,
1547 scale: 2,
1548 })),
1549 "DECIMAL",
1550 ),
1551 (JsonbValue::Binary(&[1, 2, 3]), "Binary"),
1552 (JsonbValue::Date(jsonb::Date { value: 0 }), "Date"),
1553 (
1554 JsonbValue::Timestamp(jsonb::Timestamp { value: 0 }),
1555 "Timestamp",
1556 ),
1557 (
1558 JsonbValue::TimestampTz(jsonb::TimestampTz {
1559 value: 0,
1560 offset: 0,
1561 }),
1562 "TimestampTz",
1563 ),
1564 (
1565 JsonbValue::Interval(jsonb::Interval {
1566 months: 0,
1567 days: 0,
1568 micros: 0,
1569 }),
1570 "Interval",
1571 ),
1572 ] {
1573 let schema_error =
1574 decide_column_schema_and_convert_value("attr", value.clone(), None, "logs")
1575 .expect_err("extended JSONB type must be rejected");
1576 let value_error = jsonb_value_to_log_value_data("attr", value, true)
1577 .expect_err("extended JSONB type must be rejected");
1578 for error in [schema_error, value_error] {
1579 assert!(matches!(error,
1580 Error::UnsupportedJsonDataTypeForTag { key, ty, .. }
1581 if key == "attr" && ty == expected_type
1582 ));
1583 }
1584 }
1585 }
1586
1587 #[test]
1588 fn test_jsonb_uint64_exceeding_i64_range_rejected() {
1589 let err = jsonb_value_to_log_value_data(
1594 "counter",
1595 JsonbValue::Number(JsonbNumber::UInt64(u64::MAX)),
1596 false,
1597 )
1598 .unwrap_err();
1599
1600 assert!(
1601 err.to_string()
1602 .contains("exceeds the i64 range supported by built-in log columns")
1603 );
1604 }
1605
1606 #[test]
1607 fn test_existing_int64_column_rejects_numeric_string_value() {
1608 let existing = existing_schema(
1614 vec![
1615 time_column(ConcreteDataType::timestamp_nanosecond_datatype()),
1616 column("counter", ConcreteDataType::int64_datatype()),
1617 ],
1618 &[],
1619 );
1620
1621 let err = parse_with_select(
1622 request_with_log_attrs(vec![kv(
1623 "counter",
1624 OtlpValue::StringValue("42".to_string()),
1625 )]),
1626 "counter",
1627 Some(&existing),
1628 )
1629 .unwrap_err();
1630
1631 assert!(
1632 err.to_string()
1633 .contains("failed to align log column 'counter'")
1634 );
1635 }
1636}