Repository: clflushopt/tpchgen-rs
Pull request #74: Implement arrow conversion for remaining tables
Author: login: alamb
Created: 2025-03-27T12:22:57Z | Merged: 2025-03-28T00:31:32Z

- part of https://github.com/clflushopt/tpchgen-rs/issues/46
- Follow on to https://github.com/clflushopt/tpchgen-rs/pull/71

1. Implements table --> Arrow conversion for the remaining TPCH tables
2. Adds test coverage

It is mostly a mechanical (but large) PR


Diff:
diff --git a/tpchgen-arrow/src/conversions.rs b/tpchgen-arrow/src/conversions.rs
index 552d143..d0bdd9d 100644
--- a/tpchgen-arrow/src/conversions.rs
+++ b/tpchgen-arrow/src/conversions.rs
@@ -1,5 +1,7 @@
 //! Routines to convert TPCH types to Arrow types
 
+use arrow::array::{StringViewArray, StringViewBuilder};
+use std::fmt::Write;
 use tpchgen::dates::TPCHDate;
 use tpchgen::decimal::TPCHDecimal;
 
@@ -17,6 +19,37 @@ pub fn to_arrow_date32(value: TPCHDate) -> i32 {
     value.into_inner() + TPCHDATE_TO_DATE32_OFFSET
 }
 
+/// Converts an iterator of TPCH decimals to an Arrow Decimal128Array
+pub fn decimal128_array_from_iter<I>(values: I) -> arrow::array::Decimal128Array
+where
+    I: Iterator<Item = TPCHDecimal>,
+{
+    let values = values.map(to_arrow_decimal);
+    arrow::array::Decimal128Array::from_iter_values(values)
+        .with_precision_and_scale(15, 2)
+        // safe to unwrap because 15,2 is within the valid range for Decimal128 (38)
+        .unwrap()
+}
+
+/// Coverts an iterator of displayable values to an Arrow StringViewArray
+///
+/// This results in an extra copy of the data, which could be avoided for some types
+pub fn string_view_array_from_display_iter<I>(values: I) -> StringViewArray
+where
+    I: Iterator<Item: std::fmt::Display>,
+{
+    let mut buffer = String::new();
+    let values = values.into_iter();
+    let size_hint = values.size_hint().0;
+    let mut builder = StringViewBuilder::with_capacity(size_hint);
+    for v in values {
+        buffer.clear();
+        write!(&mut buffer, "{v}").unwrap();
+        builder.append_value(&buffer);
+    }
+    builder.finish()
+}
+
 /// Number of days that must be added to a TPCH date to get an Arrow `Date32` value.
 ///
 /// * Arrow `Date32` are days since the epoch (1970-01-01)
diff --git a/tpchgen-arrow/src/customer.rs b/tpchgen-arrow/src/customer.rs
new file mode 100644
index 0000000..7aefe1b
--- /dev/null
+++ b/tpchgen-arrow/src/customer.rs
@@ -0,0 +1,121 @@
+use crate::conversions::{decimal128_array_from_iter, string_view_array_from_display_iter};
+use crate::{DEFAULT_BATCH_SIZE, RecordBatchIterator};
+use arrow::array::{Int64Array, RecordBatch};
+use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
+use std::sync::{Arc, LazyLock};
+use tpchgen::generators::{CustomerGenerator, CustomerGeneratorIterator};
+
+/// Generate [`Customer`]s in [`RecordBatch`] format
+///
+/// [`Customer`]: tpchgen::generators::Customer
+///
+/// # Example
+/// ```
+/// # use tpchgen::generators::{CustomerGenerator};
+/// # use tpchgen_arrow::CustomerArrow;
+///
+/// // Create a SF=1.0 generator and wrap it in an Arrow generator
+/// let generator = CustomerGenerator::new(1.0, 1, 1);
+/// let mut arrow_generator = CustomerArrow::new(generator)
+///   .with_batch_size(10);
+/// // Read the first 10 batches
+/// let batch = arrow_generator.next().unwrap();
+/// // compare the output by pretty printing it
+/// let formatted_batches = arrow::util::pretty::pretty_format_batches(&[batch])
+///   .unwrap()
+///   .to_string();
+/// let lines = formatted_batches.lines().collect::<Vec<_>>();
+/// assert_eq!(lines, vec![
+///   "+-----------+--------------------+---------------------------------------+-------------+-----------------+-----------+--------------+-------------------------------------------------------------------------------------------------------------------+",
+///   "| c_custkey | c_name             | c_address                             | c_nationkey | c_phone         | c_acctbal | c_mktsegment | c_comment                                                                                                         |",
+///   "+-----------+--------------------+---------------------------------------+-------------+-----------------+-----------+--------------+-------------------------------------------------------------------------------------------------------------------+",
+///   "| 1         | Customer#000000001 | IVhzIApeRb ot,c,E                     | 15          | 25-989-741-2988 | 711.56    | BUILDING     | to the even, regular platelets. regular, ironic epitaphs nag e                                                    |",
+///   "| 2         | Customer#000000002 | XSTf4,NCwDVaWNe6tEgvwfmRchLXak        | 13          | 23-768-687-3665 | 121.65    | AUTOMOBILE   | l accounts. blithely ironic theodolites integrate boldly: caref                                                   |",
+///   "| 3         | Customer#000000003 | MG9kdTD2WBHm                          | 1           | 11-719-748-3364 | 7498.12   | AUTOMOBILE   |  deposits eat slyly ironic, even instructions. express foxes detect slyly. blithely even accounts abov            |",
+///   "| 4         | Customer#000000004 | XxVSJsLAGtn                           | 4           | 14-128-190-5944 | 2866.83   | MACHINERY    |  requests. final, regular ideas sleep final accou                                                                 |",
+///   "| 5         | Customer#000000005 | KvpyuHCplrB84WgAiGV6sYpZq7Tj          | 3           | 13-750-942-6364 | 794.47    | HOUSEHOLD    | n accounts will have to unwind. foxes cajole accor                                                                |",
+///   "| 6         | Customer#000000006 | sKZz0CsnMD7mp4Xd0YrBvx,LREYKUWAh yVn  | 20          | 30-114-968-4951 | 7638.57   | AUTOMOBILE   | tions. even deposits boost according to the slyly bold packages. final accounts cajole requests. furious          |",
+///   "| 7         | Customer#000000007 | TcGe5gaZNgVePxU5kRrvXBfkasDTea        | 18          | 28-190-982-9759 | 9561.95   | AUTOMOBILE   | ainst the ironic, express theodolites. express, even pinto beans among the exp                                    |",
+///   "| 8         | Customer#000000008 | I0B10bB0AymmC, 0PrRYBCP1yGJ8xcBPmWhl5 | 17          | 27-147-574-9335 | 6819.74   | BUILDING     | among the slyly regular theodolites kindle blithely courts. carefully even theodolites haggle slyly along the ide |",
+///   "| 9         | Customer#000000009 | xKiAFTjUsCuxfeleNqefumTrjS            | 8           | 18-338-906-3675 | 8324.07   | FURNITURE    | r theodolites according to the requests wake thinly excuses: pending requests haggle furiousl                     |",
+///   "| 10        | Customer#000000010 | 6LrEaV6KR6PLVcgl2ArL Q3rqzLzcT1 v2    | 5           | 15-741-346-9870 | 2753.54   | HOUSEHOLD    | es regular deposits haggle. fur                                                                                   |",
+///   "+-----------+--------------------+---------------------------------------+-------------+-----------------+-----------+--------------+-------------------------------------------------------------------------------------------------------------------+",
+///   ]);
+/// ```
+pub struct CustomerArrow {
+    inner: CustomerGeneratorIterator<'static>,
+    batch_size: usize,
+}
+
+impl CustomerArrow {
+    pub fn new(generator: CustomerGenerator<'static>) -> Self {
+        Self {
+            inner: generator.iter(),
+            batch_size: DEFAULT_BATCH_SIZE,
+        }
+    }
+
+    /// Set the batch size
+    pub fn with_batch_size(mut self, batch_size: usize) -> Self {
+        self.batch_size = batch_size;
+        self
+    }
+}
+
+impl RecordBatchIterator for CustomerArrow {
+    fn schema(&self) -> &SchemaRef {
+        &CUSTOMER_SCHEMA
+    }
+}
+
+impl Iterator for CustomerArrow {
+    type Item = RecordBatch;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        // Get next rows to convert
+        let rows: Vec<_> = self.inner.by_ref().take(self.batch_size).collect();
+        if rows.is_empty() {
+            return None;
+        }
+
+        let c_custkey = Int64Array::from_iter_values(rows.iter().map(|r| r.c_custkey));
+        let c_name = string_view_array_from_display_iter(rows.iter().map(|r| r.c_name));
+        let c_address = string_view_array_from_display_iter(rows.iter().map(|r| &r.c_address));
+        let c_nationkey = Int64Array::from_iter_values(rows.iter().map(|r| r.c_nationkey));
+        let c_phone = string_view_array_from_display_iter(rows.iter().map(|r| &r.c_phone));
+        let c_acctbal = decimal128_array_from_iter(rows.iter().map(|r| r.c_acctbal));
+        let c_mktsegment = string_view_array_from_display_iter(rows.iter().map(|r| r.c_mktsegment));
+        let c_comment = string_view_array_from_display_iter(rows.iter().map(|r| r.c_comment));
+
+        let batch = RecordBatch::try_new(
+            Arc::clone(self.schema()),
+            vec![
+                Arc::new(c_custkey),
+                Arc::new(c_name),
+                Arc::new(c_address),
+                Arc::new(c_nationkey),
+                Arc::new(c_phone),
+                Arc::new(c_acctbal),
+                Arc::new(c_mktsegment),
+                Arc::new(c_comment),
+            ],
+        )
+        .unwrap();
+        Some(batch)
+    }
+}
+
+/// Schema for the Customer
+static CUSTOMER_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(make_customer_schema);
+fn make_customer_schema() -> SchemaRef {
+    Arc::new(Schema::new(vec![
+        Field::new("c_custkey", DataType::Int64, false),
+        Field::new("c_name", DataType::Utf8View, false),
+        Field::new("c_address", DataType::Utf8View, false),
+        Field::new("c_nationkey", DataType::Int64, false),
+        Field::new("c_phone", DataType::Utf8View, false),
+        Field::new("c_acctbal", DataType::Decimal128(15, 2), false),
+        Field::new("c_mktsegment", DataType::Utf8View, false),
+        Field::new("c_comment", DataType::Utf8View, false),
+    ]))
+}
diff --git a/tpchgen-arrow/src/lib.rs b/tpchgen-arrow/src/lib.rs
index 329f9bd..1f361a8 100644
--- a/tpchgen-arrow/src/lib.rs
+++ b/tpchgen-arrow/src/lib.rs
@@ -35,11 +35,25 @@
 //! ]);
 //! ```
 pub mod conversions;
+mod customer;
 mod lineitem;
+mod nation;
+mod order;
+mod part;
+mod partsupp;
+mod region;
+mod supplier;
 
 use arrow::array::RecordBatch;
 use arrow::datatypes::SchemaRef;
+pub use customer::CustomerArrow;
 pub use lineitem::LineItemArrow;
+pub use nation::NationArrow;
+pub use order::OrderArrow;
+pub use part::PartArrow;
+pub use partsupp::PartSuppArrow;
+pub use region::RegionArrow;
+pub use supplier::SupplierArrow;
 
 /// Iterator of Arrow [`RecordBatch`] that also knows its schema
 pub trait RecordBatchIterator: Iterator<Item = RecordBatch> + Send {
diff --git a/tpchgen-arrow/src/lineitem.rs b/tpchgen-arrow/src/lineitem.rs
index 0c6e0d2..17470ba 100644
--- a/tpchgen-arrow/src/lineitem.rs
+++ b/tpchgen-arrow/src/lineitem.rs
@@ -1,4 +1,4 @@
-use crate::conversions::{to_arrow_date32, to_arrow_decimal};
+use crate::conversions::{decimal128_array_from_iter, to_arrow_date32};
 use crate::{DEFAULT_BATCH_SIZE, RecordBatchIterator};
 use arrow::array::{
     Date32Array, Decimal128Array, Int32Array, Int64Array, RecordBatch, StringViewArray,
@@ -7,9 +7,6 @@ use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
 use std::sync::{Arc, LazyLock};
 use tpchgen::generators::{LineItemGenerator, LineItemGeneratorIterator};
 
-/// Schema for the LineItem table
-static LINEITEM_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(make_lineitem_schema);
-
 /// Generate  [`LineItem`]s in [`RecordBatch`] format
 ///
 /// [`LineItem`]: tpchgen::generators::LineItem
@@ -83,7 +80,7 @@ impl Iterator for LineItemArrow {
 
     /// Generate the next batch of data, if there is one
     fn next(&mut self) -> Option<Self::Item> {
-        // Get some rows to convert
+        // Get next rows to convert
         let rows: Vec<_> = self.inner.by_ref().take(self.batch_size).collect();
         if rows.is_empty() {
             return None;
@@ -101,23 +98,10 @@ impl Iterator for LineItemArrow {
         }))
         .with_precision_and_scale(15, 2)
         .unwrap();
-        let l_extended_price = Decimal128Array::from_iter_values(
-            rows.iter()
-                .map(|row| row.l_extendedprice)
-                .map(to_arrow_decimal),
-        )
-        .with_precision_and_scale(15, 2)
-        .unwrap();
-        let l_discount = Decimal128Array::from_iter_values(
-            rows.iter().map(|row| row.l_discount).map(to_arrow_decimal),
-        )
-        .with_precision_and_scale(15, 2)
-        .unwrap();
-        let l_tax = Decimal128Array::from_iter_values(
-            rows.iter().map(|row| row.l_tax).map(to_arrow_decimal),
-        )
-        .with_precision_and_scale(15, 2)
-        .unwrap();
+        let l_extended_price =
+            decimal128_array_from_iter(rows.iter().map(|row| row.l_extendedprice));
+        let l_discount = decimal128_array_from_iter(rows.iter().map(|row| row.l_discount));
+        let l_tax = decimal128_array_from_iter(rows.iter().map(|row| row.l_tax));
         let l_returnflag =
             StringViewArray::from_iter_values(rows.iter().map(|row| row.l_returnflag));
         let l_linestatus =
@@ -165,6 +149,9 @@ impl Iterator for LineItemArrow {
     }
 }
 
+/// Schema for the LineItem table
+static LINEITEM_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(make_lineitem_schema);
+
 fn make_lineitem_schema() -> SchemaRef {
     Arc::new(Schema::new(vec![
         Field::new("l_orderkey", DataType::Int64, false),
diff --git a/tpchgen-arrow/src/nation.rs b/tpchgen-arrow/src/nation.rs
new file mode 100644
index 0000000..aca67f1
--- /dev/null
+++ b/tpchgen-arrow/src/nation.rs
@@ -0,0 +1,108 @@
+use crate::{DEFAULT_BATCH_SIZE, RecordBatchIterator};
+use arrow::array::{Int64Array, RecordBatch, StringViewArray};
+use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
+use std::sync::{Arc, LazyLock};
+use tpchgen::generators::{NationGenerator, NationGeneratorIterator};
+
+/// Generate  [`Nation`]s in [`RecordBatch`] format
+///
+/// [`Nation`]: tpchgen::generators::Nation
+///
+/// # Example
+/// ```
+/// # use tpchgen::generators::{NationGenerator};
+/// # use tpchgen_arrow::NationArrow;
+///
+/// // Create a SF=1.0 generator and wrap it in an Arrow generator
+/// let generator = NationGenerator::new(1.0, 1, 1);
+/// let mut arrow_generator = NationArrow::new(generator)
+///   .with_batch_size(10);
+/// // Read the first 10 batches
+/// let batch = arrow_generator.next().unwrap();
+/// // compare the output by pretty printing it
+/// let formatted_batches = arrow::util::pretty::pretty_format_batches(&[batch])
+///   .unwrap()
+///   .to_string();
+/// let lines = formatted_batches.lines().collect::<Vec<_>>();
+/// assert_eq!(lines, vec![
+///   "+-------------+-----------+-------------+--------------------------------------------------------------------------------------------------------------------+",
+///   "| n_nationkey | n_name    | n_regionkey | n_comment                                                                                                          |",
+///   "+-------------+-----------+-------------+--------------------------------------------------------------------------------------------------------------------+",
+///   "| 0           | ALGERIA   | 0           |  haggle. carefully final deposits detect slyly agai                                                                |",
+///   "| 1           | ARGENTINA | 1           | al foxes promise slyly according to the regular accounts. bold requests alon                                       |",
+///   "| 2           | BRAZIL    | 1           | y alongside of the pending deposits. carefully special packages are about the ironic forges. slyly special         |",
+///   "| 3           | CANADA    | 1           | eas hang ironic, silent packages. slyly regular packages are furiously over the tithes. fluffily bold              |",
+///   "| 4           | EGYPT     | 4           | y above the carefully unusual theodolites. final dugouts are quickly across the furiously regular d                |",
+///   "| 5           | ETHIOPIA  | 0           | ven packages wake quickly. regu                                                                                    |",
+///   "| 6           | FRANCE    | 3           | refully final requests. regular, ironi                                                                             |",
+///   "| 7           | GERMANY   | 3           | l platelets. regular accounts x-ray: unusual, regular acco                                                         |",
+///   "| 8           | INDIA     | 2           | ss excuses cajole slyly across the packages. deposits print aroun                                                  |",
+///   "| 9           | INDONESIA | 2           |  slyly express asymptotes. regular deposits haggle slyly. carefully ironic hockey players sleep blithely. carefull |",
+///   "+-------------+-----------+-------------+--------------------------------------------------------------------------------------------------------------------+"
+/// ]);
+/// ```
+pub struct NationArrow {
+    inner: NationGeneratorIterator<'static>,
+    batch_size: usize,
+}
+
+impl NationArrow {
+    pub fn new(generator: NationGenerator<'static>) -> Self {
+        Self {
+            inner: generator.iter(),
+            batch_size: DEFAULT_BATCH_SIZE,
+        }
+    }
+
+    /// Set the batch size
+    pub fn with_batch_size(mut self, batch_size: usize) -> Self {
+        self.batch_size = batch_size;
+        self
+    }
+}
+
+impl RecordBatchIterator for NationArrow {
+    fn schema(&self) -> &SchemaRef {
+        &NATION_SCHEMA
+    }
+}
+
+impl Iterator for NationArrow {
+    type Item = RecordBatch;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        // Get next rows to convert
+        let rows: Vec<_> = self.inner.by_ref().take(self.batch_size).collect();
+        if rows.is_empty() {
+            return None;
+        }
+
+        let n_nationkey = Int64Array::from_iter_values(rows.iter().map(|r| r.n_nationkey));
+        let n_name = StringViewArray::from_iter_values(rows.iter().map(|r| r.n_name));
+        let n_regionkey = Int64Array::from_iter_values(rows.iter().map(|r| r.n_regionkey));
+        let n_comment = StringViewArray::from_iter_values(rows.iter().map(|r| r.n_comment));
+
+        let batch = RecordBatch::try_new(
+            Arc::clone(self.schema()),
+            vec![
+                Arc::new(n_nationkey),
+                Arc::new(n_name),
+                Arc::new(n_regionkey),
+                Arc::new(n_comment),
+            ],
+        )
+        .unwrap();
+        Some(batch)
+    }
+}
+
+/// Schema for the Nation
+static NATION_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(make_nation_schema);
+fn make_nation_schema() -> SchemaRef {
+    Arc::new(Schema::new(vec![
+        Field::new("n_nationkey", DataType::Int64, false),
+        Field::new("n_name", DataType::Utf8View, false),
+        Field::new("n_regionkey", DataType::Int64, false),
+        Field::new("n_comment", DataType::Utf8View, false),
+    ]))
+}
diff --git a/tpchgen-arrow/src/order.rs b/tpchgen-arrow/src/order.rs
new file mode 100644
index 0000000..aab1323
--- /dev/null
+++ b/tpchgen-arrow/src/order.rs
@@ -0,0 +1,129 @@
+use crate::conversions::{
+    decimal128_array_from_iter, string_view_array_from_display_iter, to_arrow_date32,
+};
+use crate::{DEFAULT_BATCH_SIZE, RecordBatchIterator};
+use arrow::array::{Date32Array, Int32Array, Int64Array, RecordBatch, StringViewArray};
+use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
+use std::sync::{Arc, LazyLock};
+use tpchgen::generators::{OrderGenerator, OrderGeneratorIterator};
+
+/// Generate [`Order`]s in [`RecordBatch`] format
+///
+/// [`Order`]: tpchgen::generators::Order
+///
+/// # Example
+/// ```
+/// # use tpchgen::generators::{OrderGenerator};
+/// # use tpchgen_arrow::OrderArrow;
+///
+/// // Create a SF=1.0 generator and wrap it in an Arrow generator
+/// let generator = OrderGenerator::new(1.0, 1, 1);
+/// let mut arrow_generator = OrderArrow::new(generator)
+///   .with_batch_size(10);
+/// // Read the first 10 batches
+/// let batch = arrow_generator.next().unwrap();
+/// // compare the output by pretty printing it
+/// let formatted_batches = arrow::util::pretty::pretty_format_batches(&[batch])
+///   .unwrap()
+///   .to_string();
+/// let lines = formatted_batches.lines().collect::<Vec<_>>();
+/// assert_eq!(lines, vec![
+///   "+------------+-----------+---------------+--------------+-------------+-----------------+-----------------+----------------+---------------------------------------------------------------------------+",
+///   "| o_orderkey | o_custkey | o_orderstatus | o_totalprice | o_orderdate | o_orderpriority | o_clerk         | o_shippriority | o_comment                                                                 |",
+///   "+------------+-----------+---------------+--------------+-------------+-----------------+-----------------+----------------+---------------------------------------------------------------------------+",
+///   "| 1          | 36901     | O             | 173665.47    | 1996-01-02  | 5-LOW           | Clerk#000000951 | 0              | nstructions sleep furiously among                                         |",
+///   "| 2          | 78002     | O             | 46929.18     | 1996-12-01  | 1-URGENT        | Clerk#000000880 | 0              |  foxes. pending accounts at the pending, silent asymptot                  |",
+///   "| 3          | 123314    | F             | 193846.25    | 1993-10-14  | 5-LOW           | Clerk#000000955 | 0              | sly final accounts boost. carefully regular ideas cajole carefully. depos |",
+///   "| 4          | 136777    | O             | 32151.78     | 1995-10-11  | 5-LOW           | Clerk#000000124 | 0              | sits. slyly regular warthogs cajole. regular, regular theodolites acro    |",
+///   "| 5          | 44485     | F             | 144659.20    | 1994-07-30  | 5-LOW           | Clerk#000000925 | 0              | quickly. bold deposits sleep slyly. packages use slyly                    |",
+///   "| 6          | 55624     | F             | 58749.59     | 1992-02-21  | 4-NOT SPECIFIED | Clerk#000000058 | 0              | ggle. special, final requests are against the furiously specia            |",
+///   "| 7          | 39136     | O             | 252004.18    | 1996-01-10  | 2-HIGH          | Clerk#000000470 | 0              | ly special requests                                                       |",
+///   "| 32         | 130057    | O             | 208660.75    | 1995-07-16  | 2-HIGH          | Clerk#000000616 | 0              | ise blithely bold, regular requests. quickly unusual dep                  |",
+///   "| 33         | 66958     | F             | 163243.98    | 1993-10-27  | 3-MEDIUM        | Clerk#000000409 | 0              | uriously. furiously final request                                         |",
+///   "| 34         | 61001     | O             | 58949.67     | 1998-07-21  | 3-MEDIUM        | Clerk#000000223 | 0              | ly final packages. fluffily final deposits wake blithely ideas. spe       |",
+///   "+------------+-----------+---------------+--------------+-------------+-----------------+-----------------+----------------+---------------------------------------------------------------------------+"
+/// ]);
+/// ```
+pub struct OrderArrow {
+    inner: OrderGeneratorIterator<'static>,
+    batch_size: usize,
+}
+
+impl OrderArrow {
+    pub fn new(generator: OrderGenerator<'static>) -> Self {
+        Self {
+            inner: generator.iter(),
+            batch_size: DEFAULT_BATCH_SIZE,
+        }
+    }
+
+    /// Set the batch size
+    pub fn with_batch_size(mut self, batch_size: usize) -> Self {
+        self.batch_size = batch_size;
+        self
+    }
+}
+
+impl RecordBatchIterator for OrderArrow {
+    fn schema(&self) -> &SchemaRef {
+        &ORDER_SCHEMA
+    }
+}
+
+impl Iterator for OrderArrow {
+    type Item = RecordBatch;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        // Get next rows to convert
+        let rows: Vec<_> = self.inner.by_ref().take(self.batch_size).collect();
+        if rows.is_empty() {
+            return None;
+        }
+
+        let o_orderkey = Int64Array::from_iter_values(rows.iter().map(|r| r.o_orderkey));
+        let o_custkey = Int64Array::from_iter_values(rows.iter().map(|r| r.o_custkey));
+        let o_orderstatus =
+            string_view_array_from_display_iter(rows.iter().map(|r| r.o_orderstatus));
+        let o_totalprice = decimal128_array_from_iter(rows.iter().map(|r| r.o_totalprice));
+        let o_orderdate =
+            Date32Array::from_iter_values(rows.iter().map(|r| r.o_orderdate).map(to_arrow_date32));
+        let o_orderpriority =
+            StringViewArray::from_iter_values(rows.iter().map(|r| r.o_orderpriority));
+        let o_clerk = string_view_array_from_display_iter(rows.iter().map(|r| r.o_clerk));
+        let o_shippriority = Int32Array::from_iter_values(rows.iter().map(|r| r.o_shippriority));
+        let o_comment = StringViewArray::from_iter_values(rows.iter().map(|r| r.o_comment));
+
+        let batch = RecordBatch::try_new(
+            Arc::clone(self.schema()),
+            vec![
+                Arc::new(o_orderkey),
+                Arc::new(o_custkey),
+                Arc::new(o_orderstatus),
+                Arc::new(o_totalprice),
+                Arc::new(o_orderdate),
+                Arc::new(o_orderpriority),
+                Arc::new(o_clerk),
+                Arc::new(o_shippriority),
+                Arc::new(o_comment),
+            ],
+        )
+        .unwrap();
+        Some(batch)
+    }
+}
+
+/// Schema for the Order
+static ORDER_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(make_order_schema);
+fn make_order_schema() -> SchemaRef {
+    Arc::new(Schema::new(vec![
+        Field::new("o_orderkey", DataType::Int64, false),
+        Field::new("o_custkey", DataType::Int64, false),
+        Field::new("o_orderstatus", DataType::Utf8View, false),
+        Field::new("o_totalprice", DataType::Decimal128(15, 2), false),
+        Field::new("o_orderdate", DataType::Date32, false),
+        Field::new("o_orderpriority", DataType::Utf8View, false),
+        Field::new("o_clerk", DataType::Utf8View, false),
+        Field::new("o_shippriority", DataType::Int32, false),
+        Field::new("o_comment", DataType::Utf8View, false),
+    ]))
+}
diff --git a/tpchgen-arrow/src/part.rs b/tpchgen-arrow/src/part.rs
new file mode 100644
index 0000000..4c52a2c
--- /dev/null
+++ b/tpchgen-arrow/src/part.rs
@@ -0,0 +1,124 @@
+use crate::conversions::{decimal128_array_from_iter, string_view_array_from_display_iter};
+use crate::{DEFAULT_BATCH_SIZE, RecordBatchIterator};
+use arrow::array::{Int32Array, Int64Array, RecordBatch, StringViewArray};
+use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
+use std::sync::{Arc, LazyLock};
+use tpchgen::generators::{PartGenerator, PartGeneratorIterator};
+
+/// Generate [`Part`]s in [`RecordBatch`] format
+///
+/// [`Part`]: tpchgen::generators::Part
+///
+/// # Example
+/// ```
+/// # use tpchgen::generators::{PartGenerator};
+/// # use tpchgen_arrow::PartArrow;
+///
+/// // Create a SF=1.0 generator and wrap it in an Arrow generator
+/// let generator = PartGenerator::new(1.0, 1, 1);
+/// let mut arrow_generator = PartArrow::new(generator)
+///   .with_batch_size(10);
+/// // Read the first 10 batches
+/// let batch = arrow_generator.next().unwrap();
+/// // compare the output by pretty printing it
+/// let formatted_batches = arrow::util::pretty::pretty_format_batches(&[batch])
+///   .unwrap()
+///   .to_string();
+/// let lines = formatted_batches.lines().collect::<Vec<_>>();
+/// assert_eq!(lines, vec![
+///  "+-----------+------------------------------------------+----------------+----------+-------------------------+--------+-------------+---------------+----------------------+",
+///   "| p_partkey | p_name                                   | p_mfgr         | p_brand  | p_type                  | p_size | p_container | p_retailprice | p_comment            |",
+///   "+-----------+------------------------------------------+----------------+----------+-------------------------+--------+-------------+---------------+----------------------+",
+///   "| 1         | goldenrod lavender spring chocolate lace | Manufacturer#1 | Brand#13 | PROMO BURNISHED COPPER  | 7      | JUMBO PKG   | 901.00        | ly. slyly ironi      |",
+///   "| 2         | blush thistle blue yellow saddle         | Manufacturer#1 | Brand#13 | LARGE BRUSHED BRASS     | 1      | LG CASE     | 902.00        | lar accounts amo     |",
+///   "| 3         | spring green yellow purple cornsilk      | Manufacturer#4 | Brand#42 | STANDARD POLISHED BRASS | 21     | WRAP CASE   | 903.00        | egular deposits hag  |",
+///   "| 4         | cornflower chocolate smoke green pink    | Manufacturer#3 | Brand#34 | SMALL PLATED BRASS      | 14     | MED DRUM    | 904.00        | p furiously r        |",
+///   "| 5         | forest brown coral puff cream            | Manufacturer#3 | Brand#32 | STANDARD POLISHED TIN   | 15     | SM PKG      | 905.00        |  wake carefully      |",
+///   "| 6         | bisque cornflower lawn forest magenta    | Manufacturer#2 | Brand#24 | PROMO PLATED STEEL      | 4      | MED BAG     | 906.00        | sual a               |",
+///   "| 7         | moccasin green thistle khaki floral      | Manufacturer#1 | Brand#11 | SMALL PLATED COPPER     | 45     | SM BAG      | 907.00        | lyly. ex             |",
+///   "| 8         | misty lace thistle snow royal            | Manufacturer#4 | Brand#44 | PROMO BURNISHED TIN     | 41     | LG DRUM     | 908.00        | eposi                |",
+///   "| 9         | thistle dim navajo dark gainsboro        | Manufacturer#4 | Brand#43 | SMALL BURNISHED STEEL   | 12     | WRAP CASE   | 909.00        | ironic foxe          |",
+///   "| 10        | linen pink saddle puff powder            | Manufacturer#5 | Brand#54 | LARGE BURNISHED STEEL   | 44     | LG CAN      | 910.01        | ithely final deposit |",
+///   "+-----------+------------------------------------------+----------------+----------+-------------------------+--------+-------------+---------------+----------------------+"
+/// ]);
+/// ```
+pub struct PartArrow {
+    inner: PartGeneratorIterator<'static>,
+    batch_size: usize,
+}
+
+impl PartArrow {
+    pub fn new(generator: PartGenerator<'static>) -> Self {
+        Self {
+            inner: generator.iter(),
+            batch_size: DEFAULT_BATCH_SIZE,
+        }
+    }
+
+    /// Set the batch size
+    pub fn with_batch_size(mut self, batch_size: usize) -> Self {
+        self.batch_size = batch_size;
+        self
+    }
+}
+
+impl RecordBatchIterator for PartArrow {
+    fn schema(&self) -> &SchemaRef {
+        &PART_SCHEMA
+    }
+}
+
+impl Iterator for PartArrow {
+    type Item = RecordBatch;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        // Get next rows to convert
+        let rows: Vec<_> = self.inner.by_ref().take(self.batch_size).collect();
+        if rows.is_empty() {
+            return None;
+        }
+
+        let p_partkey = Int64Array::from_iter_values(rows.iter().map(|r| r.p_partkey));
+        let p_name = string_view_array_from_display_iter(rows.iter().map(|r| &r.p_name));
+        let p_mfgr = string_view_array_from_display_iter(rows.iter().map(|r| r.p_mfgr));
+        let p_brand = string_view_array_from_display_iter(rows.iter().map(|r| r.p_brand));
+        let p_type = StringViewArray::from_iter_values(rows.iter().map(|r| r.p_type));
+        let p_size = Int32Array::from_iter_values(rows.iter().map(|r| r.p_size));
+        let p_container = StringViewArray::from_iter_values(rows.iter().map(|r| r.p_container));
+        let p_retailprice = decimal128_array_from_iter(rows.iter().map(|r| r.p_retailprice));
+        let p_comment = StringViewArray::from_iter_values(rows.iter().map(|r| r.p_comment));
+
+        let batch = RecordBatch::try_new(
+            Arc::clone(self.schema()),
+            vec![
+                Arc::new(p_partkey),
+                Arc::new(p_name),
+                Arc::new(p_mfgr),
+                Arc::new(p_brand),
+                Arc::new(p_type),
+                Arc::new(p_size),
+                Arc::new(p_container),
+                Arc::new(p_retailprice),
+                Arc::new(p_comment),
+            ],
+        )
+        .unwrap();
+        Some(batch)
+    }
+}
+
+/// Schema for the Part
+static PART_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(make_part_schema);
+fn make_part_schema() -> SchemaRef {
+    Arc::new(Schema::new(vec![
+        Field::new("p_partkey", DataType::Int64, false),
+        Field::new("p_name", DataType::Utf8View, false),
+        Field::new("p_mfgr", DataType::Utf8View, false),
+        Field::new("p_brand", DataType::Utf8View, false),
+        Field::new("p_type", DataType::Utf8View, false),
+        Field::new("p_size", DataType::Int32, false),
+        Field::new("p_container", DataType::Utf8View, false),
+        Field::new("p_retailprice", DataType::Decimal128(15, 2), false),
+        Field::new("p_comment", DataType::Utf8View, false),
+    ]))
+}
diff --git a/tpchgen-arrow/src/partsupp.rs b/tpchgen-arrow/src/partsupp.rs
new file mode 100644
index 0000000..8291b46
--- /dev/null
+++ b/tpchgen-arrow/src/partsupp.rs
@@ -0,0 +1,112 @@
+use crate::conversions::{decimal128_array_from_iter, string_view_array_from_display_iter};
+use crate::{DEFAULT_BATCH_SIZE, RecordBatchIterator};
+use arrow::array::{Int32Array, Int64Array, RecordBatch};
+use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
+use std::sync::{Arc, LazyLock};
+use tpchgen::generators::{PartSuppGenerator, PartSuppGeneratorIterator};
+
+/// Generate [`PartSupp`]s in [`RecordBatch`] format
+///
+/// [`PartSupp`]: tpchgen::generators::PartSupp
+///
+/// # Example
+/// ```
+/// # use tpchgen::generators::{PartSuppGenerator};
+/// # use tpchgen_arrow::PartSuppArrow;
+///
+/// // Create a SF=1.0 generator and wrap it in an Arrow generator
+/// let generator = PartSuppGenerator::new(1.0, 1, 1);
+/// let mut arrow_generator = PartSuppArrow::new(generator)
+///   .with_batch_size(10);
+/// // Read the first 10 batches
+/// let batch = arrow_generator.next().unwrap();
+/// // compare the output by pretty printing it
+/// let formatted_batches = arrow::util::pretty::pretty_format_batches(&[batch])
+///   .unwrap()
+///   .to_string();
+/// let lines = formatted_batches.lines().collect::<Vec<_>>();
+/// assert_eq!(lines, vec![
+///   "+------------+------------+-------------+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+",
+///   "| ps_partkey | ps_suppkey | ps_availqty | ps_supplycost | ps_comment                                                                                                                                                                                         |",
+///   "+------------+------------+-------------+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+",
+///   "| 1          | 2          | 3325        | 771.64        | , even theodolites. regular, final theodolites eat after the carefully pending foxes. furiously regular deposits sleep slyly. carefully bold realms above the ironic dependencies haggle careful   |",
+///   "| 1          | 2502       | 8076        | 993.49        | ven ideas. quickly even packages print. pending multipliers must have to are fluff                                                                                                                 |",
+///   "| 1          | 5002       | 3956        | 337.09        | after the fluffily ironic deposits? blithely special dependencies integrate furiously even excuses. blithely silent theodolites could have to haggle pending, express requests; fu                 |",
+///   "| 1          | 7502       | 4069        | 357.84        | al, regular dependencies serve carefully after the quickly final pinto beans. furiously even deposits sleep quickly final, silent pinto beans. fluffily reg                                        |",
+///   "| 2          | 3          | 8895        | 378.49        | nic accounts. final accounts sleep furiously about the ironic, bold packages. regular, regular accounts                                                                                            |",
+///   "| 2          | 2503       | 4969        | 915.27        | ptotes. quickly pending dependencies integrate furiously. fluffily ironic ideas impress blithely above the express accounts. furiously even epitaphs need to wak                                   |",
+///   "| 2          | 5003       | 8539        | 438.37        | blithely bold ideas. furiously stealthy packages sleep fluffily. slyly special deposits snooze furiously carefully regular accounts. regular deposits according to the accounts nag carefully slyl |",
+///   "| 2          | 7503       | 3025        | 306.39        | olites. deposits wake carefully. even, express requests cajole. carefully regular ex                                                                                                               |",
+///   "| 3          | 4          | 4651        | 920.92        | ilent foxes affix furiously quickly unusual requests. even packages across the carefully even theodolites nag above the sp                                                                         |",
+///   "| 3          | 2504       | 4093        | 498.13        | ending dependencies haggle fluffily. regular deposits boost quickly carefully regular requests. deposits affix furiously around the pinto beans. ironic, unusual platelets across the p            |",
+///   "+------------+------------+-------------+---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+"
+/// ]);
+/// ```
+pub struct PartSuppArrow {
+    inner: PartSuppGeneratorIterator<'static>,
+    batch_size: usize,
+}
+
+impl PartSuppArrow {
+    pub fn new(generator: PartSuppGenerator<'static>) -> Self {
+        Self {
+            inner: generator.iter(),
+            batch_size: DEFAULT_BATCH_SIZE,
+        }
+    }
+
+    /// Set the batch size
+    pub fn with_batch_size(mut self, batch_size: usize) -> Self {
+        self.batch_size = batch_size;
+        self
+    }
+}
+
+impl RecordBatchIterator for PartSuppArrow {
+    fn schema(&self) -> &SchemaRef {
+        &PARTSUPP_SCHEMA
+    }
+}
+
+impl Iterator for PartSuppArrow {
+    type Item = RecordBatch;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        // Get next rows to convert
+        let rows: Vec<_> = self.inner.by_ref().take(self.batch_size).collect();
+        if rows.is_empty() {
+            return None;
+        }
+
+        let ps_partkey = Int64Array::from_iter_values(rows.iter().map(|r| r.ps_partkey));
+        let ps_suppkey = Int64Array::from_iter_values(rows.iter().map(|r| r.ps_suppkey));
+        let ps_availqty = Int32Array::from_iter_values(rows.iter().map(|r| r.ps_availqty));
+        let ps_supplycost = decimal128_array_from_iter(rows.iter().map(|r| r.ps_supplycost));
+        let ps_comment = string_view_array_from_display_iter(rows.iter().map(|r| r.ps_comment));
+
+        let batch = RecordBatch::try_new(
+            Arc::clone(self.schema()),
+            vec![
+                Arc::new(ps_partkey),
+                Arc::new(ps_suppkey),
+                Arc::new(ps_availqty),
+                Arc::new(ps_supplycost),
+                Arc::new(ps_comment),
+            ],
+        )
+        .unwrap();
+        Some(batch)
+    }
+}
+
+/// Schema for the PartSupp
+static PARTSUPP_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(make_partsupp_schema);
+fn make_partsupp_schema() -> SchemaRef {
+    Arc::new(Schema::new(vec![
+        Field::new("ps_partkey", DataType::Int64, false),
+        Field::new("ps_suppkey", DataType::Int64, false),
+        Field::new("ps_availqty", DataType::Int32, false),
+        Field::new("ps_supplycost", DataType::Decimal128(15, 2), false),
+        Field::new("ps_comment", DataType::Utf8View, false),
+    ]))
+}
diff --git a/tpchgen-arrow/src/region.rs b/tpchgen-arrow/src/region.rs
new file mode 100644
index 0000000..c6fc944
--- /dev/null
+++ b/tpchgen-arrow/src/region.rs
@@ -0,0 +1,96 @@
+use crate::{DEFAULT_BATCH_SIZE, RecordBatchIterator};
+use arrow::array::{Int64Array, RecordBatch, StringViewArray};
+use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
+use std::sync::{Arc, LazyLock};
+use tpchgen::generators::{RegionGenerator, RegionGeneratorIterator};
+
+/// Generate  [`Region`]s in [`RecordBatch`] format
+///
+/// [`Region`]: tpchgen::generators::Region
+///
+/// # Example
+/// ```
+/// # use tpchgen::generators::{RegionGenerator};
+/// # use tpchgen_arrow::RegionArrow;
+///
+/// // Create a SF=1.0 generator and wrap it in an Arrow generator
+/// let generator = RegionGenerator::new(1.0, 1, 1);
+/// let mut arrow_generator = RegionArrow::new(generator)
+///   .with_batch_size(10);
+/// // Read the first 10 batches
+/// let batch = arrow_generator.next().unwrap();
+/// // compare the output by pretty printing it
+/// let formatted_batches = arrow::util::pretty::pretty_format_batches(&[batch])
+///   .unwrap()
+///   .to_string();
+/// let lines = formatted_batches.lines().collect::<Vec<_>>();
+/// assert_eq!(lines, vec![
+///   "+-------------+-------------+---------------------------------------------------------------------------------------------------------------------+",
+///   "| r_regionkey | r_name      | r_comment                                                                                                           |",
+///   "+-------------+-------------+---------------------------------------------------------------------------------------------------------------------+",
+///   "| 0           | AFRICA      | lar deposits. blithely final packages cajole. regular waters are final requests. regular accounts are according to  |",
+///   "| 1           | AMERICA     | hs use ironic, even requests. s                                                                                     |",
+///   "| 2           | ASIA        | ges. thinly even pinto beans ca                                                                                     |",
+///   "| 3           | EUROPE      | ly final courts cajole furiously final excuse                                                                       |",
+///   "| 4           | MIDDLE EAST | uickly special accounts cajole carefully blithely close requests. carefully final asymptotes haggle furiousl        |",
+///   "+-------------+-------------+---------------------------------------------------------------------------------------------------------------------+"
+/// ]);
+/// ```
+pub struct RegionArrow {
+    inner: RegionGeneratorIterator<'static>,
+    batch_size: usize,
+}
+
+impl RegionArrow {
+    pub fn new(generator: RegionGenerator<'static>) -> Self {
+        Self {
+            inner: generator.iter(),
+            batch_size: DEFAULT_BATCH_SIZE,
+        }
+    }
+
+    /// Set the batch size
+    pub fn with_batch_size(mut self, batch_size: usize) -> Self {
+        self.batch_size = batch_size;
+        self
+    }
+}
+
+impl RecordBatchIterator for RegionArrow {
+    fn schema(&self) -> &SchemaRef {
+        &REGION_SCHEMA
+    }
+}
+
+impl Iterator for RegionArrow {
+    type Item = RecordBatch;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        // Get next rows to convert
+        let rows: Vec<_> = self.inner.by_ref().take(self.batch_size).collect();
+        if rows.is_empty() {
+            return None;
+        }
+
+        let r_regionkey = Int64Array::from_iter_values(rows.iter().map(|r| r.r_regionkey));
+        let r_name = StringViewArray::from_iter_values(rows.iter().map(|r| r.r_name));
+        let r_comment = StringViewArray::from_iter_values(rows.iter().map(|r| r.r_comment));
+
+        let batch = RecordBatch::try_new(
+            Arc::clone(self.schema()),
+            vec![Arc::new(r_regionkey), Arc::new(r_name), Arc::new(r_comment)],
+        )
+        .unwrap();
+        Some(batch)
+    }
+}
+
+/// Schema for the Region
+static REGION_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(make_region_schema);
+fn make_region_schema() -> SchemaRef {
+    Arc::new(Schema::new(vec![
+        Field::new("r_regionkey", DataType::Int64, false),
+        Field::new("r_name", DataType::Utf8View, false),
+        Field::new("r_comment", DataType::Utf8View, false),
+    ]))
+}
diff --git a/tpchgen-arrow/src/supplier.rs b/tpchgen-arrow/src/supplier.rs
new file mode 100644
index 0000000..ecf8ad4
--- /dev/null
+++ b/tpchgen-arrow/src/supplier.rs
@@ -0,0 +1,105 @@
+use crate::conversions::{decimal128_array_from_iter, string_view_array_from_display_iter};
+use crate::{DEFAULT_BATCH_SIZE, RecordBatchIterator};
+use arrow::array::{Int64Array, RecordBatch};
+use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
+use std::sync::{Arc, LazyLock};
+use tpchgen::generators::{SupplierGenerator, SupplierGeneratorIterator};
+
+/// Generate [`Supplier`]s in [`RecordBatch`] format
+///
+/// [`Supplier`]: tpchgen::generators::Supplier
+///
+/// # Example:
+/// ```
+/// # use tpchgen::generators::{SupplierGenerator};
+/// # use tpchgen_arrow::SupplierArrow;
+///
+/// // Create a SF=1.0 generator and wrap it in an Arrow generator
+/// let generator = SupplierGenerator::new(1.0, 1, 1);
+/// let mut arrow_generator = SupplierArrow::new(generator)
+///   .with_batch_size(10);
+/// // Read the first 10 batches
+/// let batch = arrow_generator.next().unwrap();
+/// // compare the output by pretty printing it
+/// let formatted_batches = arrow::util::pretty::pretty_format_batches(&[batch])
+///   .unwrap()
+///   .to_string();
+/// let lines = formatted_batches.lines().collect::<Vec<_>>();
+/// assert_eq!(lines, vec![
+///   "+-----------+--------------------+-------------------------------------+-------------+-----------------+-----------+-----------------------------------------------------------------------------------------------------+", "| s_suppkey | s_name             | s_address                           | s_nationkey | s_phone         | s_acctbal | s_comment                                                                                           |", "+-----------+--------------------+-------------------------------------+-------------+-----------------+-----------+-----------------------------------------------------------------------------------------------------+", "| 1         | Supplier#000000001 |  N kD4on9OM Ipw3,gf0JBoQDd7tgrzrddZ | 17          | 27-918-335-1736 | 5755.94   | each slyly above the careful                                                                        |", "| 2         | Supplier#000000002 | 89eJ5ksX3ImxJQBvxObC,               | 5           | 15-679-861-2259 | 4032.68   |  slyly bold instructions. idle dependen                                                             |", "| 3         | Supplier#000000003 | q1,G3Pj6OjIuUYfUoH18BFTKP5aU9bEV3   | 1           | 11-383-516-1199 | 4192.40   | blithely silent requests after the express dependencies are sl                                      |", "| 4         | Supplier#000000004 | Bk7ah4CK8SYQTepEmvMkkgMwg           | 15          | 25-843-787-7479 | 4641.08   | riously even requests above the exp                                                                 |", "| 5         | Supplier#000000005 | Gcdm2rJRzl5qlTVzc                   | 11          | 21-151-690-3663 | -283.84   | . slyly regular pinto bea                                                                           |", "| 6         | Supplier#000000006 | tQxuVm7s7CnK                        | 14          | 24-696-997-4969 | 1365.79   | final accounts. regular dolphins use against the furiously ironic decoys.                           |", "| 7         | Supplier#000000007 | s,4TicNGB4uO6PaSqNBUq               | 23          | 33-990-965-2201 | 6820.35   | s unwind silently furiously regular courts. final requests are deposits. requests wake quietly blit |", "| 8         | Supplier#000000008 | 9Sq4bBH2FQEmaFOocY45sRTxo6yuoG      | 17          | 27-498-742-3860 | 7627.85   | al pinto beans. asymptotes haggl                                                                    |", "| 9         | Supplier#000000009 | 1KhUgZegwM3ua7dsYmekYBsK            | 10          | 20-403-398-8662 | 5302.37   | s. unusual, even requests along the furiously regular pac                                           |", "| 10        | Supplier#000000010 | Saygah3gYWMp72i PY                  | 24          | 34-852-489-8585 | 3891.91   | ing waters. regular requests ar                                                                     |", "+-----------+--------------------+-------------------------------------+-------------+-----------------+-----------+-----------------------------------------------------------------------------------------------------+"
+/// ]);
+/// ```
+pub struct SupplierArrow {
+    inner: SupplierGeneratorIterator<'static>,
+    batch_size: usize,
+}
+
+impl SupplierArrow {
+    pub fn new(generator: SupplierGenerator<'static>) -> Self {
+        Self {
+            inner: generator.iter(),
+            batch_size: DEFAULT_BATCH_SIZE,
+        }
+    }
+
+    /// Set the batch size
+    pub fn with_batch_size(mut self, batch_size: usize) -> Self {
+        self.batch_size = batch_size;
+        self
+    }
+}
+
+impl RecordBatchIterator for SupplierArrow {
+    fn schema(&self) -> &SchemaRef {
+        &SUPPLIER_SCHEMA
+    }
+}
+
+impl Iterator for SupplierArrow {
+    type Item = RecordBatch;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        // Get next rows to convert
+        let rows: Vec<_> = self.inner.by_ref().take(self.batch_size).collect();
+        if rows.is_empty() {
+            return None;
+        }
+
+        let s_suppkey = Int64Array::from_iter_values(rows.iter().map(|r| r.s_suppkey));
+        let s_name = string_view_array_from_display_iter(rows.iter().map(|r| r.s_name));
+        let s_address = string_view_array_from_display_iter(rows.iter().map(|r| &r.s_address));
+        let s_nationkey = Int64Array::from_iter_values(rows.iter().map(|r| r.s_nationkey));
+        let s_phone = string_view_array_from_display_iter(rows.iter().map(|r| &r.s_phone));
+        let s_acctbal = decimal128_array_from_iter(rows.iter().map(|r| r.s_acctbal));
+        let s_comment = string_view_array_from_display_iter(rows.iter().map(|r| &r.s_comment));
+
+        let batch = RecordBatch::try_new(
+            Arc::clone(self.schema()),
+            vec![
+                Arc::new(s_suppkey),
+                Arc::new(s_name),
+                Arc::new(s_address),
+                Arc::new(s_nationkey),
+                Arc::new(s_phone),
+                Arc::new(s_acctbal),
+                Arc::new(s_comment),
+            ],
+        )
+        .unwrap();
+        Some(batch)
+    }
+}
+
+/// Schema for the PartSupp
+static SUPPLIER_SCHEMA: LazyLock<SchemaRef> = LazyLock::new(make_supplier_schema);
+fn make_supplier_schema() -> SchemaRef {
+    Arc::new(Schema::new(vec![
+        Field::new("s_suppkey", DataType::Int64, false),
+        Field::new("s_name", DataType::Utf8View, false),
+        Field::new("s_address", DataType::Utf8View, false),
+        Field::new("s_nationkey", DataType::Int64, false),
+        Field::new("s_phone", DataType::Utf8View, false),
+        Field::new("s_acctbal", DataType::Decimal128(15, 2), false),
+        Field::new("s_comment", DataType::Utf8View, false),
+    ]))
+}
diff --git a/tpchgen-arrow/tests/reparse.rs b/tpchgen-arrow/tests/reparse.rs
index 388c891..a00f795 100644
--- a/tpchgen-arrow/tests/reparse.rs
+++ b/tpchgen-arrow/tests/reparse.rs
@@ -5,76 +5,157 @@ use arrow::array::RecordBatch;
 use arrow::datatypes::SchemaRef;
 use std::io::Write;
 use std::sync::Arc;
-use tpchgen::csv::LineItemCsv;
-use tpchgen::generators::{LineItem, LineItemGenerator};
-use tpchgen_arrow::{LineItemArrow, RecordBatchIterator};
+use tpchgen::csv::{
+    CustomerCsv, LineItemCsv, NationCsv, OrderCsv, PartCsv, PartSuppCsv, RegionCsv, SupplierCsv,
+};
+use tpchgen::generators::{
+    Customer, CustomerGenerator, LineItem, LineItemGenerator, Nation, NationGenerator, Order,
+    OrderGenerator, Part, PartGenerator, PartSupp, PartSuppGenerator, Region, RegionGenerator,
+    Supplier, SupplierGenerator,
+};
+use tpchgen_arrow::{
+    CustomerArrow, LineItemArrow, NationArrow, OrderArrow, PartArrow, PartSuppArrow,
+    RecordBatchIterator, RegionArrow, SupplierArrow,
+};
 
-#[test]
-fn test_tpchgen_lineitem_tbl() {
-    do_test_tpchgen_lineitem(0.1, TestFormat::TBL)
+/// Macro that defines tests for tbl for a given type
+macro_rules! test_row_type {
+    ($FUNCNAME:ident, $GENERATOR:ty, $ARROWITER:ty, $FORMATTYPE:expr) => {
+        #[test]
+        fn $FUNCNAME() {
+            let scale_factor = 0.1;
+            let batch_size = 1000;
+            let part = 1;
+            let part_count = 1;
+            let generator = <$GENERATOR>::new(scale_factor, part, part_count);
+            $FORMATTYPE.test(
+                generator.clone().iter(),
+                <$ARROWITER>::new(generator).with_batch_size(batch_size),
+            );
+        }
+    };
 }
 
-#[test]
-fn test_tpchgen_lineitem_csv() {
-    do_test_tpchgen_lineitem(0.1, TestFormat::CSV)
+test_row_type!(customer_tbl, CustomerGenerator, CustomerArrow, Test::tbl());
+test_row_type!(customer_csv, CustomerGenerator, CustomerArrow, Test::csv());
+test_row_type!(lineitem_tbl, LineItemGenerator, LineItemArrow, Test::tbl());
+test_row_type!(lineitem_csv, LineItemGenerator, LineItemArrow, Test::csv());
+test_row_type!(nation_tbl, NationGenerator, NationArrow, Test::tbl());
+test_row_type!(nation_csv, NationGenerator, NationArrow, Test::csv());
+test_row_type!(order_tbl, OrderGenerator, OrderArrow, Test::tbl());
+test_row_type!(order_csv, OrderGenerator, OrderArrow, Test::csv());
+test_row_type!(part_tbl, PartGenerator, PartArrow, Test::tbl());
+test_row_type!(part_csv, PartGenerator, PartArrow, Test::csv());
+test_row_type!(partsupp_tbl, PartSuppGenerator, PartSuppArrow, Test::tbl());
+test_row_type!(partsupp_csv, PartSuppGenerator, PartSuppArrow, Test::csv());
+test_row_type!(region_tbl, RegionGenerator, RegionArrow, Test::tbl());
+test_row_type!(region_csv, RegionGenerator, RegionArrow, Test::csv());
+test_row_type!(supplier_tbl, SupplierGenerator, SupplierArrow, Test::tbl());
+test_row_type!(supplier_csv, SupplierGenerator, SupplierArrow, Test::csv());
+
+/// Common trait for writing rows in TBL and CSV format
+trait RowType {
+    /// write a row in TBL format, WITHOUT newline
+    fn write_tbl_row(self, text_data: &mut Vec<u8>);
+    /// write the header in CSV format
+    fn write_csv_header(text_data: &mut Vec<u8>);
+    /// write a row in CSV format, WITH newline
+    fn write_csv_row(self, text_data: &mut Vec<u8>);
 }
 
-/// Generates LineItem's using the specified format and compares the results of
-/// parsing with the Arrow CSV parser with directly generated the batches
-fn do_test_tpchgen_lineitem(scale_factor: f64, format: TestFormat) {
-    let batch_size = 1000;
-
-    // TPCH scale factor 1
-    let lineitem_generator = LineItemGenerator::new(scale_factor, 1, 1);
-    let mut lineitem_iter = lineitem_generator.clone().iter();
-    let mut arrow_iter = LineItemArrow::new(lineitem_generator).with_batch_size(batch_size);
-
-    let mut batch_num = 0;
-    while let Some(arrow_batch) = arrow_iter.next() {
-        println!("Batch {}", batch_num);
-        batch_num += 1;
-        let mut text_data = Vec::new();
-        format.write_lineitem_header(&mut text_data);
-        lineitem_iter.by_ref().take(batch_size).for_each(|item| {
-            format.write_lineitem(item, &mut text_data);
-        });
-        let tbl_batch = format.parse(&text_data, arrow_iter.schema(), batch_size);
-        assert_eq!(tbl_batch, arrow_batch);
-    }
+/// Macro that implements the RowType trait for a given type
+macro_rules! impl_row_type {
+    ($type:ty, $csv_type:ty) => {
+        impl RowType for $type {
+            fn write_tbl_row(self, text_data: &mut Vec<u8>) {
+                write!(text_data, "{}", self).unwrap();
+            }
+            fn write_csv_header(text_data: &mut Vec<u8>) {
+                writeln!(text_data, "{}", <$csv_type>::header()).unwrap();
+            }
+            fn write_csv_row(self, text_data: &mut Vec<u8>) {
+                writeln!(text_data, "{}", <$csv_type>::new(self)).unwrap();
+            }
+        }
+    };
 }
 
+impl_row_type!(Customer<'_>, CustomerCsv);
+impl_row_type!(LineItem<'_>, LineItemCsv);
+impl_row_type!(Nation<'_>, NationCsv);
+impl_row_type!(Order<'_>, OrderCsv);
+impl_row_type!(Part<'_>, PartCsv);
+impl_row_type!(PartSupp<'_>, PartSuppCsv);
+impl_row_type!(Region<'_>, RegionCsv);
+impl_row_type!(Supplier, SupplierCsv);
+
 #[derive(Debug, Clone, Copy)]
 #[allow(clippy::upper_case_acronyms)]
-enum TestFormat {
+enum Test {
     /// Generate and parse data as TBL format ('|' delimited)
     TBL,
     /// Generate and parse data as CSV format
     CSV,
 }
 
-impl TestFormat {
-    /// Write the header for the LineItem format into the provided buffer
-    fn write_lineitem_header(&self, text_data: &mut Vec<u8>) {
+impl Test {
+    /// Create a test for TBL format
+    fn tbl() -> Self {
+        Self::TBL
+    }
+
+    /// Create a test for CSV format
+    fn csv() -> Self {
+        Self::CSV
+    }
+
+    /// Generates data using the row iterator and the arrow iterator using the
+    /// specified format and compares the results of parsing with the Arrow CSV
+    /// parser with the directly generated the batches
+    fn test<R, RI, RBI>(&self, mut row_iter: RI, mut arrow_iter: RBI)
+    where
+        R: RowType,
+        RI: Iterator<Item = R>,
+        RBI: RecordBatchIterator,
+    {
+        // For each batch generated by the arrow iterator
+        while let Some(arrow_batch) = arrow_iter.next() {
+            // Generate the same number of rows using the row iterator
+            // in the specified format
+            let batch_size = arrow_batch.num_rows();
+            let mut text_data = Vec::new();
+            self.write_header::<R>(&mut text_data);
+            row_iter.by_ref().take(batch_size).for_each(|item| {
+                self.write_row(item, &mut text_data);
+            });
+            // reparse the generated data and compare with the arrow batch
+            let reparsed_batch = self.parse(&text_data, arrow_iter.schema(), batch_size);
+            assert_eq!(reparsed_batch, arrow_batch);
+        }
+    }
+
+    /// Write the header for the row type
+    fn write_header<R: RowType>(&self, text_data: &mut Vec<u8>) {
         match self {
-            TestFormat::TBL => {}
-            TestFormat::CSV => {
-                writeln!(text_data, "{}\n", LineItemCsv::header()).unwrap();
+            Test::TBL => {}
+            Test::CSV => {
+                R::write_csv_header(text_data);
             }
         }
     }
 
-    /// Write a LineItem into the provided buffer
-    fn write_lineitem(&self, line_item: LineItem<'_>, text_data: &mut Vec<u8>) {
+    /// Write a row into the provided buffer
+    fn write_row<R: RowType>(&self, row: R, text_data: &mut Vec<u8>) {
         match self {
-            TestFormat::TBL => {
-                write!(text_data, "{}", line_item).unwrap();
+            Test::TBL => {
+                row.write_tbl_row(text_data);
                 // Note: TBL lines end with '|' which the arrow csv parser treats as a
                 // delimiter for a new column so replace the last '|' with a newline
                 let end_offset = text_data.len() - 1;
                 text_data[end_offset] = b'\n';
             }
-            TestFormat::CSV => {
-                writeln!(text_data, "{}", LineItemCsv::new(line_item)).unwrap();
+            Test::CSV => {
+                row.write_csv_row(text_data);
             }
         }
     }
@@ -85,8 +166,8 @@ impl TestFormat {
             arrow_csv::reader::ReaderBuilder::new(Arc::clone(schema)).with_batch_size(batch_size);
 
         let builder = match self {
-            TestFormat::TBL => builder.with_header(false).with_delimiter(b'|'),
-            TestFormat::CSV => builder.with_header(true),
+            Test::TBL => builder.with_header(false).with_delimiter(b'|'),
+            Test::CSV => builder.with_header(true),
         };
 
         let mut parser = builder.build(data).unwrap();
diff --git a/tpchgen-cli/src/csv.rs b/tpchgen-cli/src/csv.rs
index d1ad566..81d3074 100644
--- a/tpchgen-cli/src/csv.rs
+++ b/tpchgen-cli/src/csv.rs
@@ -6,7 +6,7 @@ use tpchgen::csv::{
 };
 use tpchgen::generators::{
     CustomerGenerator, LineItemGenerator, NationGenerator, OrderGenerator, PartGenerator,
-    PartSupplierGenerator, RegionGenerator, SupplierGenerator,
+    PartSuppGenerator, RegionGenerator, SupplierGenerator,
 };
 
 /// Define a Source that writes the table in CSV format
@@ -41,11 +41,7 @@ define_csv_source!(NationCsvSource, NationGenerator<'static>, NationCsv);
 define_csv_source!(RegionCsvSource, RegionGenerator<'static>, RegionCsv);
 define_csv_source!(PartCsvSource, PartGenerator<'static>, PartCsv);
 define_csv_source!(SupplierCsvSource, SupplierGenerator<'static>, SupplierCsv);
-define_csv_source!(
-    PartSuppCsvSource,
-    PartSupplierGenerator<'static>,
-    PartSuppCsv
-);
+define_csv_source!(PartSuppCsvSource, PartSuppGenerator<'static>, PartSuppCsv);
 define_csv_source!(CustomerCsvSource, CustomerGenerator<'static>, CustomerCsv);
 define_csv_source!(OrderCsvSource, OrderGenerator<'static>, OrderCsv);
 define_csv_source!(LineItemCsvSource, LineItemGenerator<'static>, LineItemCsv);
diff --git a/tpchgen-cli/src/main.rs b/tpchgen-cli/src/main.rs
index d116845..c0032ff 100644
--- a/tpchgen-cli/src/main.rs
+++ b/tpchgen-cli/src/main.rs
@@ -45,7 +45,7 @@ use std::time::Instant;
 use tpchgen::distribution::Distributions;
 use tpchgen::generators::{
     CustomerGenerator, LineItemGenerator, NationGenerator, OrderGenerator, PartGenerator,
-    PartSupplierGenerator, RegionGenerator, SupplierGenerator,
+    PartSuppGenerator, RegionGenerator, SupplierGenerator,
 };
 use tpchgen::text::TextPool;
 
@@ -246,7 +246,7 @@ impl Cli {
     define_generate!(
         generate_partsupp,
         Table::PartSupp,
-        PartSupplierGenerator,
+        PartSuppGenerator,
         PartSuppTblSource,
         PartSuppCsvSource
     );
@@ -318,7 +318,7 @@ impl Cli {
             ),
             Table::PartSupp => (
                 148,
-                PartSupplierGenerator::calculate_row_count(self.scale_factor, 1, 1),
+                PartSuppGenerator::calculate_row_count(self.scale_factor, 1, 1),
             ),
             Table::Customer => (
                 160,
diff --git a/tpchgen-cli/src/tbl.rs b/tpchgen-cli/src/tbl.rs
index 701f8fd..2d0683f 100644
--- a/tpchgen-cli/src/tbl.rs
+++ b/tpchgen-cli/src/tbl.rs
@@ -4,7 +4,7 @@ use super::generate::Source;
 use std::io::Write;
 use tpchgen::generators::{
     CustomerGenerator, LineItemGenerator, NationGenerator, OrderGenerator, PartGenerator,
-    PartSupplierGenerator, RegionGenerator, SupplierGenerator,
+    PartSuppGenerator, RegionGenerator, SupplierGenerator,
 };
 
 /// Define a Source that writes the table in TBL format
@@ -37,7 +37,7 @@ define_tbl_source!(NationTblSource, NationGenerator<'static>);
 define_tbl_source!(RegionTblSource, RegionGenerator<'static>);
 define_tbl_source!(PartTblSource, PartGenerator<'static>);
 define_tbl_source!(SupplierTblSource, SupplierGenerator<'static>);
-define_tbl_source!(PartSuppTblSource, PartSupplierGenerator<'static>);
+define_tbl_source!(PartSuppTblSource, PartSuppGenerator<'static>);
 define_tbl_source!(CustomerTblSource, CustomerGenerator<'static>);
 define_tbl_source!(OrderTblSource, OrderGenerator<'static>);
 define_tbl_source!(LineItemTblSource, LineItemGenerator<'static>);
diff --git a/tpchgen/src/csv.rs b/tpchgen/src/csv.rs
index a3cd2cb..2868715 100644
--- a/tpchgen/src/csv.rs
+++ b/tpchgen/src/csv.rs
@@ -274,11 +274,11 @@ impl Display for CustomerCsv<'_> {
 ///
 /// # Example
 /// ```
-/// # use tpchgen::generators::PartSupplierGenerator;
+/// # use tpchgen::generators::PartSuppGenerator;
 /// # use tpchgen::csv::PartSuppCsv;
 /// # use std::fmt::Write;
 /// // Output the first 3 rows in CSV format
-/// let generator = PartSupplierGenerator::new(1.0, 1, 1);
+/// let generator = PartSuppGenerator::new(1.0, 1, 1);
 /// let mut csv = String::new();
 /// writeln!(&mut csv, "{}", PartSuppCsv::header()).unwrap(); // write header
 /// for line in generator.iter().take(3) {
diff --git a/tpchgen/src/generators.rs b/tpchgen/src/generators.rs
index 6ed426a..6a42c88 100644
--- a/tpchgen/src/generators.rs
+++ b/tpchgen/src/generators.rs
@@ -1,6 +1,4 @@
 //! Generators for each TPC-H Tables
-use core::fmt;
-
 use crate::dates;
 use crate::decimal::TPCHDecimal;
 use crate::distribution::Distribution;
@@ -10,12 +8,14 @@ use crate::random::RowRandomInt;
 use crate::random::{PhoneNumberInstance, RandomBoundedLong, StringSequenceInstance};
 use crate::random::{RandomAlphaNumeric, RandomAlphaNumericInstance};
 use crate::text::TextPool;
+use core::fmt;
+use std::fmt::Display;
 
 use crate::dates::{GenerateUtils, TPCHDate};
 use crate::random::{RandomBoundedInt, RandomString, RandomStringSequence, RandomText};
 
 /// Generator for Nation table data
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct NationGenerator<'a> {
     distributions: &'a Distributions,
     text_pool: &'a TextPool,
@@ -206,7 +206,7 @@ impl<'a> Region<'a> {
 }
 
 /// Generator for Region table data
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct RegionGenerator<'a> {
     distributions: &'a Distributions,
     text_pool: &'a TextPool,
@@ -390,7 +390,7 @@ impl fmt::Display for Part<'_> {
 }
 
 /// Generator for Part table data
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct PartGenerator<'a> {
     scale_factor: f64,
     part: i32,
@@ -660,7 +660,7 @@ impl fmt::Display for Supplier {
 }
 
 /// Generator for Supplier table data
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct SupplierGenerator<'a> {
     scale_factor: f64,
     part: i32,
@@ -971,7 +971,7 @@ impl fmt::Display for Customer<'_> {
 }
 
 /// Generator for Customer table data
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct CustomerGenerator<'a> {
     scale_factor: f64,
     part: i32,
@@ -1186,16 +1186,16 @@ impl fmt::Display for PartSupp<'_> {
     }
 }
 
-/// Generator for PartSupplier table data
-#[derive(Debug)]
-pub struct PartSupplierGenerator<'a> {
+/// Generator for PartSupp table data
+#[derive(Debug, Clone)]
+pub struct PartSuppGenerator<'a> {
     scale_factor: f64,
     part: i32,
     part_count: i32,
     text_pool: &'a TextPool,
 }
 
-impl<'a> PartSupplierGenerator<'a> {
+impl<'a> PartSuppGenerator<'a> {
     /// Base scale for part-supplier generation
     const SUPPLIERS_PER_PART: i32 = 4;
 
@@ -1206,11 +1206,11 @@ impl<'a> PartSupplierGenerator<'a> {
     const SUPPLY_COST_MAX: i32 = 100000;
     const COMMENT_AVERAGE_LENGTH: i32 = 124;
 
-    /// Creates a new PartSupplierGenerator with the given scale factor
+    /// Creates a new PartSuppGenerator with the given scale factor
     ///
     /// Note the generator's lifetime is `&'static`. See [`NationGenerator`] for
     /// more details.
-    pub fn new(scale_factor: f64, part: i32, part_count: i32) -> PartSupplierGenerator<'static> {
+    pub fn new(scale_factor: f64, part: i32, part_count: i32) -> PartSuppGenerator<'static> {
         // Note: use explicit lifetime to ensure this remains `&'static`
         Self::new_with_text_pool(
             scale_factor,
@@ -1220,14 +1220,14 @@ impl<'a> PartSupplierGenerator<'a> {
         )
     }
 
-    /// Creates a PartSupplierGenerator with specified text pool
+    /// Creates a PartSuppGenerator with specified text pool
     pub fn new_with_text_pool(
         scale_factor: f64,
         part: i32,
         part_count: i32,
         text_pool: &TextPool,
-    ) -> PartSupplierGenerator<'_> {
-        PartSupplierGenerator {
+    ) -> PartSuppGenerator<'_> {
+        PartSuppGenerator {
             scale_factor,
             part,
             part_count,
@@ -1247,10 +1247,10 @@ impl<'a> PartSupplierGenerator<'a> {
     }
 
     /// Returns an iterator over the part supplier rows
-    pub fn iter(&self) -> PartSupplierGeneratorIterator<'a> {
+    pub fn iter(&self) -> PartSuppGeneratorIterator<'a> {
         let scale_base = PartGenerator::SCALE_BASE;
 
-        PartSupplierGeneratorIterator::new(
+        PartSuppGeneratorIterator::new(
             self.text_pool,
             self.scale_factor,
             GenerateUtils::calculate_start_index(
@@ -1264,18 +1264,18 @@ impl<'a> PartSupplierGenerator<'a> {
     }
 }
 
-impl<'a> IntoIterator for &'a PartSupplierGenerator<'a> {
+impl<'a> IntoIterator for &'a PartSuppGenerator<'a> {
     type Item = PartSupp<'a>;
-    type IntoIter = PartSupplierGeneratorIterator<'a>;
+    type IntoIter = PartSuppGeneratorIterator<'a>;
 
     fn into_iter(self) -> Self::IntoIter {
         self.iter()
     }
 }
 
-/// Iterator that generates PartSupplier rows
+/// Iterator that generates PartSupp rows
 #[derive(Debug)]
-pub struct PartSupplierGeneratorIterator<'a> {
+pub struct PartSuppGeneratorIterator<'a> {
     scale_factor: f64,
     start_index: i64,
     row_count: i64,
@@ -1288,25 +1288,25 @@ pub struct PartSupplierGeneratorIterator<'a> {
     part_supplier_number: i32,
 }
 
-impl<'a> PartSupplierGeneratorIterator<'a> {
+impl<'a> PartSuppGeneratorIterator<'a> {
     fn new(text_pool: &'a TextPool, scale_factor: f64, start_index: i64, row_count: i64) -> Self {
         let mut available_quantity_random = RandomBoundedInt::new_with_seeds_per_row(
             1671059989,
-            PartSupplierGenerator::AVAILABLE_QUANTITY_MIN,
-            PartSupplierGenerator::AVAILABLE_QUANTITY_MAX,
-            PartSupplierGenerator::SUPPLIERS_PER_PART,
+            PartSuppGenerator::AVAILABLE_QUANTITY_MIN,
+            PartSuppGenerator::AVAILABLE_QUANTITY_MAX,
+            PartSuppGenerator::SUPPLIERS_PER_PART,
         );
         let mut supply_cost_random = RandomBoundedInt::new_with_seeds_per_row(
             1051288424,
-            PartSupplierGenerator::SUPPLY_COST_MIN,
-            PartSupplierGenerator::SUPPLY_COST_MAX,
-            PartSupplierGenerator::SUPPLIERS_PER_PART,
+            PartSuppGenerator::SUPPLY_COST_MIN,
+            PartSuppGenerator::SUPPLY_COST_MAX,
+            PartSuppGenerator::SUPPLIERS_PER_PART,
         );
         let mut comment_random = RandomText::new_with_expected_row_count(
             1961692154,
             text_pool,
-            PartSupplierGenerator::COMMENT_AVERAGE_LENGTH as f64,
-            PartSupplierGenerator::SUPPLIERS_PER_PART,
+            PartSuppGenerator::COMMENT_AVERAGE_LENGTH as f64,
+            PartSuppGenerator::SUPPLIERS_PER_PART,
         );
 
         // Advance all generators to the starting position
@@ -1314,7 +1314,7 @@ impl<'a> PartSupplierGeneratorIterator<'a> {
         supply_cost_random.advance_rows(start_index);
         comment_random.advance_rows(start_index);
 
-        PartSupplierGeneratorIterator {
+        PartSuppGeneratorIterator {
             scale_factor,
             start_index,
             row_count,
@@ -1354,14 +1354,14 @@ impl<'a> PartSupplierGeneratorIterator<'a> {
 
         ((part_key
             + (supplier_number
-                * ((supplier_count / PartSupplierGenerator::SUPPLIERS_PER_PART as i64)
+                * ((supplier_count / PartSuppGenerator::SUPPLIERS_PER_PART as i64)
                     + ((part_key - 1) / supplier_count))))
             % supplier_count)
             + 1
     }
 }
 
-impl<'a> Iterator for PartSupplierGeneratorIterator<'a> {
+impl<'a> Iterator for PartSuppGeneratorIterator<'a> {
     type Item = PartSupp<'a>;
 
     fn next(&mut self) -> Option<Self::Item> {
@@ -1374,7 +1374,7 @@ impl<'a> Iterator for PartSupplierGeneratorIterator<'a> {
         self.part_supplier_number += 1;
 
         // advance next row only when all suppliers for the part have been produced
-        if self.part_supplier_number >= PartSupplierGenerator::SUPPLIERS_PER_PART {
+        if self.part_supplier_number >= PartSuppGenerator::SUPPLIERS_PER_PART {
             self.available_quantity_random.row_finished();
             self.supply_cost_random.row_finished();
             self.comment_random.row_finished();
@@ -1404,6 +1404,27 @@ impl fmt::Display for ClerkName {
     }
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
+/// Order status (F=final, O=open, P=pending)
+pub enum OrderStatus {
+    /// Fulfilled - all line items shipped
+    Fulfilled,
+    /// Open - no line items shipped
+    Open,
+    /// Partially fulfilled - some line items shipped
+    Pending,
+}
+
+impl Display for OrderStatus {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            OrderStatus::Fulfilled => write!(f, "F"),
+            OrderStatus::Open => write!(f, "O"),
+            OrderStatus::Pending => write!(f, "P"),
+        }
+    }
+}
+
 /// The ORDERS table
 ///
 /// The Display trait is implemented to format the line item data as a string
@@ -1420,7 +1441,7 @@ pub struct Order<'a> {
     /// Foreign key to CUSTOMER
     pub o_custkey: i64,
     /// Order status (F=final, O=open, P=pending)
-    pub o_orderstatus: char,
+    pub o_orderstatus: OrderStatus,
     /// Order total price
     pub o_totalprice: TPCHDecimal,
     /// Order date
@@ -1454,7 +1475,7 @@ impl fmt::Display for Order<'_> {
 }
 
 /// Generator for Order table data
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub struct OrderGenerator<'a> {
     scale_factor: f64,
     part: i32,
@@ -1697,11 +1718,11 @@ impl<'a> OrderGeneratorIterator<'a> {
         }
 
         let order_status = if shipped_count == line_count {
-            'F' // Fulfilled - all line items shipped
+            OrderStatus::Fulfilled
         } else if shipped_count > 0 {
-            'P' // Partially fulfilled - some line items shipped
+            OrderStatus::Pending
         } else {
-            'O' // Open - no line items shipped
+            OrderStatus::Open
         };
 
         let clerk_id = self.clerk_random.next_value();
@@ -2124,7 +2145,7 @@ impl<'a> LineItemGeneratorIterator<'a> {
         let part_key = self.line_part_key_random.next_value();
 
         let supplier_number = self.supplier_number_random.next_value() as i64;
-        let supplier_key = PartSupplierGeneratorIterator::select_part_supplier(
+        let supplier_key = PartSuppGeneratorIterator::select_part_supplier(
             part_key,
             supplier_number,
             self.scale_factor,
@@ -2310,7 +2331,7 @@ mod tests {
     #[test]
     fn test_part_supplier_generation() {
         // Create a generator with a small scale factor
-        let generator = PartSupplierGenerator::new(0.01, 1, 1);
+        let generator = PartSuppGenerator::new(0.01, 1, 1);
         let part_suppliers: Vec<_> = generator.iter().collect();
 
         // Should have 0.01 * 200,000 * 4 = 8,000 part-supplier relationships
@@ -2339,7 +2360,7 @@ mod tests {
 
         assert_eq!(
             suppliers_for_first_part.len(),
-            PartSupplierGenerator::SUPPLIERS_PER_PART as usize
+            PartSuppGenerator::SUPPLIERS_PER_PART as usize
         );
 
         // Supplier keys should be unique for each part
@@ -2347,7 +2368,7 @@ mod tests {
             suppliers_for_first_part.iter().collect();
         assert_eq!(
             unique_suppliers.len(),
-            PartSupplierGenerator::SUPPLIERS_PER_PART as usize
+            PartSuppGenerator::SUPPLIERS_PER_PART as usize
         );
     }
 
@@ -2357,17 +2378,17 @@ mod tests {
         let scale_factor = 1.0;
 
         // Same part with different supplier numbers should yield different suppliers
-        let supplier1 = PartSupplierGeneratorIterator::select_part_supplier(1, 0, scale_factor);
-        let supplier2 = PartSupplierGeneratorIterator::select_part_supplier(1, 1, scale_factor);
-        let supplier3 = PartSupplierGeneratorIterator::select_part_supplier(1, 2, scale_factor);
-        let supplier4 = PartSupplierGeneratorIterator::select_part_supplier(1, 3, scale_factor);
+        let supplier1 = PartSuppGeneratorIterator::select_part_supplier(1, 0, scale_factor);
+        let supplier2 = PartSuppGeneratorIterator::select_part_supplier(1, 1, scale_factor);
+        let supplier3 = PartSuppGeneratorIterator::select_part_supplier(1, 2, scale_factor);
+        let supplier4 = PartSuppGeneratorIterator::select_part_supplier(1, 3, scale_factor);
 
         // All suppliers should be different
         let suppliers = vec![supplier1, supplier2, supplier3, supplier4];
         let unique_suppliers: std::collections::HashSet<_> = suppliers.iter().collect();
         assert_eq!(
             unique_suppliers.len(),
-            PartSupplierGenerator::SUPPLIERS_PER_PART as usize
+            PartSuppGenerator::SUPPLIERS_PER_PART as usize
         );
 
         // All supplier keys should be within valid range (1 to supplier_count)
@@ -2390,9 +2411,6 @@ mod tests {
         let first = &orders[0];
         assert_eq!(first.o_orderkey, OrderGenerator::make_order_key(1));
         assert!(first.o_custkey > 0);
-        assert!(
-            first.o_orderstatus == 'F' || first.o_orderstatus == 'P' || first.o_orderstatus == 'O'
-        );
         assert!(first.o_totalprice > TPCHDecimal::ZERO);
 
         // Check order status distribution
@@ -2494,8 +2512,7 @@ mod tests {
         let _iter: PartGeneratorIterator<'static> = PartGenerator::new(0.1, 1, 1).iter();
         let _iter: SupplierGeneratorIterator<'static> = SupplierGenerator::new(0.1, 1, 1).iter();
         let _iter: CustomerGeneratorIterator<'static> = CustomerGenerator::new(0.1, 1, 1).iter();
-        let _iter: PartSupplierGeneratorIterator<'static> =
-            PartSupplierGenerator::new(0.1, 1, 1).iter();
+        let _iter: PartSuppGeneratorIterator<'static> = PartSuppGenerator::new(0.1, 1, 1).iter();
         let _iter: OrderGeneratorIterator<'static> = OrderGenerator::new(0.1, 1, 1).iter();
         let _iter: LineItemGeneratorIterator<'static> = LineItemGenerator::new(0.1, 1, 1).iter();
     }
diff --git a/tpchgen/src/random.rs b/tpchgen/src/random.rs
index f4e6e7f..12222c6 100644
--- a/tpchgen/src/random.rs
+++ b/tpchgen/src/random.rs
@@ -440,7 +440,7 @@ impl RandomPhoneNumber {
 /// ```text
 /// 27-918-335-1736
 /// ```
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
 pub struct PhoneNumberInstance {
     country_code: i32,
     local1: i32,
diff --git a/tpchgen/tests/integration_tests.rs b/tpchgen/tests/integration_tests.rs
index f5c53fe..a7036f4 100644
--- a/tpchgen/tests/integration_tests.rs
+++ b/tpchgen/tests/integration_tests.rs
@@ -6,7 +6,7 @@ use std::io::{BufRead, BufReader};
 use std::path::{Path, PathBuf};
 use tpchgen::generators::{
     CustomerGenerator, LineItemGenerator, NationGenerator, OrderGenerator, PartGenerator,
-    PartSupplierGenerator, RegionGenerator, SupplierGenerator,
+    PartSuppGenerator, RegionGenerator, SupplierGenerator,
 };
 
 fn read_tbl_gz<P: AsRef<Path>>(path: P) -> Vec<String> {
@@ -94,7 +94,7 @@ fn test_supplier_sf_0_001() {
 #[test]
 fn test_partsupp_sf_0_001() {
     let sf = 0.001;
-    let generator = PartSupplierGenerator::new(sf, 1, 1);
+    let generator = PartSuppGenerator::new(sf, 1, 1);
     test_generator(generator.iter(), "data/sf-0.001/partsupp.tbl.gz", |ps| {
         ps.to_string()
     });
@@ -170,7 +170,7 @@ fn test_supplier_sf_0_01() {
 #[test]
 fn test_partsupp_sf_0_01() {
     let sf = 0.01;
-    let generator = PartSupplierGenerator::new(sf, 1, 1);
+    let generator = PartSuppGenerator::new(sf, 1, 1);
     test_generator(generator.iter(), "data/sf-0.01/partsupp.tbl.gz", |ps| {
         ps.to_string()
     });


Reviews:
user: login: alamb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T12:23:51Z
user: login: alamb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T15:18:24Z
user: login: clflushopt
body: lgtm, my comments are non-blocking so feel free to merge ! this seems to have been cumbersone 😅 thanks for taking the initiative !!
state: APPROVED
author_association: OWNER
submitted_at: 2025-03-27T23:25:55Z
user: login: alamb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T00:20:13Z
user: login: alamb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T00:20:48Z
user: login: alamb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T00:21:36Z
user: login: alamb
body: Thank you so much for the review @clflushopt 
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T00:22:01Z
user: login: alamb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T00:25:25Z
user: login: alamb
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-28T00:25:35Z

Review comments:
diff_hunk: @@ -1186,16 +1186,16 @@ impl fmt::Display for PartSupp<'_> {
     }
 }
 
-/// Generator for PartSupplier table data
-#[derive(Debug)]
-pub struct PartSupplierGenerator<'a> {
+/// Generator for PartSupp table data
+#[derive(Debug, Clone)]
+pub struct PartSuppGenerator<'a> {
path: tpchgen/src/generators.rs
user: login: alamb
body: I also renamed PartSupplier --> PartSupp to be consistent with TPCH terminology where it is always referred to as `partsupp`
created_at: 2025-03-27T12:23:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,121 @@
+use crate::conversions::{decimal128_array_from_iter, string_view_array_from_display_iter};
path: tpchgen-arrow/src/customer.rs
user: login: alamb
body: Basically I just applied the same pattern for all the remaining TPCH tables
created_at: 2025-03-27T15:17:13Z
author_association: COLLABORATOR
diff_hunk: @@ -1404,6 +1404,27 @@ impl fmt::Display for ClerkName {
     }
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
path: tpchgen/src/generators.rs
user: login: alamb
body: I also changed this from `char` to an enum so I could more easily convert it to StringViewArray 

I actually think I can apply the same idea for some of the status codes in LineItem and make it faster too. As a follow on PR
created_at: 2025-03-27T15:18:22Z
author_association: COLLABORATOR
diff_hunk: @@ -1,5 +1,7 @@
 //! Routines to convert TPCH types to Arrow types
 
+use arrow::array::{StringViewArray, StringViewBuilder};
+use std::fmt::Write;
path: tpchgen-arrow/src/conversions.rs
user: login: clflushopt
body: suggestion: this is mostly a personal preference so feel free to omit it but I would like it if we can keep imports sorted and grouped similar to how Go's default `fmt` does it (first block is `std` library, followed by local/internal crates and finally third party dependencies). Not important though mostly a personal taste/preference thing !
created_at: 2025-03-27T23:21:23Z
author_association: OWNER
diff_hunk: @@ -17,6 +19,36 @@ pub fn to_arrow_date32(value: TPCHDate) -> i32 {
     value.into_inner() + TPCHDATE_TO_DATE32_OFFSET
 }
 
+/// Converts an iterator of TPCH decimals to an Arrow Decimal128Array
+pub fn decimal128_array_from_iter<I>(values: I) -> arrow::array::Decimal128Array
+where
+    I: Iterator<Item = TPCHDecimal>,
+{
+    let values = values.map(to_arrow_decimal);
+    arrow::array::Decimal128Array::from_iter_values(values)
+        .with_precision_and_scale(15, 2)
+        .unwrap()
path: tpchgen-arrow/src/conversions.rs
user: login: clflushopt
body: question: can the `unwrap` fail in this case ? I don't see how since we control the values and we know "hand-wavey" that the input space fits in the decimal type, but I am asking because if the function is exposed do we need to communicate some kind of "failure modes" to the users ?
created_at: 2025-03-27T23:23:33Z
author_association: OWNER
diff_hunk: @@ -1404,6 +1404,27 @@ impl fmt::Display for ClerkName {
     }
 }
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)]
path: tpchgen/src/generators.rs
user: login: alamb
body: Follow up the idea is here: https://github.com/clflushopt/tpchgen-rs/pull/75 (but still not complete)
created_at: 2025-03-28T00:20:13Z
author_association: COLLABORATOR
diff_hunk: @@ -1,5 +1,7 @@
 //! Routines to convert TPCH types to Arrow types
 
+use arrow::array::{StringViewArray, StringViewBuilder};
+use std::fmt::Write;
path: tpchgen-arrow/src/conversions.rs
user: login: alamb
body: Make sense -- will fix. I think my editor (RustRover does  this and I don't really pay attention to the imports)
created_at: 2025-03-28T00:20:48Z
author_association: COLLABORATOR
diff_hunk: @@ -17,6 +19,36 @@ pub fn to_arrow_date32(value: TPCHDate) -> i32 {
     value.into_inner() + TPCHDATE_TO_DATE32_OFFSET
 }
 
+/// Converts an iterator of TPCH decimals to an Arrow Decimal128Array
+pub fn decimal128_array_from_iter<I>(values: I) -> arrow::array::Decimal128Array
+where
+    I: Iterator<Item = TPCHDecimal>,
+{
+    let values = values.map(to_arrow_decimal);
+    arrow::array::Decimal128Array::from_iter_values(values)
+        .with_precision_and_scale(15, 2)
+        .unwrap()
path: tpchgen-arrow/src/conversions.rs
user: login: alamb
body: In this case it can not fail -- the only way that `with_precision_and_scale` can fail is if the precison / scale are outside of the allowable range of arrow decimals (38 and 20 I believe). I will clarify this in comments
created_at: 2025-03-28T00:21:36Z
author_association: COLLABORATOR
diff_hunk: @@ -1,5 +1,7 @@
 //! Routines to convert TPCH types to Arrow types
 
+use arrow::array::{StringViewArray, StringViewBuilder};
+use std::fmt::Write;
path: tpchgen-arrow/src/conversions.rs
user: login: alamb
body: done in 9a12ac03528e97b956d24c5be063fc0692f0dcd8
created_at: 2025-03-28T00:25:25Z
author_association: COLLABORATOR
diff_hunk: @@ -17,6 +19,36 @@ pub fn to_arrow_date32(value: TPCHDate) -> i32 {
     value.into_inner() + TPCHDATE_TO_DATE32_OFFSET
 }
 
+/// Converts an iterator of TPCH decimals to an Arrow Decimal128Array
+pub fn decimal128_array_from_iter<I>(values: I) -> arrow::array::Decimal128Array
+where
+    I: Iterator<Item = TPCHDecimal>,
+{
+    let values = values.map(to_arrow_decimal);
+    arrow::array::Decimal128Array::from_iter_values(values)
+        .with_precision_and_scale(15, 2)
+        .unwrap()
path: tpchgen-arrow/src/conversions.rs
user: login: alamb
body: in 9a12ac03528e97b956d24c5be063fc0692f0dcd8
created_at: 2025-03-28T00:25:35Z
author_association: COLLABORATOR

PR comments:
user: login: alamb
created_at: 2025-03-27T15:47:29Z
body: @clflushopt  I think this is the last PR before I can add real parquet support
author_association: COLLABORATOR
user: login: alamb
created_at: 2025-03-28T00:31:29Z
body: Here we go! On to parquet generation directly
author_association: COLLABORATOR