Skip to content

Commit f024bcf

Browse files
committed
feat(ingest): --record-shape flag for new-datasource
Buckets datasources can flatten each source record into table rows with a named record shape (the worker's connectors/shapes registry: mqtt_observations, otel_traces). Until now only the CLI's wizard/API could set it, so a shaped continuous datasource — e.g. OTLP traces via otel_traces — needed a raw API call. Mirror the existing --continuous flag: filesystem-only, threaded straight into IngestRequest.record_shape, omitted from the body when unset (skip_serializing_if). The worker validates the name and 422s an unknown one.
1 parent 39b78ae commit f024bcf

2 files changed

Lines changed: 41 additions & 0 deletions

File tree

‎src/client/ingest.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,11 @@ pub struct IngestRequest {
432432
/// re-runs it incrementally (append only new objects). Ignored otherwise.
433433
#[serde(skip_serializing_if = "std::ops::Not::not")]
434434
pub continuous: bool,
435+
/// filesystem only: flatten each source record into table rows with a named
436+
/// shape (e.g. `otel_traces`, `mqtt_observations`). Omitted → a record is a
437+
/// row. The worker validates the name and rejects an unknown one.
438+
#[serde(skip_serializing_if = "Option::is_none")]
439+
pub record_shape: Option<String>,
435440
#[serde(skip_serializing_if = "Option::is_none")]
436441
pub database_id: Option<String>,
437442
}

‎src/commands/ingest.rs‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,11 @@ pub struct CreateArgs {
698698
#[arg(long)]
699699
continuous: bool,
700700

701+
/// Flatten each source record into rows with a named shape, e.g.
702+
/// otel_traces or mqtt_observations (buckets only)
703+
#[arg(long = "record-shape")]
704+
record_shape: Option<String>,
705+
701706
/// Catalog type, e.g. rest (iceberg)
702707
#[arg(long = "catalog-type")]
703708
catalog_type: Option<String>,
@@ -719,6 +724,7 @@ impl CreateArgs {
719724
|| self.format.is_some()
720725
|| self.glob.is_some()
721726
|| self.continuous
727+
|| self.record_shape.is_some()
722728
|| self.catalog_type.is_some()
723729
|| self.database_id.is_some()
724730
}
@@ -786,6 +792,7 @@ fn build_create_request(
786792
file_glob: args.glob,
787793
file_format: args.format,
788794
continuous: args.continuous,
795+
record_shape: args.record_shape,
789796
..Default::default()
790797
},
791798
"iceberg" => IngestRequest {
@@ -1845,6 +1852,7 @@ mod tests {
18451852
format: None,
18461853
glob: None,
18471854
continuous: false,
1855+
record_shape: None,
18481856
catalog_type: None,
18491857
database_id: None,
18501858
}
@@ -1902,6 +1910,34 @@ mod tests {
19021910
);
19031911
}
19041912

1913+
#[test]
1914+
fn create_request_filesystem_carries_record_shape() {
1915+
let e = entry("buckets", "filesystem");
1916+
let mut args = create_args();
1917+
args.bucket_url = Some("s3://b/otel".into());
1918+
args.format = Some("jsonl".into());
1919+
args.record_shape = Some("otel_traces".into());
1920+
let req = build_create_request(&e, args, None).unwrap();
1921+
assert_eq!(req.record_shape.as_deref(), Some("otel_traces"));
1922+
assert!(
1923+
serde_json::to_string(&req)
1924+
.unwrap()
1925+
.contains("\"record_shape\":\"otel_traces\"")
1926+
);
1927+
1928+
// Omitted by default, and skipped from the body entirely (skip_serializing_if).
1929+
let mut off = create_args();
1930+
off.bucket_url = Some("s3://b".into());
1931+
off.format = Some("jsonl".into());
1932+
let req_off = build_create_request(&e, off, None).unwrap();
1933+
assert!(req_off.record_shape.is_none());
1934+
assert!(
1935+
!serde_json::to_string(&req_off)
1936+
.unwrap()
1937+
.contains("record_shape")
1938+
);
1939+
}
1940+
19051941
#[test]
19061942
fn create_request_rejects_invalid_names() {
19071943
let e = entry("postgres", "sql");

0 commit comments

Comments
 (0)