GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [connectors] Parse Arrow batches faster in the Delta Lake and Iceberg inputs. by bestouff · Pull Request #7910 · feldera/feldera

Conversation

@bestouff

@bestouff bestouff commented Nov 10, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

The Delta Lake and Iceberg input connectors spent most of their CPU time in the conversion of Arrow record batches to SQL records. This PR makes that conversion faster and moves it off the Tokio worker threads.

Result

delta_parse benchmark in static-connectors (new in this PR): one batch of 8192 wide rows, about 100 fields for each row, deserialization only.

State Time for each batch
main about 205 ms
This PR about 58 ms

delta_pipeline benchmark in static-connectors (new in this PR): the Delta Lake input connector in snapshot mode, through the controller, into an input table of a circuit with 8 workers that counts the records. The table is local, with 300 files and 2 457 600 of the same wide rows (4.3 GB). Median of 3 interleaved runs on a machine with 18 cores.

num_parsers main This PR Speed-up
Default (4 on main, 16 in this PR) 16.9 s, 145 000 records/s 3.06 s, 804 000 records/s 5.5x
1 62.9 s, 39 000 records/s 17.05 s, 144 000 records/s 3.7x
4 16.9 s, 146 000 records/s 4.67 s, 526 000 records/s 3.6x
16 6.24 s, 394 000 records/s 3.11 s, 791 000 records/s 2.0x

The circuit has no views and the table is on a local disk, so this shows the connector, not a full SQL pipeline on object storage.

Changes

Change Where
Read struct fields by position. A layout pass puts the columns in the order of the SQL table and marks the batch with feldera.positional_fields. The mark is a random value of the process, so a batch from a file or a client that carries the key is read by name. A batch with an absent column is read by name as before. crates/adapterlib/src/utils/arrow_layout.rs, crates/feldera-types/src/serde_with_context/deserialize.rs, crates/static-connectors/src/deinput.rs
Erase the deserializer one time only. The context deserializer erased a deserializer that was already erased, which made one heap allocation for each visitor call. crates/feldera-types/src/serde_with_context/deserialize.rs (as_erased)
Read a Decimal128 column as 128-bit significands when its scale is the scale of the SQL column. Other decimals go through a string as before. crates/adapterlib/src/utils/arrow_layout.rs, crates/fxp/src/serde_impl.rs, crates/fxp/src/dbsp_impl.rs
Normalize timestamp columns one time for each batch, and read them as integers. The new TimestampFormat::MicrosSinceEpochOrString also parses a string column with the format that these connectors used before, so a string column continues to load into a TIMESTAMP column. crates/adapterlib/src/utils/arrow_timestamps.rs, crates/sqllib/src/timestamp.rs
Run the parsers on a dedicated CPU thread pool. All job queues together keep at most two jobs per CPU core in flight, so many connectors do not each hold num_parsers batches in memory. crates/adapterlib/src/utils/cpu_pool.rs, crates/adapterlib/src/utils/job_queue.rs
Make num_parsers optional. When it is absent, the pipeline selects the value from the number of CPUs of its host, not the pipeline manager. The Iceberg file read concurrency keeps its default of 4. crates/feldera-types/src/transport/delta_table.rs, crates/feldera-types/src/transport/iceberg.rs

as_erased contains one unsafe block with a SAFETY comment. It casts a deserializer to &mut dyn erased_serde::Deserializer after a typeid::of comparison shows that it has that type.

Linked Issue

None.

Describe Manual Test Plan

  • Unit tests: feldera-types, feldera-fxp (with dbsp), feldera-adapterlib, feldera-iceberg, static-connectors (the two Kafka metadata tests skipped, they need a broker).
  • connector_suites: the Delta Lake and format:: suites, and the Iceberg file-system suites (--features iceberg-tests-fs).
  • New tests in crates/static-connectors/src/test/delta_tests.rs send a batch through the record deserializer: columns in a different order with nanosecond timestamps, an absent nullable column, and a string timestamp column.
  • For the new tests as_erased, deserialize_significand, decimal_significands, in_flight_limit_is_shared and delta_string_column_is_read_as_timestamp, I reverted the related change and saw each test fail.
  • Benchmark: cargo bench -p static-connectors --bench delta_parse, with interleaved A/B rounds against the previous state.
  • Benchmark: cargo bench -p static-connectors --bench delta_pipeline -- <table directory> 300 <num_parsers>, on this branch and on main with the same table.
  • Not done: a run of a SQL-compiled pipeline on a real Delta Lake table in object storage.

Checklist

  • Unit tests added/updated
  • Integration tests added/updated
  • Documentation updated
  • Changelog updated

Breaking Changes?

  • OpenAPI / REST HTTP API / feldera-types / manager
  • Feldera SQL (Syntax, Semantics)
  • feldera-sqllib (incl. dependencies fxp, etc.)
  • Python SDK
  • fda (CLI arguments)
  • Adapters (including configuration)
  • Storage Format / Checkpoints
  • Others (specify)

Describe Incompatible Changes

  • num_parsers of the Delta Lake and Iceberg input connectors is now nullable in the OpenAPI schema, and Option<u32> in feldera-types. Existing configurations that set a number continue to work. The default when it is absent changes from 4 to the number of CPU cores, at most 16.
  • All Delta Lake and Iceberg input connectors of a pipeline together keep at most two record batches per CPU core in the parsers. A pipeline with many connectors, or with a large num_parsers, now makes the additional batches wait.
  • feldera-fxp: Fixed<P, S> implements DeserializeWithContext for SqlSerdeConfig only. It was generic over the configuration type before.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Nov 10, 2026
View reviewed changes
Comment thread crates/connectors/src/integrated/delta_table.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Nov 10, 2026
View reviewed changes
Comment thread crates/feldera-types/src/transport/delta_table.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Nov 10, 2026
View reviewed changes
Comment thread crates/iceberg/src/input.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

@mythical-fred-oss

mythical-fred-oss Bot commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

What I ran

Command Result
cargo test -p feldera-adapterlib --lib utils:: ✅ 38 passed
cargo test -p feldera-types --lib serde_with_context ✅
cargo test -p feldera-fxp --features dbsp --lib ✅ 55 passed
cargo test -p static-connectors --lib delta_tests + the QA test below ✅
pre-commit run --files <changed> ⚠️ timed out here, but it regenerated web-console/.../types.gen.ts (num_parsers?: number | null and the new doc text)

Findings

  • Missing, high: types.gen.ts was not regenerated after the OpenAPI change, so the pre-merge pre-commit check will fail. Run the generator and commit the result.
  • Missing, medium (QA): the reorder path is covered only at the Arrow level. No test sends a reordered batch with nanosecond timestamps through the real record deserializer. I wrote the test below. It passes locally, so it adds coverage and doesn't expose a bug. Please also add a by-name fallback case: an absent nullable column should come back as NULL from the same batch.
  • Unsafe gate: as_erased has a SAFETY comment. I checked the typeid and lifetime argument and I'm convinced it is sound.
QA test (crates/static-connectors/src/test/delta_tests.rs)
+/// A batch with reversed columns, an extra column, and nanosecond timestamps,
+/// read by position after `timestamps_to_micros` and `order_columns`, gives
+/// back the records that were written.
+#[test]
+fn delta_positional_read_matches_written_records() {
+    use std::sync::Arc;
+    use arrow::array::{ArrayRef, Int64Array, RecordBatch};
+    use arrow::compute::cast;
+    use arrow::datatypes::{DataType, Field as ArrowField, TimeUnit};
+    use feldera_adapterlib::utils::arrow_layout::{has_positional_fields, order_columns};
+    use feldera_adapterlib::utils::arrow_timestamps::timestamps_to_micros;
+    use feldera_types::transport::delta_table::DeltaVariantEncoding;
+
+    let records: Vec<DeltaTestStruct> = (0..50).map(|i| delta_test_record(i * 2)).collect();
+    let fields = DeltaTestStruct::schema();
+    let arrow_schema = ArrowSchema::new(relation_to_arrow_fields(&fields, delta_schema_options()));
+    let batch = serde_arrow::to_record_batch(arrow_schema.fields(),
+        &SerializeWithContextWrapper::new(&records, &delta_output_serde_config(DeltaVariantEncoding::Variant))).unwrap();
+    let mut new_fields = vec![ArrowField::new("extra", DataType::Int64, false)];
+    let mut new_columns: Vec<ArrayRef> = vec![Arc::new(Int64Array::from(vec![7; records.len()]))];
+    for (field, column) in batch.schema().fields().iter().zip(batch.columns()).rev() {
+        match field.data_type() {
+            DataType::Timestamp(_, tz) => {
+                let ns = DataType::Timestamp(TimeUnit::Nanosecond, tz.clone());
+                new_fields.push(field.as_ref().clone().with_data_type(ns.clone()));
+                new_columns.push(cast(column, &ns).unwrap());
+            }
+            _ => { new_fields.push(field.as_ref().clone()); new_columns.push(column.clone()); }
+        }
+    }
+    let batch = RecordBatch::try_new(Arc::new(ArrowSchema::new(new_fields)), new_columns).unwrap();
+    let batch = timestamps_to_micros(batch).unwrap();
+    let (batch, reason) = order_columns(batch, &fields);
+    assert_eq!(reason, None);
+    assert!(has_positional_fields(batch.schema_ref()));
+    let read: Vec<DeltaTestStruct> = Vec::deserialize_with_context(
+        serde_arrow::Deserializer::from_record_batch(&batch).unwrap(),
+        &delta_input_serde_config().with_positional_fields(true)).unwrap();
+    assert_eq!(read, records);
+}
bestouff reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bestouff
bestouff force-pushed the faster-parser branch from 090c35c to 7734c2b Compare November 12, 2026 10:32
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Nov 12, 2026
View reviewed changes
Comment thread crates/static-connectors/src/deinput.rs Resolved

Uh oh!

There was an error while loading. Please reload this page.

@mythical-fred-oss

mythical-fred-oss Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

What I ran (debug builds, at 24b9e84)

Command Result
cargo check --locked -p feldera-adapterlib -p feldera-types -p feldera-fxp -p feldera-sqllib ✅
cargo test --locked -p feldera-adapterlib -p feldera-types -p feldera-fxp --features feldera-fxp/dbsp ✅ all pass
cargo test --locked -p feldera-sqllib --lib timestamp ✅ 58 pass
cargo test --locked -p static-connectors delta (includes the delta connector suites) ✅ 186 pass
New QA tests (diff below) ✅ 3 pass

Missing, medium (SQL type coverage gate): the new positional and I128 paths are tested only through DeltaTestStruct, and its nested TestStruct uses derived serde (deserialize_without_context!). The positional deserialize_tuple path for compiler-generated nested ROWs is therefore never exercised. That includes nullable ROWs, ROWs inside ARRAY, and DECIMAL inside ARRAY/MAP (the FixedSizeBinary(16) rewrite). These columns are also missing: unsigned ints, TIME, INTERVAL, CHAR. I wrote tests for the nested cases and for sliced batches, which DataFusion produces, and they pass. Please add them, or equivalents, to the PR.

Missing, medium: Iceberg has no deserialization test for the new path (sql_fields, timestamps_to_micros, decimal significands together with UuidFormat::Binary). The PR description also says the Iceberg suites were run only before the rebase.

QA tests (all pass)
diff --git a/crates/adapterlib/src/utils/arrow_layout.rs b/crates/adapterlib/src/utils/arrow_layout.rs
index 26fda65..da14dba 100644
--- a/crates/adapterlib/src/utils/arrow_layout.rs
+++ b/crates/adapterlib/src/utils/arrow_layout.rs
@@ -400,3 +400,37 @@ mod test {
         assert_eq!(ordered.column(1).data_type(), &DataType::Decimal128(10, 3));
     }
 }
+
+#[cfg(test)]
+mod qa_test {
+    use std::sync::Arc;
+
+    use arrow::array::{Array, ArrayRef, AsArray, Decimal128Array, RecordBatch};
+    use feldera_types::program_schema::{ColumnType, Field, SqlIdentifier};
+
+    use super::order_columns;
+
+    /// A sliced batch, as DataFusion produces, keeps the slice offset.
+    #[test]
+    fn sliced_decimal_significands() {
+        let decimals: ArrayRef = Arc::new(
+            Decimal128Array::from(vec![Some(1), Some(2), None, Some(4)])
+                .with_precision_and_scale(10, 2)
+                .unwrap(),
+        );
+        let batch = RecordBatch::try_from_iter(vec![("d", decimals)])
+            .unwrap()
+            .slice(1, 3);
+        let fields = [Field::new(
+            SqlIdentifier::new("d", false),
+            ColumnType::decimal(10, 2, true),
+        )];
+        let (ordered, reason) = order_columns(batch, &fields);
+        assert_eq!(reason, None);
+        let d = ordered.column(0).as_fixed_size_binary();
+        assert_eq!(d.len(), 3);
+        assert_eq!(d.value(0), 2_i128.to_ne_bytes());
+        assert!(d.is_null(1));
+        assert_eq!(d.value(2), 4_i128.to_ne_bytes());
+    }
+}
diff --git a/crates/static-connectors/src/test/delta_tests.rs b/crates/static-connectors/src/test/delta_tests.rs
index 77a05d0..3231ab4 100644
--- a/crates/static-connectors/src/test/delta_tests.rs
+++ b/crates/static-connectors/src/test/delta_tests.rs
@@ -264,3 +264,178 @@ fn delta_string_column_is_read_as_timestamp() {
     assert!(!positional);
     assert_eq!(read, expected);
 }
+
+#[derive(Debug, Default, PartialEq, Eq, Clone)]
+struct QaInner {
+    a: bool,
+    b: Option<i64>,
+}
+feldera_types::deserialize_table_record!(QaInner["QaInner", feldera_sqllib::SqlVariant, 2] {
+    (a, "a", false, bool, |_| None),
+    (b, "b", false, Option<i64>, |_| Some(None))
+});
+
+#[derive(Debug, Default, PartialEq, Eq, Clone)]
+struct QaOuter {
+    id: i64,
+    inner: Option<QaInner>,
+    list: Vec<QaInner>,
+}
+feldera_types::deserialize_table_record!(QaOuter["QaOuter", feldera_sqllib::SqlVariant, 3] {
+    (id, "id", false, i64, |_| None),
+    (inner, "inner", false, Option<QaInner>, |_| Some(None)),
+    (list, "list", false, Vec<QaInner>, |_| None)
+});
+
+/// A nullable nested ROW with reversed fields, generated as the compiler does,
+/// is read by position.
+#[test]
+fn qa_positional_nullable_nested_row() {
+    use arrow::array::{ArrayRef, BooleanArray, Int64Array, ListArray, RecordBatch, StructArray};
+    use arrow::buffer::{NullBuffer, OffsetBuffer};
+    use arrow::datatypes::{DataType, Field as AF, Fields};
+    use feldera_adapterlib::utils::arrow_layout::{has_positional_fields, order_columns};
+    use feldera_types::program_schema::{ColumnType, Field, SqlIdentifier};
+    use std::sync::Arc;
+
+    // Fields stored as (b, a).
+    let inner_fields = Fields::from(vec![
+        AF::new("b", DataType::Int64, true),
+        AF::new("A", DataType::Boolean, false),
+    ]);
+    let inner_cols = |b: Vec<Option<i64>>, a: Vec<bool>| -> Vec<ArrayRef> {
+        vec![Arc::new(Int64Array::from(b)), Arc::new(BooleanArray::from(a))]
+    };
+    let inner = StructArray::new(
+        inner_fields.clone(),
+        inner_cols(vec![Some(1), None, Some(3)], vec![true, false, true]),
+        Some(NullBuffer::from(vec![true, false, true])),
+    );
+    let items = StructArray::new(
+        inner_fields.clone(),
+        inner_cols(vec![Some(7), None], vec![false, true]),
+        None,
+    );
+    let list = ListArray::new(
+        Arc::new(AF::new("item", DataType::Struct(inner_fields), false)),
+        OffsetBuffer::from_lengths([2, 0, 0]),
+        Arc::new(items),
+        None,
+    );
+    let batch = RecordBatch::try_from_iter(vec![
+        ("list", Arc::new(list) as ArrayRef),
+        ("inner", Arc::new(inner) as ArrayRef),
+        ("id", Arc::new(Int64Array::from(vec![1, 2, 3])) as ArrayRef),
+    ])
+    .unwrap();
+
+    let f = |n: &str, t| Field::new(SqlIdentifier::new(n, false), t);
+    let row = ColumnType::structure(
+        true,
+        &[f("a", ColumnType::boolean(false)), f("b", ColumnType::bigint(true))],
+    );
+    let mut row_nn = row.clone();
+    row_nn.nullable = false;
+    let fields = [
+        f("id", ColumnType::bigint(false)),
+        f("inner", row),
+        f("list", ColumnType::array(false, row_nn)),
+    ];
+    let (batch, reason) = order_columns(batch, &fields);
+    assert_eq!(reason, None);
+    assert!(has_positional_fields(batch.schema_ref()));
+
+    let read: Vec<QaOuter> = Vec::deserialize_with_context(
+        serde_arrow::Deserializer::from_record_batch(&batch).unwrap(),
+        &delta_input_serde_config().with_positional_fields(true),
+    )
+    .unwrap();
+    assert_eq!(
+        read,
+        vec![
+            QaOuter {
+                id: 1,
+                inner: Some(QaInner { a: true, b: Some(1) }),
+                list: vec![
+                    QaInner { a: false, b: Some(7) },
+                    QaInner { a: true, b: None }
+                ],
+            },
+            QaOuter { id: 2, inner: None, list: vec![] },
+            QaOuter {
+                id: 3,
+                inner: Some(QaInner { a: true, b: Some(3) }),
+                list: vec![],
+            },
+        ]
+    );
+}
+
+#[derive(Debug, Default, PartialEq, Eq, Clone)]
+struct QaDec {
+    arr: Vec<Option<feldera_sqllib::SqlDecimal<10, 2>>>,
+    map: BTreeMap<String, feldera_sqllib::SqlDecimal<10, 2>>,
+}
+feldera_types::deserialize_table_record!(QaDec["QaDec", feldera_sqllib::SqlVariant, 2] {
+    (arr, "arr", false, Vec<Option<feldera_sqllib::SqlDecimal<10, 2>>>, |_| None),
+    (map, "map", false, BTreeMap<String, feldera_sqllib::SqlDecimal<10, 2>>, |_| None)
+});
+
+#[test]
+fn qa_positional_nested_decimals() {
+    use arrow::array::{
+        ArrayRef, Decimal128Array, ListArray, MapArray, RecordBatch, StringArray, StructArray,
+    };
+    use arrow::buffer::OffsetBuffer;
+    use arrow::datatypes::{DataType, Field as AF, Fields};
+    use feldera_adapterlib::utils::arrow_layout::order_columns;
+    use feldera_types::program_schema::{ColumnType, Field, SqlIdentifier};
+    use std::sync::Arc;
+
+    let dec = |v: Vec<Option<i128>>| {
+        Arc::new(Decimal128Array::from(v).with_precision_and_scale(10, 2).unwrap()) as ArrayRef
+    };
+    let arr = ListArray::new(
+        Arc::new(AF::new("item", DataType::Decimal128(10, 2), true)),
+        OffsetBuffer::from_lengths([2]),
+        dec(vec![Some(12345), None]),
+        None,
+    );
+    let entries = StructArray::new(
+        Fields::from(vec![
+            AF::new("key", DataType::Utf8, false),
+            AF::new("value", DataType::Decimal128(10, 2), false),
+        ]),
+        vec![Arc::new(StringArray::from(vec!["k"])) as ArrayRef, dec(vec![Some(-5)])],
+        None,
+    );
+    let map = MapArray::new(
+        Arc::new(AF::new("entries", arrow::array::Array::data_type(&entries).clone(), false)),
+        OffsetBuffer::from_lengths([1]),
+        entries,
+        None,
+        false,
+    );
+    let batch = RecordBatch::try_from_iter(vec![
+        ("map", Arc::new(map) as ArrayRef),
+        ("arr", Arc::new(arr) as ArrayRef),
+    ])
+    .unwrap();
+    let f = |n: &str, t| Field::new(SqlIdentifier::new(n, false), t);
+    let fields = [
+        f("arr", ColumnType::array(false, ColumnType::decimal(10, 2, true))),
+        f(
+            "map",
+            ColumnType::map(false, ColumnType::varchar(false), ColumnType::decimal(10, 2, false)),
+        ),
+    ];
+    let (batch, reason) = order_columns(batch, &fields);
+    assert_eq!(reason, None);
+    let read: Vec<QaDec> = Vec::deserialize_with_context(
+        serde_arrow::Deserializer::from_record_batch(&batch).unwrap(),
+        &delta_input_serde_config().with_positional_fields(true),
+    )
+    .unwrap();
+    assert_eq!(read[0].arr, vec![Some("123.45".parse().unwrap()), None]);
+    assert_eq!(read[0].map.get("k"), Some(&"-0.05".parse().unwrap()));
+}
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@bestouff
bestouff requested a review from ryzhyk November 12, 2026 14:31
bestouff and others added 5 commits November 12, 2026 16:12
@bestouff
[connectors] Parse Arrow batches faster in the Delta Lake and Iceberg…
4a651c7
… inputs.

The Delta Lake and Iceberg input connectors spent most of their CPU
time in the conversion of Arrow record batches to SQL records. On a
benchmark with wide rows (about 100 fields), one batch of 8192 rows
took about 205 ms to parse. It now takes about 58 ms.

Changes:

- Read struct fields by position. A layout pass puts the columns of a
  batch in the order of the SQL table and marks the batch with
  `feldera.positional_fields`. The deserializer then reads a sequence
  and does not compare field names. A batch with an absent column is
  read by name as before.
- Erase the deserializer one time only. The context deserializer
  erased a deserializer that was already erased, which made one heap
  allocation for each visitor call.
- Read a `Decimal128` column as 128-bit significands when its scale is
  the scale of the SQL column. Other decimals go through a string as
  before.
- Normalize timestamp columns one time for each batch.
- Run the parsers on a dedicated CPU thread pool, so that they do not
  occupy the Tokio worker threads.
- Make `num_parsers` optional. When it is absent, the pipeline selects
  the value from the number of CPUs of its host, not the pipeline
  manager. The Iceberg file read concurrency keeps its default of 4.

Also add the `delta_parse` benchmark in `static-connectors`.

Signed-off-by: Xavier Bestel <[email protected]>
@feldera-bot @bestouff
[ci] apply automatic fixes
32b94bd
Signed-off-by: feldera-bot <[email protected]>
@bestouff
[connectors] Arrow parsing: read string timestamps, limit the batches…
087abe2
… in flight.

Changes after review of the faster Arrow parsing:

- A string column loads into a SQL TIMESTAMP column again. The new
  `TimestampFormat::MicrosSinceEpochOrString` reads an integer as
  microseconds and parses a string with the format that the Delta Lake
  and Iceberg inputs used before. The startup check accepts string
  columns and continues to reject integer columns.
- All job queues together keep at most two jobs per CPU core in flight.
  The default `num_parsers` is now up to 16 for each connector, so
  without this limit a pipeline with many connectors held more batches
  in memory than before.
- The Iceberg input panics with a clear message if a batch is read
  before the SQL columns are set. Before, it read a batch with no
  columns, and each record failed.
- Add tests that send a batch through the record deserializer: columns
  in a different order with nanosecond timestamps, an absent nullable
  column, and a string timestamp column.
- Add the changelog entry.

Signed-off-by: Xavier Bestel <[email protected]>
@bestouff
[connectors] Add a benchmark of a pipeline that reads a wide Delta ta…
cf57319
…ble.

`delta_pipeline` writes a local Delta table with the wide CDC record of
the `delta_parse` benchmark. It then runs the Delta Lake input connector
in `snapshot` mode, through the controller, into an input table of a
circuit that counts the records, and prints the rate.

Unlike `delta_parse`, it covers the Parquet read, DataFusion, the job
queue and the parser pool. Arguments: the table directory, the number of
batches of 8192 rows in a new table, and `num_parsers`.

Signed-off-by: Xavier Bestel <[email protected]>
@bestouff
[connectors] Arrow parsing: do not trust a positional mark from outside.
5e43a58
`ArrowZSetStream` and `ArrowMapStream` read a batch by position when its
schema metadata has `feldera.positional_fields`. They serve all Arrow
inputs, and schema metadata also comes from a file or from a client. A
batch that only claimed to be in order was read into the wrong columns.

The mark is now a random value of the process, which only
`order_columns` writes, so a batch from outside is read by name.

Also add tests for the positional and significand paths: a nullable
nested ROW and a ROW in an ARRAY with records as the SQL compiler
generates them, a DECIMAL in an ARRAY and in a MAP, a sliced batch, and
the Iceberg configuration with a binary UUID, a nanosecond timestamp and
a decimal.

Signed-off-by: Xavier Bestel <[email protected]>
@bestouff
bestouff force-pushed the faster-parser branch from 45e66d8 to 5e43a58 Compare November 12, 2026 15:12

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@mythical-fred-oss mythical-fred-oss[bot] mythical-fred-oss[bot] left review comments
@ryzhyk ryzhyk Awaiting requested review from ryzhyk

At least 1 approving review is required to merge this pull request.

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@bestouff @feldera-bot
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.