Skip to content

Commit 8838f6d

Browse files
catch within-batch type mismatches on new fields
remove time partition headers from create/update logstream
1 parent 04f2fb8 commit 8838f6d

3 files changed

Lines changed: 25 additions & 42 deletions

File tree

src/event/format/mod.rs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -584,21 +584,22 @@ pub fn rename_per_record_type_mismatches(
584584
existing_schema: &HashMap<String, Arc<Field>>,
585585
schema_version: SchemaVersion,
586586
) -> Vec<Value> {
587-
if values.len() <= 1 || existing_schema.is_empty() {
588-
return values;
589-
}
590-
// Bail out unless at least one inferred field collides with storage at
591-
// the same type. Without that, arrow's inference can't have hidden a
592-
// mixed-type batch behind a matching aggregate type.
593-
let needs_check = inferred_schema.fields().iter().any(|f| {
594-
existing_schema
595-
.get(f.name())
596-
.is_some_and(|s| s.data_type() == f.data_type())
597-
});
598-
if !needs_check {
587+
if values.len() <= 1 {
599588
return values;
600589
}
601590

591+
// Lookup of inferred field types — used as the reference type when a field
592+
// isn't yet in storage (e.g. first batch for the stream, or a new column).
593+
// Without this, mixed-type batches for new fields slip through: arrow picks
594+
// a single aggregate type (Utf8 wins over Bool, etc.), the batch-level
595+
// conflict check sees nothing to compare against in empty storage, and
596+
// records carrying the loser type later fail `fields_mismatch`.
597+
let inferred_types: HashMap<&str, &DataType> = inferred_schema
598+
.fields()
599+
.iter()
600+
.map(|f| (f.name().as_str(), f.data_type()))
601+
.collect();
602+
602603
values
603604
.into_iter()
604605
.map(|value| {
@@ -611,11 +612,16 @@ pub fn rename_per_record_type_mismatches(
611612
if val.is_null() {
612613
return (key, val);
613614
}
614-
let Some(existing_field) = existing_schema.get(&key) else {
615+
// Prefer storage's declared type; fall back to the inferred
616+
// type so within-batch mismatches on new fields are caught.
617+
let target_type = existing_schema
618+
.get(&key)
619+
.map(|f| f.data_type())
620+
.or_else(|| inferred_types.get(key.as_str()).copied());
621+
let Some(target_type) = target_type else {
615622
return (key, val);
616623
};
617-
if value_compatible_with_type(&val, existing_field.data_type(), schema_version)
618-
{
624+
if value_compatible_with_type(&val, target_type, schema_version) {
619625
return (key, val);
620626
}
621627
let suffix = get_datatype_suffix(&datatype_for_value(&val));

src/handlers/http/modal/utils/logstream_utils.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,14 @@ use crate::{
2323
handlers::{
2424
CUSTOM_PARTITION_KEY, DATASET_LABELS_KEY, DATASET_TAG_KEY, DATASET_TAGS_KEY, DatasetTag,
2525
INFER_TIMESTAMP_KEY, LOG_SOURCE_KEY, STATIC_SCHEMA_FLAG, STREAM_TYPE_KEY,
26-
TELEMETRY_TYPE_KEY, TIME_PARTITION_KEY, TIME_PARTITION_LIMIT_KEY, TelemetryType,
27-
UPDATE_STREAM_KEY, parse_dataset_labels, parse_dataset_tags,
26+
TELEMETRY_TYPE_KEY, TIME_PARTITION_LIMIT_KEY, TelemetryType, UPDATE_STREAM_KEY,
27+
parse_dataset_labels, parse_dataset_tags,
2828
},
2929
storage::StreamType,
3030
};
3131

3232
#[derive(Debug)]
3333
pub struct PutStreamHeaders {
34-
pub time_partition: String,
3534
pub time_partition_limit: String,
3635
pub custom_partition: Option<String>,
3736
pub static_schema_flag: bool,
@@ -49,7 +48,6 @@ pub struct PutStreamHeaders {
4948
impl Default for PutStreamHeaders {
5049
fn default() -> Self {
5150
Self {
52-
time_partition: String::default(),
5351
time_partition_limit: String::default(),
5452
custom_partition: None,
5553
static_schema_flag: false,
@@ -80,10 +78,6 @@ impl From<&HeaderMap> for PutStreamHeaders {
8078
.get(TELEMETRY_TYPE_KEY)
8179
.and_then(|v| v.to_str().ok());
8280
PutStreamHeaders {
83-
time_partition: headers
84-
.get(TIME_PARTITION_KEY)
85-
.map_or("", |v| v.to_str().unwrap())
86-
.to_string(),
8781
time_partition_limit: headers
8882
.get(TIME_PARTITION_LIMIT_KEY)
8983
.map_or("", |v| v.to_str().unwrap())

src/parseable/mod.rs

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,6 @@ impl Parseable {
695695
tenant_id: &Option<String>,
696696
) -> Result<HeaderMap, StreamError> {
697697
let PutStreamHeaders {
698-
time_partition,
699698
time_partition_limit,
700699
custom_partition,
701700
static_schema_flag,
@@ -763,7 +762,6 @@ impl Parseable {
763762
.update_stream(
764763
headers,
765764
stream_name,
766-
&time_partition,
767765
static_schema_flag,
768766
&time_partition_limit,
769767
custom_partition.as_ref(),
@@ -782,25 +780,18 @@ impl Parseable {
782780
validate_custom_partition(custom_partition)?;
783781
}
784782

785-
if !time_partition.is_empty() && custom_partition.is_some() {
786-
return Err(StreamError::Custom {
787-
msg: "Cannot set both time partition and custom partition".to_string(),
788-
status: StatusCode::BAD_REQUEST,
789-
});
790-
}
791-
792783
let schema = validate_static_schema(
793784
body,
794785
stream_name,
795-
&time_partition,
786+
"",
796787
custom_partition.as_ref(),
797788
static_schema_flag,
798789
)?;
799790

800791
let log_source_entry = LogSourceEntry::new(log_source, HashSet::new());
801792
self.create_stream(
802793
stream_name.to_string(),
803-
&time_partition,
794+
"",
804795
time_partition_in_days,
805796
custom_partition.as_ref(),
806797
static_schema_flag,
@@ -818,12 +809,10 @@ impl Parseable {
818809
Ok(headers.clone())
819810
}
820811

821-
#[allow(clippy::too_many_arguments)]
822812
async fn update_stream(
823813
&self,
824814
headers: &HeaderMap,
825815
stream_name: &str,
826-
time_partition: &str,
827816
static_schema_flag: bool,
828817
time_partition_limit: &str,
829818
custom_partition: Option<&String>,
@@ -832,12 +821,6 @@ impl Parseable {
832821
if !self.streams.contains(stream_name, tenant_id) {
833822
return Err(StreamNotFound(stream_name.to_string()).into());
834823
}
835-
if !time_partition.is_empty() {
836-
return Err(StreamError::Custom {
837-
msg: "Altering the time partition of an existing stream is restricted.".to_string(),
838-
status: StatusCode::BAD_REQUEST,
839-
});
840-
}
841824
if static_schema_flag {
842825
return Err(StreamError::Custom {
843826
msg: "Altering the schema of an existing stream is restricted.".to_string(),

0 commit comments

Comments
 (0)