use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use arrow::array::{ArrayRef, Int64Array};
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use arrow::record_batch::RecordBatch;
use datafusion::error::{DataFusionError, Result as DataFusionResult};
use datafusion::execution::{RecordBatchStream, SendableRecordBatchStream, TaskContext};
use datafusion::physical_expr::EquivalenceProperties;
use datafusion::physical_plan::{
DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties, PlanProperties,
};
use futures::Stream;
pub const ROWID_COLUMN_NAME: &str = "rowid";
pub const ROW_POS_COLUMN_NAME: &str = "__ducklake_row_pos";
pub const ROW_ID_PARQUET_FIELD_ID: i32 = 2_147_483_540;
pub const EMBEDDED_ROW_ID_COLUMN_NAME: &str = "_ducklake_internal_row_id";
pub fn embedded_rowid_field() -> Field {
let mut metadata = std::collections::HashMap::new();
metadata.insert(
"PARQUET:field_id".to_string(),
ROW_ID_PARQUET_FIELD_ID.to_string(),
);
Field::new(EMBEDDED_ROW_ID_COLUMN_NAME, DataType::Int64, true).with_metadata(metadata)
}
pub const SNAPSHOT_ID_PARQUET_FIELD_ID: i32 = 2_147_483_539;
pub const EMBEDDED_SNAPSHOT_ID_COLUMN_NAME: &str = "_ducklake_internal_snapshot_id";
pub fn embedded_snapshot_id_field() -> Field {
let mut metadata = std::collections::HashMap::new();
metadata.insert(
"PARQUET:field_id".to_string(),
SNAPSHOT_ID_PARQUET_FIELD_ID.to_string(),
);
Field::new(EMBEDDED_SNAPSHOT_ID_COLUMN_NAME, DataType::Int64, true).with_metadata(metadata)
}
pub fn rowid_field() -> Field {
Field::new(ROWID_COLUMN_NAME, DataType::Int64, true)
}
pub fn row_pos_field() -> Field {
Field::new(ROW_POS_COLUMN_NAME, DataType::Int64, false)
}
#[derive(Debug)]
pub struct FileRowNumberExec {
input: Arc<dyn ExecutionPlan>,
partition_starts: Arc<Vec<i64>>,
schema: SchemaRef,
properties: Arc<PlanProperties>,
}
impl FileRowNumberExec {
pub fn new(input: Arc<dyn ExecutionPlan>, partition_starts: Vec<i64>) -> Self {
let mut fields: Vec<Arc<Field>> = input.schema().fields().iter().cloned().collect();
fields.push(Arc::new(row_pos_field()));
let schema = Arc::new(Schema::new(fields));
let properties = Arc::new(PlanProperties::new(
EquivalenceProperties::new(schema.clone()),
input.output_partitioning().clone(),
input.pipeline_behavior(),
input.boundedness(),
));
Self {
input,
partition_starts: Arc::new(partition_starts),
schema,
properties,
}
}
}
impl DisplayAs for FileRowNumberExec {
fn fmt_as(&self, _t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"FileRowNumberExec: starts={:?}",
self.partition_starts.as_ref()
)
}
}
impl ExecutionPlan for FileRowNumberExec {
fn name(&self) -> &str {
"FileRowNumberExec"
}
fn properties(&self) -> &Arc<PlanProperties> {
&self.properties
}
fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
vec![&self.input]
}
fn maintains_input_order(&self) -> Vec<bool> {
vec![true]
}
fn benefits_from_input_partitioning(&self) -> Vec<bool> {
vec![false]
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn ExecutionPlan>>,
) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
if children.len() != 1 {
return Err(DataFusionError::Internal(
"FileRowNumberExec expects exactly one child".into(),
));
}
Ok(Arc::new(FileRowNumberExec::new(
children.into_iter().next().unwrap(),
self.partition_starts.as_ref().clone(),
)))
}
fn execute(
&self,
partition: usize,
context: Arc<TaskContext>,
) -> DataFusionResult<SendableRecordBatchStream> {
let start = *self.partition_starts.get(partition).ok_or_else(|| {
DataFusionError::Internal(format!(
"FileRowNumberExec: no starting position for partition {partition} \
(have {} partitions)",
self.partition_starts.len()
))
})?;
Ok(Box::pin(FileRowNumberStream {
input: self.input.execute(partition, context)?,
schema: self.schema.clone(),
cursor: start,
}))
}
}
struct FileRowNumberStream {
input: SendableRecordBatchStream,
schema: SchemaRef,
cursor: i64,
}
impl Stream for FileRowNumberStream {
type Item = DataFusionResult<RecordBatch>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match Pin::new(&mut self.input).poll_next(cx) {
Poll::Ready(Some(Ok(batch))) => {
let n = batch.num_rows();
let mut builder = Int64Array::builder(n);
for i in 0..n {
builder.append_value(self.cursor + i as i64);
}
self.cursor += n as i64;
let mut cols: Vec<ArrayRef> = batch.columns().to_vec();
cols.push(Arc::new(builder.finish()));
let out = RecordBatch::try_new(self.schema.clone(), cols)
.map_err(|e| DataFusionError::ArrowError(Box::new(e), None));
Poll::Ready(Some(out))
},
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
}
}
impl RecordBatchStream for FileRowNumberStream {
fn schema(&self) -> SchemaRef {
self.schema.clone()
}
}
#[derive(Debug)]
pub struct RowIdExec {
input: Arc<dyn ExecutionPlan>,
row_id_start: Option<i64>,
pos_index: usize,
schema: SchemaRef,
properties: Arc<PlanProperties>,
}
impl RowIdExec {
pub fn try_new(
input: Arc<dyn ExecutionPlan>,
row_id_start: Option<i64>,
) -> DataFusionResult<Self> {
let input_schema = input.schema();
let pos_index = input_schema.index_of(ROW_POS_COLUMN_NAME).map_err(|_| {
DataFusionError::Internal(format!(
"RowIdExec input is missing the `{ROW_POS_COLUMN_NAME}` column"
))
})?;
let mut fields: Vec<Arc<Field>> = input_schema.fields().iter().cloned().collect();
fields.push(Arc::new(rowid_field()));
let schema = Arc::new(Schema::new(fields));
let properties = Arc::new(PlanProperties::new(
EquivalenceProperties::new(schema.clone()),
input.output_partitioning().clone(),
input.pipeline_behavior(),
input.boundedness(),
));
Ok(Self {
input,
row_id_start,
pos_index,
schema,
properties,
})
}
}
impl DisplayAs for RowIdExec {
fn fmt_as(&self, _t: DisplayFormatType, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"RowIdExec: row_id_start={}",
self.row_id_start
.map_or_else(|| "NULL".to_string(), |v| v.to_string())
)
}
}
impl ExecutionPlan for RowIdExec {
fn name(&self) -> &str {
"RowIdExec"
}
fn properties(&self) -> &Arc<PlanProperties> {
&self.properties
}
fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
vec![&self.input]
}
fn maintains_input_order(&self) -> Vec<bool> {
vec![true]
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn ExecutionPlan>>,
) -> DataFusionResult<Arc<dyn ExecutionPlan>> {
if children.len() != 1 {
return Err(DataFusionError::Internal(
"RowIdExec expects exactly one child".into(),
));
}
Ok(Arc::new(RowIdExec::try_new(
children.into_iter().next().unwrap(),
self.row_id_start,
)?))
}
fn execute(
&self,
partition: usize,
context: Arc<TaskContext>,
) -> DataFusionResult<SendableRecordBatchStream> {
Ok(Box::pin(RowIdStream {
input: self.input.execute(partition, context)?,
schema: self.schema.clone(),
row_id_start: self.row_id_start,
pos_index: self.pos_index,
}))
}
}
struct RowIdStream {
input: SendableRecordBatchStream,
schema: SchemaRef,
row_id_start: Option<i64>,
pos_index: usize,
}
impl Stream for RowIdStream {
type Item = DataFusionResult<RecordBatch>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match Pin::new(&mut self.input).poll_next(cx) {
Poll::Ready(Some(Ok(batch))) => {
let n = batch.num_rows();
let rowid_col: ArrayRef = match self.row_id_start {
Some(start) => {
let pos = match batch
.column(self.pos_index)
.as_any()
.downcast_ref::<Int64Array>()
{
Some(p) => p,
None => {
return Poll::Ready(Some(Err(DataFusionError::Internal(format!(
"`{ROW_POS_COLUMN_NAME}` column is not Int64"
)))));
},
};
let mut builder = Int64Array::builder(n);
for i in 0..n {
builder.append_value(start + pos.value(i));
}
Arc::new(builder.finish())
},
None => {
let mut builder = Int64Array::builder(n);
for _ in 0..n {
builder.append_null();
}
Arc::new(builder.finish())
},
};
let mut cols: Vec<ArrayRef> = batch.columns().to_vec();
cols.push(rowid_col);
let out = RecordBatch::try_new(self.schema.clone(), cols)
.map_err(|e| DataFusionError::ArrowError(Box::new(e), None));
Poll::Ready(Some(out))
},
Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e))),
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
}
}
}
impl RecordBatchStream for RowIdStream {
fn schema(&self) -> SchemaRef {
self.schema.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::{Array, Int32Array};
use datafusion::datasource::memory::MemorySourceConfig;
use futures::StreamExt;
fn batch_with_pos(values: &[i32], positions: &[i64]) -> (SchemaRef, RecordBatch) {
let schema = Arc::new(Schema::new(vec![
Field::new("v", DataType::Int32, false),
row_pos_field(),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int32Array::from(values.to_vec())) as ArrayRef,
Arc::new(Int64Array::from(positions.to_vec())) as ArrayRef,
],
)
.unwrap();
(schema, batch)
}
#[tokio::test]
async fn file_row_number_seeds_per_partition() {
let schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Int32, false)]));
let mk = |vals: Vec<i32>| {
RecordBatch::try_new(
schema.clone(),
vec![Arc::new(Int32Array::from(vals)) as ArrayRef],
)
.unwrap()
};
let mem = MemorySourceConfig::try_new_exec(
&[vec![mk(vec![1, 2, 3])], vec![mk(vec![4, 5])]],
schema.clone(),
None,
)
.unwrap();
let exec = Arc::new(FileRowNumberExec::new(mem, vec![0, 100]));
assert_eq!(exec.schema().field(1).name(), ROW_POS_COLUMN_NAME);
let ctx = Arc::new(TaskContext::default());
let pos_of = |b: &RecordBatch| {
b.column(1)
.as_any()
.downcast_ref::<Int64Array>()
.unwrap()
.values()
.to_vec()
};
let mut s0 = exec.clone().execute(0, ctx.clone()).unwrap();
let p0 = s0.next().await.unwrap().unwrap();
assert_eq!(pos_of(&p0), vec![0, 1, 2]);
let mut s1 = exec.execute(1, ctx).unwrap();
let p1 = s1.next().await.unwrap().unwrap();
assert_eq!(
pos_of(&p1),
vec![100, 101],
"partition 1 seeds at its start"
);
}
#[tokio::test]
async fn file_row_number_continues_across_batches() {
let schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Int32, false)]));
let mk = |vals: Vec<i32>| {
RecordBatch::try_new(
schema.clone(),
vec![Arc::new(Int32Array::from(vals)) as ArrayRef],
)
.unwrap()
};
let mem = MemorySourceConfig::try_new_exec(
&[vec![mk(vec![1, 2, 3]), mk(vec![4, 5])]],
schema.clone(),
None,
)
.unwrap();
let exec = Arc::new(FileRowNumberExec::new(mem, vec![10]));
let ctx = Arc::new(TaskContext::default());
let mut s = exec.execute(0, ctx).unwrap();
let pos_of = |b: &RecordBatch| {
b.column(1)
.as_any()
.downcast_ref::<Int64Array>()
.unwrap()
.values()
.to_vec()
};
assert_eq!(pos_of(&s.next().await.unwrap().unwrap()), vec![10, 11, 12]);
assert_eq!(pos_of(&s.next().await.unwrap().unwrap()), vec![13, 14]);
}
#[tokio::test]
async fn file_row_number_zero_column_input() {
let schema = Arc::new(Schema::new(Vec::<Field>::new()));
let batch = RecordBatch::try_new_with_options(
schema.clone(),
vec![],
&arrow::record_batch::RecordBatchOptions::new().with_row_count(Some(4)),
)
.unwrap();
let mem = MemorySourceConfig::try_new_exec(&[vec![batch]], schema, None).unwrap();
let exec = Arc::new(FileRowNumberExec::new(mem, vec![7]));
let ctx = Arc::new(TaskContext::default());
let mut s = exec.execute(0, ctx).unwrap();
let out = s.next().await.unwrap().unwrap();
assert_eq!(out.num_columns(), 1);
assert_eq!(
out.column(0)
.as_any()
.downcast_ref::<Int64Array>()
.unwrap()
.values()
.to_vec(),
vec![7, 8, 9, 10]
);
}
#[tokio::test]
async fn rowid_is_start_plus_position() {
let (schema, batch) = batch_with_pos(&[10, 20, 30], &[5, 6, 9]);
let mem = MemorySourceConfig::try_new_exec(&[vec![batch]], schema, None).unwrap();
let exec = Arc::new(RowIdExec::try_new(mem, Some(1000)).unwrap());
assert_eq!(exec.schema().field(2).name(), ROWID_COLUMN_NAME);
let ctx = Arc::new(TaskContext::default());
let mut s = exec.execute(0, ctx).unwrap();
let out = s.next().await.unwrap().unwrap();
let rowids = out
.column(2)
.as_any()
.downcast_ref::<Int64Array>()
.unwrap()
.values()
.to_vec();
assert_eq!(rowids, vec![1005, 1006, 1009]);
assert_eq!(
out.column(1)
.as_any()
.downcast_ref::<Int64Array>()
.unwrap()
.values()
.to_vec(),
vec![5, 6, 9]
);
}
#[tokio::test]
async fn rowid_null_when_start_is_none() {
let (schema, batch) = batch_with_pos(&[1, 2], &[0, 1]);
let mem = MemorySourceConfig::try_new_exec(&[vec![batch]], schema, None).unwrap();
let exec = Arc::new(RowIdExec::try_new(mem, None).unwrap());
let ctx = Arc::new(TaskContext::default());
let mut s = exec.execute(0, ctx).unwrap();
let out = s.next().await.unwrap().unwrap();
let rowid = out.column(2).as_any().downcast_ref::<Int64Array>().unwrap();
assert!(rowid.is_null(0) && rowid.is_null(1));
}
#[test]
fn rowid_errors_without_position_column() {
let schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Int32, false)]));
let mem = MemorySourceConfig::try_new_exec(&[vec![]], schema, None).unwrap();
assert!(
RowIdExec::try_new(mem, Some(0)).is_err(),
"RowIdExec must require the position column"
);
}
}