Skip to content

Commit cb49df4

Browse files
authored
Simplify data model (#16)
* Simplify data model * Simplify internal data model
1 parent 5949738 commit cb49df4

16 files changed

Lines changed: 187 additions & 235 deletions

File tree

crates/strata/src/datagen.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use schema::{DataType, Field, Schema};
1212
use serde_json::{Map, Value, json};
1313

1414
use crate::dataset::Dataset;
15-
use crate::record::{Records, stringify_text_columns};
15+
use crate::record::{Batch, stringify_text_columns};
1616

1717
pub struct Generator {
1818
schema: Schema,
@@ -42,10 +42,10 @@ impl Generator {
4242
}
4343

4444
/// The rows in `range`, Arrow-encoded — one page.
45-
pub fn rows(&self, range: Range<usize>) -> Result<Records> {
45+
pub fn rows(&self, range: Range<usize>) -> Result<Batch> {
4646
let mut rows: Vec<Value> = range.map(|i| self.row(i)).collect();
4747
stringify_text_columns(&self.schema, &mut rows);
48-
Records::encode(self.schema.clone(), &rows)
48+
Batch::encode(&self.schema, &rows)
4949
}
5050

5151
/// The first `n` rows as a [`Dataset`] (schema + Arrow records).

crates/strata/src/dataset.rs

Lines changed: 11 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
//! over Arrow Flight the same `(schema, rows)` rides natively in a `DoPut`.
88
99
use anyhow::{Result, bail};
10-
use futures::stream::{BoxStream, StreamExt};
10+
use futures::stream::StreamExt;
1111
use schema::Schema;
1212
use serde::Serialize;
1313
use serde_json::Value;
1414

15-
use crate::page::Cursor;
16-
use crate::record::Records;
15+
use crate::{
16+
DataStream,
17+
record::{Batch, BatchPage},
18+
};
1719

1820
/// How a sink should apply a written dataset. Rides as metadata on the existing
1921
/// `put` verb (the reserved `disposition` query param) rather than a new verb, so
@@ -66,11 +68,11 @@ impl Disposition {
6668
#[derive(Debug, Clone)]
6769
pub struct Dataset {
6870
pub schema: Schema,
69-
pub records: Records,
71+
pub records: Batch,
7072
}
7173

7274
impl Dataset {
73-
pub fn new(schema: Schema, records: Records) -> Self {
75+
pub fn new(schema: Schema, records: Batch) -> Self {
7476
Dataset { schema, records }
7577
}
7678

@@ -79,21 +81,21 @@ impl Dataset {
7981
/// against it. The typed-input counterpart of a `put`.
8082
pub fn of<T: serde::Serialize + schema::HasSchema>(rows: &[T]) -> Result<Self> {
8183
let schema = T::schema();
82-
let records = Records::encode(schema.clone(), rows)?;
84+
let records = Batch::encode(&schema, rows)?;
8385
Ok(Dataset::new(schema, records))
8486
}
8587

8688
/// Interim bridge: decode the Arrow rows to JSON for sinks that aren't yet
8789
/// Arrow-native. This is the one labeled Arrow→JSON on the write path; Phase B
8890
/// removes it as each sink binds Arrow columns directly.
8991
pub fn to_json_rows(&self) -> Result<Vec<Value>> {
90-
self.records.to_json_rows()
92+
self.records.to_json_rows(&self.schema)
9193
}
9294

9395
/// Bridge to a single-chunk [`DataStream`], until readers produce batches lazily.
9496
pub fn into_stream(self) -> DataStream {
95-
let chunk = Chunk {
96-
records: self.records,
97+
let chunk = BatchPage {
98+
data: self.records,
9799
cursor: None,
98100
};
99101
DataStream {
@@ -102,57 +104,3 @@ impl Dataset {
102104
}
103105
}
104106
}
105-
106-
/// One batch of a data-plane stream, with an optional resume [`Cursor`] valid
107-
/// after it (sparse — `Some` only at checkpoint boundaries). Over Flight it maps
108-
/// to a `FlightData`'s `app_metadata`.
109-
pub struct Chunk {
110-
pub records: Records,
111-
pub cursor: Option<Cursor>,
112-
}
113-
114-
impl Chunk {
115-
/// Decode this batch's rows into typed values.
116-
pub fn decode<T: serde::de::DeserializeOwned>(&self) -> Result<Vec<T>> {
117-
self.records.decode()
118-
}
119-
}
120-
121-
/// The streaming form of a [`Dataset`]: the native `schema` plus a lazy stream of
122-
/// [`Chunk`]s following it, consumed by a sink without materializing the table.
123-
pub struct DataStream {
124-
pub schema: Schema,
125-
pub chunks: BoxStream<'static, Result<Chunk>>,
126-
}
127-
128-
impl DataStream {
129-
/// Pull just the first [`Chunk`] (one page) — the `list_once` case for callers
130-
/// that want a bounded read instead of draining. The schema rides alongside.
131-
pub async fn first(mut self) -> Result<Option<Chunk>> {
132-
self.chunks.next().await.transpose()
133-
}
134-
135-
/// Drain every chunk into one materialized [`Dataset`] — the bridge for sinks
136-
/// that don't consume the stream incrementally.
137-
pub async fn collect(self) -> Result<Dataset> {
138-
let DataStream { schema, mut chunks } = self;
139-
let mut batches = Vec::new();
140-
let mut arrow_schema = None;
141-
while let Some(chunk) = chunks.next().await {
142-
let records = chunk?.records;
143-
arrow_schema.get_or_insert_with(|| records.schema.clone());
144-
batches.extend(records.batches);
145-
}
146-
let arrow_schema = match arrow_schema {
147-
Some(s) => s,
148-
None => std::sync::Arc::new(crate::record::strata_schema_to_arrow_schema(&schema)),
149-
};
150-
Ok(Dataset::new(
151-
schema,
152-
Records {
153-
schema: arrow_schema,
154-
batches,
155-
},
156-
))
157-
}
158-
}

crates/strata/src/flight/mod.rs

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ use tonic::transport::Server;
2828
use tonic::{Request, Response, Status, Streaming};
2929

3030
use crate::Registry;
31-
use crate::dataset::{Chunk, DataStream};
32-
use crate::record::{Records, arrow_schema_to_strata, strata_schema_to_arrow_schema};
31+
use crate::record::{
32+
Batch, BatchPage, DataStream, arrow_schema_to_strata, strata_schema_to_arrow_schema,
33+
};
34+
use schema::Schema as StrataSchema;
3335
use crate::router::{Body, Method};
3436

3537
/// Start the Flight server on `addr`, serving every registered provider.
@@ -115,20 +117,20 @@ fn ticket_target(ticket: &Ticket) -> Result<(String, String), Status> {
115117

116118
type FlightStream<T> = BoxStream<'static, Result<T, Status>>;
117119

118-
/// Encode one [`Chunk`] (a page): its batches as Arrow `FlightData`, then a
119-
/// metadata-only message whose `app_metadata` is the chunk's cursor (when present)
120-
/// — the per-batch checkpoint, forwarded opaquely.
121-
fn encode_chunk(chunk: Chunk) -> FlightStream<FlightData> {
122-
let Chunk { records, cursor } = chunk;
123-
let schema = records.schema.clone();
124-
let batches = futures::stream::iter(
125-
records
126-
.batches
127-
.into_iter()
128-
.map(Ok::<RecordBatch, FlightError>),
129-
);
120+
/// Encode one [`BatchPage`]: its columns as Arrow `FlightData`
121+
fn encode_chunk(chunk: BatchPage, schema: &StrataSchema) -> FlightStream<FlightData> {
122+
let BatchPage { data, cursor } = chunk;
123+
let batch = match data.to_record_batch(schema) {
124+
Ok(batch) => batch,
125+
Err(e) => {
126+
return futures::stream::once(async move { Err(Status::internal(e.to_string())) })
127+
.boxed();
128+
}
129+
};
130+
let arrow_schema = batch.schema();
131+
let batches = futures::stream::iter([Ok::<RecordBatch, FlightError>(batch)]);
130132
let data = FlightDataEncoderBuilder::new()
131-
.with_schema(schema)
133+
.with_schema(arrow_schema)
132134
.build(batches)
133135
.map(|d| d.map_err(|e| Status::internal(e.to_string())));
134136
let trailer = cursor.map(|cursor| {
@@ -225,10 +227,11 @@ impl FlightService for StrataFlight {
225227
.read(&path)
226228
.await
227229
.map_err(|e| Status::internal(e.to_string()))?;
230+
let schema = stream.schema.clone();
228231
let out = stream
229232
.chunks
230-
.flat_map(|chunk| match chunk {
231-
Ok(chunk) => encode_chunk(chunk),
233+
.flat_map(move |chunk| match chunk {
234+
Ok(chunk) => encode_chunk(chunk, &schema),
232235
Err(e) => {
233236
futures::stream::once(async move { Err(Status::internal(e.to_string())) })
234237
.boxed()
@@ -291,24 +294,19 @@ impl FlightService for StrataFlight {
291294
.clone();
292295
let schema = arrow_schema_to_strata(&arrow_schema);
293296

294-
// One chunk per batch; the tail streams lazily. No cursor on the write path.
295-
let rest_schema = arrow_schema.clone();
296-
let head_chunk = futures::stream::iter(first_batch.map(move |batch| {
297-
Ok::<_, anyhow::Error>(Chunk {
298-
records: Records {
299-
schema: arrow_schema,
300-
batches: vec![batch],
301-
},
297+
// One page per batch; the tail streams lazily. No cursor on the write path.
298+
// The first batch was pulled only to make the schema available, so it is put
299+
// back on the front here.
300+
let head_chunk = futures::stream::iter(first_batch.map(|batch| {
301+
Ok::<_, anyhow::Error>(BatchPage {
302+
data: Batch::from_record_batch(&batch),
302303
cursor: None,
303304
})
304305
}));
305-
let rest_chunks = decoder.map(move |batch| {
306+
let rest_chunks = decoder.map(|batch| {
306307
let batch = batch.map_err(|e| anyhow::anyhow!(e.to_string()))?;
307-
Ok(Chunk {
308-
records: Records {
309-
schema: rest_schema.clone(),
310-
batches: vec![batch],
311-
},
308+
Ok(BatchPage {
309+
data: Batch::from_record_batch(&batch),
312310
cursor: None,
313311
})
314312
});

crates/strata/src/graphql/mod.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,13 @@ async fn tables(registry: &Arc<Registry>, mount: &str) -> Vec<String> {
110110
let Ok(stream) = provider.read("/tables").await else {
111111
return Vec::new();
112112
};
113+
let schema = stream.schema.clone();
113114
let Ok(Some(chunk)) = stream.first().await else {
114115
return Vec::new();
115116
};
116117
chunk
117-
.records
118-
.to_json_rows()
118+
.data
119+
.to_json_rows(&schema)
119120
.unwrap_or_default()
120121
.iter()
121122
.filter_map(|row| row.get("name")?.as_str().map(String::from))
@@ -197,8 +198,10 @@ fn nested_field(
197198
serde_json::json!({ "cmp": { "field": target_col, "op": "eq", "value": fk } });
198199
let encoded = urlencoding::encode(&filter.to_string()).into_owned();
199200
let path = format!("/tables/{target_table}?filter={encoded}&limit=1");
200-
let rows = match registry.get(&mount)?.read(&path).await?.first().await? {
201-
Some(chunk) => chunk.records.to_json_rows()?,
201+
let stream = registry.get(&mount)?.read(&path).await?;
202+
let schema = stream.schema.clone();
203+
let rows = match stream.first().await? {
204+
Some(chunk) => chunk.data.to_json_rows(&schema)?,
202205
None => Vec::new(),
203206
};
204207
Ok(rows.into_iter().next().map(FieldValue::owned_any))
@@ -249,8 +252,10 @@ fn table_field(
249252

250253
let path = read_path(&table, filter.as_ref(), &fields, limit);
251254
let provider = registry.get(&mount)?;
252-
let rows = match provider.read(&path).await?.first().await? {
253-
Some(chunk) => chunk.records.to_json_rows()?,
255+
let stream = provider.read(&path).await?;
256+
let schema = stream.schema.clone();
257+
let rows = match stream.first().await? {
258+
Some(chunk) => chunk.data.to_json_rows(&schema)?,
254259
None => Vec::new(),
255260
};
256261
Ok(Some(FieldValue::list(

crates/strata/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ use anyhow::Result;
2424

2525
pub use catalog::Catalog;
2626
pub use config::{Config, ProviderConfig};
27-
pub use dataset::{Chunk, DataStream, Dataset, Disposition};
27+
pub use dataset::{Dataset, Disposition};
2828
pub use page::{Cursor, Page};
2929
pub use provider::{Provider, ProviderObject, Registry};
30+
pub use record::DataStream;
3031
pub use router::{Body, EndpointInfo, Method, Params, Response, Router};
3132

3233
/// Config file consulted by [`registry`] when no explicit path is given.

crates/strata/src/main.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,17 @@ async fn run() -> Result<()> {
136136
let display = match method {
137137
// Data plane: read the stream and show the first chunk (one page)
138138
// as `{ items, cursor }`. Drain the whole stream with `--follow`.
139-
Verb::List => match provider.read(&path).await?.first().await? {
140-
Some(chunk) => serde_json::json!({
141-
"items": chunk.records.to_json_rows()?,
142-
"cursor": chunk.cursor,
143-
}),
144-
None => serde_json::json!({ "items": [], "cursor": null }),
145-
},
139+
Verb::List => {
140+
let stream = provider.read(&path).await?;
141+
let schema = stream.schema.clone();
142+
match stream.first().await? {
143+
Some(chunk) => serde_json::json!({
144+
"items": chunk.data.to_json_rows(&schema)?,
145+
"cursor": chunk.cursor,
146+
}),
147+
None => serde_json::json!({ "items": [], "cursor": null }),
148+
}
149+
}
146150
// Entity plane: `get` takes no body; `create` reads its JSON entity
147151
// from stdin (the `meta` channel).
148152
Verb::Get | Verb::Create => {
@@ -290,12 +294,13 @@ fn strip_query(path: &str) -> &str {
290294
/// this just consumes the stream to the tail.
291295
async fn follow_list(provider: &dyn strata::ProviderObject, path: &str) -> Result<()> {
292296
let mut stream = provider.read(path).await?;
297+
let schema = stream.schema.clone();
293298
let mut page = 1;
294299
while let Some(chunk) = stream.chunks.next().await {
295300
let chunk = chunk?;
296301
let output = serde_json::json!({
297302
"page": page,
298-
"items": chunk.records.to_json_rows()?,
303+
"items": chunk.data.to_json_rows(&schema)?,
299304
"cursor": chunk.cursor,
300305
});
301306
println!("{}", serde_json::to_string_pretty(&output)?);

crates/strata/src/pipe/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use futures::StreamExt;
99
use serde_json::Value;
1010
use strata_types::Pipe;
1111

12-
use crate::dataset::DataStream;
12+
use crate::record::DataStream;
1313
use crate::request::{ReadRequest, WriteRequest};
1414
use crate::router::Body;
1515
use crate::{Method, Registry};
@@ -48,7 +48,7 @@ pub async fn run_pass<S: PipeStore>(registry: &Registry, store: &S, pipe: &mut P
4848
let progress = sync::advance(strategy, &chunk);
4949

5050
// Write the rows (skip an empty tail page — nothing to store).
51-
let rows = chunk.records.row_count() as u64;
51+
let rows = chunk.data.row_count() as u64;
5252
if rows > 0 {
5353
let page = DataStream {
5454
schema: schema.clone(),
@@ -157,7 +157,7 @@ mod tests {
157157
let mut chunks = stream.chunks;
158158
let mut total = 0usize;
159159
while let Some(chunk) = chunks.next().await {
160-
total += chunk?.records.row_count();
160+
total += chunk?.data.row_count();
161161
}
162162
anyhow::Ok(total)
163163
};

crates/strata/src/pipe/sync.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
//! chunk that brought no rows means we've reached the live edge for now, so the
1818
//! driver stops pulling and drops the (otherwise infinite) stream.
1919
20-
use crate::dataset::Chunk;
2120
use crate::page::ListStrategy;
21+
use crate::record::BatchPage;
2222

23-
/// What the sync driver should do after writing one [`Chunk`].
23+
/// What the sync driver should do after writing one [`BatchPage`].
2424
#[derive(Debug, Clone, PartialEq, Eq)]
2525
pub enum Progress {
2626
/// More to pull right now: keep consuming the live stream. The token is this
@@ -36,7 +36,7 @@ pub enum Progress {
3636
///
3737
/// `Offset` follows the cursor until it goes `None`; `NextLink`'s cursor never goes
3838
/// `None`, so it stops when a page comes back empty. See the module docs.
39-
pub fn advance(strategy: ListStrategy, chunk: &Chunk) -> Progress {
39+
pub fn advance(strategy: ListStrategy, chunk: &BatchPage) -> Progress {
4040
let next = chunk.cursor.as_ref().and_then(|c| c.next.clone());
4141
match strategy {
4242
// Finite backfill: drain while the cursor advances; `None` is the tail.
@@ -49,7 +49,7 @@ pub fn advance(strategy: ListStrategy, chunk: &Chunk) -> Progress {
4949
// `None` — marks the live edge. When caught up, resume from the same
5050
// forward position so the next run picks up whatever has since arrived.
5151
ListStrategy::NextLink => {
52-
if chunk.records.row_count() == 0 {
52+
if chunk.data.row_count() == 0 {
5353
Progress::CaughtUp(next)
5454
} else {
5555
match next {

crates/strata/src/provider.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use serde_json::{Value, json};
1313

1414
use crate::catalog::Catalog;
1515
use crate::config::ProviderConfig;
16-
use crate::dataset::DataStream;
16+
use crate::record::DataStream;
1717
use crate::router::{Body, BoxFuture, EndpointInfo, Method, Response, Router, SchemaSource};
1818

1919
/// Implemented by each concrete provider. Knows its state type and how to wire

0 commit comments

Comments
 (0)