You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by al...@apache.org on 2023/06/06 10:21:11 UTC

[arrow-rs] branch master updated: feat(flight): add xdbc type info helpers (#4359)

This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/master by this push:
     new d48391d20 feat(flight): add xdbc type info helpers (#4359)
d48391d20 is described below

commit d48391d208b7cb6c0ae7f2511c69cdb2b40bb76d
Author: Robert Pack <42...@users.noreply.github.com>
AuthorDate: Tue Jun 6 12:21:04 2023 +0200

    feat(flight): add xdbc type info helpers (#4359)
    
    * feat(flight): add xdbc type info helpers
    
    * test: add test for creating record batch
    
    * test: filter record batch
    
    * docs: add some basic docstrings and examples
    
    * fix: xdbc info example imports
    
    * fix: actually fix example
    
    * docs: fix link
    
    * fix: rename structs and add GetXdbcTypeInfoBuilder
    
    * fix: clippy
    
    * Update arrow-flight/src/sql/metadata/xdbc_info.rs
    
    Co-authored-by: Andrew Lamb <an...@nerdnetworks.org>
    
    * fix: pr feedback
    
    * fix: docs
    
    * fix: missed one name
    
    * fix: example
    
    ---------
    
    Co-authored-by: Andrew Lamb <an...@nerdnetworks.org>
---
 arrow-flight/examples/flight_sql_server.rs |  66 ++++-
 arrow-flight/src/sql/metadata/mod.rs       |  17 ++
 arrow-flight/src/sql/metadata/sql_info.rs  |  13 +-
 arrow-flight/src/sql/metadata/xdbc_info.rs | 433 +++++++++++++++++++++++++++++
 4 files changed, 505 insertions(+), 24 deletions(-)

diff --git a/arrow-flight/examples/flight_sql_server.rs b/arrow-flight/examples/flight_sql_server.rs
index ecd8db76b..e9dba08f0 100644
--- a/arrow-flight/examples/flight_sql_server.rs
+++ b/arrow-flight/examples/flight_sql_server.rs
@@ -30,7 +30,9 @@ use tonic::{Request, Response, Status, Streaming};
 use arrow_array::builder::StringBuilder;
 use arrow_array::{ArrayRef, RecordBatch};
 use arrow_flight::encode::FlightDataEncoderBuilder;
-use arrow_flight::sql::metadata::SqlInfoList;
+use arrow_flight::sql::metadata::{
+    SqlInfoList, XdbcTypeInfo, XdbcTypeInfoData, XdbcTypeInfoDataBuilder,
+};
 use arrow_flight::sql::{
     server::FlightSqlService, ActionBeginSavepointRequest, ActionBeginSavepointResult,
     ActionBeginTransactionRequest, ActionBeginTransactionResult,
@@ -42,8 +44,8 @@ use arrow_flight::sql::{
     CommandGetImportedKeys, CommandGetPrimaryKeys, CommandGetSqlInfo,
     CommandGetTableTypes, CommandGetTables, CommandGetXdbcTypeInfo,
     CommandPreparedStatementQuery, CommandPreparedStatementUpdate, CommandStatementQuery,
-    CommandStatementSubstraitPlan, CommandStatementUpdate, ProstMessageExt, SqlInfo,
-    TicketStatementQuery,
+    CommandStatementSubstraitPlan, CommandStatementUpdate, Nullable, ProstMessageExt,
+    Searchable, SqlInfo, TicketStatementQuery, XdbcDataType,
 };
 use arrow_flight::utils::batches_to_flight_data;
 use arrow_flight::{
@@ -73,6 +75,32 @@ static INSTANCE_SQL_INFO: Lazy<SqlInfoList> = Lazy::new(|| {
         .with_sql_info(SqlInfo::FlightSqlServerArrowVersion, "1.3")
 });
 
+static INSTANCE_XBDC_DATA: Lazy<XdbcTypeInfoData> = Lazy::new(|| {
+    let mut builder = XdbcTypeInfoDataBuilder::new();
+    builder.append(XdbcTypeInfo {
+        type_name: "INTEGER".into(),
+        data_type: XdbcDataType::XdbcInteger,
+        column_size: Some(32),
+        literal_prefix: None,
+        literal_suffix: None,
+        create_params: None,
+        nullable: Nullable::NullabilityNullable,
+        case_sensitive: false,
+        searchable: Searchable::Full,
+        unsigned_attribute: Some(false),
+        fixed_prec_scale: false,
+        auto_increment: Some(false),
+        local_type_name: Some("INTEGER".into()),
+        minimum_scale: None,
+        maximum_scale: None,
+        sql_data_type: XdbcDataType::XdbcInteger,
+        datetime_subcode: None,
+        num_prec_radix: Some(2),
+        interval_precision: None,
+    });
+    builder.build().unwrap()
+});
+
 static TABLES: Lazy<Vec<&'static str>> = Lazy::new(|| vec!["flight_sql.example.table"]);
 
 #[derive(Clone)]
@@ -367,12 +395,20 @@ impl FlightSqlService for FlightSqlServiceImpl {
 
     async fn get_flight_info_xdbc_type_info(
         &self,
-        _query: CommandGetXdbcTypeInfo,
-        _request: Request<FlightDescriptor>,
+        query: CommandGetXdbcTypeInfo,
+        request: Request<FlightDescriptor>,
     ) -> Result<Response<FlightInfo>, Status> {
-        Err(Status::unimplemented(
-            "get_flight_info_xdbc_type_info not implemented",
-        ))
+        let flight_descriptor = request.into_inner();
+        let ticket = Ticket::new(query.encode_to_vec());
+        let endpoint = FlightEndpoint::new().with_ticket(ticket);
+
+        let flight_info = FlightInfo::new()
+            .try_with_schema(query.into_builder(&INSTANCE_XBDC_DATA).schema().as_ref())
+            .map_err(|e| status!("Unable to encode schema", e))?
+            .with_endpoint(endpoint)
+            .with_descriptor(flight_descriptor);
+
+        Ok(tonic::Response::new(flight_info))
     }
 
     // do_get
@@ -544,12 +580,18 @@ impl FlightSqlService for FlightSqlServiceImpl {
 
     async fn do_get_xdbc_type_info(
         &self,
-        _query: CommandGetXdbcTypeInfo,
+        query: CommandGetXdbcTypeInfo,
         _request: Request<Ticket>,
     ) -> Result<Response<<Self as FlightService>::DoGetStream>, Status> {
-        Err(Status::unimplemented(
-            "do_get_xdbc_type_info not implemented",
-        ))
+        // create a builder with pre-defined Xdbc data:
+        let builder = query.into_builder(&INSTANCE_XBDC_DATA);
+        let schema = builder.schema();
+        let batch = builder.build();
+        let stream = FlightDataEncoderBuilder::new()
+            .with_schema(schema)
+            .build(futures::stream::once(async { batch }))
+            .map_err(Status::from);
+        Ok(Response::new(Box::pin(stream)))
     }
 
     // do_put
diff --git a/arrow-flight/src/sql/metadata/mod.rs b/arrow-flight/src/sql/metadata/mod.rs
index 9d3810806..b823c1f4a 100644
--- a/arrow-flight/src/sql/metadata/mod.rs
+++ b/arrow-flight/src/sql/metadata/mod.rs
@@ -30,11 +30,13 @@ mod catalogs;
 mod db_schemas;
 mod sql_info;
 mod tables;
+mod xdbc_info;
 
 pub use catalogs::GetCatalogsBuilder;
 pub use db_schemas::GetDbSchemasBuilder;
 pub use sql_info::SqlInfoList;
 pub use tables::GetTablesBuilder;
+pub use xdbc_info::{XdbcTypeInfo, XdbcTypeInfoData, XdbcTypeInfoDataBuilder};
 
 use arrow_array::ArrayRef;
 use arrow_array::UInt32Array;
@@ -53,3 +55,18 @@ fn lexsort_to_indices(arrays: &[ArrayRef]) -> UInt32Array {
     sort.sort_unstable_by(|(_, a), (_, b)| a.cmp(b));
     UInt32Array::from_iter_values(sort.iter().map(|(i, _)| *i as u32))
 }
+
+#[cfg(test)]
+mod tests {
+    use arrow_array::RecordBatch;
+    use arrow_cast::pretty::pretty_format_batches;
+    pub fn assert_batches_eq(batches: &[RecordBatch], expected_lines: &[&str]) {
+        let formatted = pretty_format_batches(batches).unwrap().to_string();
+        let actual_lines: Vec<_> = formatted.trim().lines().collect();
+        assert_eq!(
+            &actual_lines, expected_lines,
+            "\n\nexpected:\n\n{:#?}\nactual:\n\n{:#?}\n\n",
+            expected_lines, actual_lines
+        );
+    }
+}
diff --git a/arrow-flight/src/sql/metadata/sql_info.rs b/arrow-flight/src/sql/metadata/sql_info.rs
index 3dcee1e58..4b4604078 100644
--- a/arrow-flight/src/sql/metadata/sql_info.rs
+++ b/arrow-flight/src/sql/metadata/sql_info.rs
@@ -440,21 +440,10 @@ mod tests {
     use std::collections::HashMap;
 
     use super::SqlInfoList;
+    use crate::sql::metadata::tests::assert_batches_eq;
     use crate::sql::{
         SqlInfo, SqlNullOrdering, SqlSupportedTransaction, SqlSupportsConvert,
     };
-    use arrow_array::RecordBatch;
-    use arrow_cast::pretty::pretty_format_batches;
-
-    fn assert_batches_eq(batches: &[RecordBatch], expected_lines: &[&str]) {
-        let formatted = pretty_format_batches(batches).unwrap().to_string();
-        let actual_lines: Vec<_> = formatted.trim().lines().collect();
-        assert_eq!(
-            &actual_lines, expected_lines,
-            "\n\nexpected:\n\n{:#?}\nactual:\n\n{:#?}\n\n",
-            expected_lines, actual_lines
-        );
-    }
 
     #[test]
     fn test_sql_infos() {
diff --git a/arrow-flight/src/sql/metadata/xdbc_info.rs b/arrow-flight/src/sql/metadata/xdbc_info.rs
new file mode 100644
index 000000000..cecef1b49
--- /dev/null
+++ b/arrow-flight/src/sql/metadata/xdbc_info.rs
@@ -0,0 +1,433 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Helpers for [`CommandGetXdbcTypeInfo`] metadata requests.
+//!
+//! - [`XdbcTypeInfo`] - a typed struct that holds the xdbc info corresponding to expected schema.
+//! - [`XdbcTypeInfoDataBuilder`] - a builder for collecting type infos
+//!   and building a conformant `RecordBatch`.
+//! - [`XdbcTypeInfoData`] - a helper type wrapping a `RecordBatch`
+//!   used for storing xdbc server metadata.
+//! - [`GetXdbcTypeInfoBuilder`] - a builder for consructing [`CommandGetXdbcTypeInfo`] responses.
+//!
+use std::sync::Arc;
+
+use arrow_array::builder::{BooleanBuilder, Int32Builder, ListBuilder, StringBuilder};
+use arrow_array::cast::downcast_array;
+use arrow_array::{ArrayRef, Int32Array, ListArray, RecordBatch};
+use arrow_ord::comparison::eq_scalar;
+use arrow_schema::{DataType, Field, Schema, SchemaRef};
+use arrow_select::filter::filter_record_batch;
+use arrow_select::take::take;
+use once_cell::sync::Lazy;
+
+use super::lexsort_to_indices;
+use crate::error::*;
+use crate::sql::{
+    CommandGetXdbcTypeInfo, Nullable, Searchable, XdbcDataType, XdbcDatetimeSubcode,
+};
+
+/// Data structure representing type information for xdbc types.
+#[derive(Debug, Clone, Default)]
+pub struct XdbcTypeInfo {
+    pub type_name: String,
+    pub data_type: XdbcDataType,
+    pub column_size: Option<i32>,
+    pub literal_prefix: Option<String>,
+    pub literal_suffix: Option<String>,
+    pub create_params: Option<Vec<String>>,
+    pub nullable: Nullable,
+    pub case_sensitive: bool,
+    pub searchable: Searchable,
+    pub unsigned_attribute: Option<bool>,
+    pub fixed_prec_scale: bool,
+    pub auto_increment: Option<bool>,
+    pub local_type_name: Option<String>,
+    pub minimum_scale: Option<i32>,
+    pub maximum_scale: Option<i32>,
+    pub sql_data_type: XdbcDataType,
+    pub datetime_subcode: Option<XdbcDatetimeSubcode>,
+    pub num_prec_radix: Option<i32>,
+    pub interval_precision: Option<i32>,
+}
+
+/// Helper to create [`CommandGetXdbcTypeInfo`] responses.
+///
+/// [`CommandGetXdbcTypeInfo`] are metadata requests used by a Flight SQL
+/// server to communicate supported capabilities to Flight SQL clients.
+///
+/// Servers constuct - usually static - [`XdbcTypeInfoData`] via the [XdbcTypeInfoDataBuilder`],
+/// and build responses by passing the [`GetXdbcTypeInfoBuilder`].
+pub struct XdbcTypeInfoData {
+    batch: RecordBatch,
+}
+
+impl XdbcTypeInfoData {
+    /// Return the raw (not encoded) RecordBatch that will be returned
+    /// from [`CommandGetXdbcTypeInfo`]
+    pub fn record_batch(&self, data_type: impl Into<Option<i32>>) -> Result<RecordBatch> {
+        if let Some(dt) = data_type.into() {
+            let arr: Int32Array = downcast_array(self.batch.column(1).as_ref());
+            let filter = eq_scalar(&arr, dt)?;
+            Ok(filter_record_batch(&self.batch, &filter)?)
+        } else {
+            Ok(self.batch.clone())
+        }
+    }
+
+    /// Return the schema of the RecordBatch that will be returned
+    /// from [`CommandGetXdbcTypeInfo`]
+    pub fn schema(&self) -> SchemaRef {
+        self.batch.schema()
+    }
+}
+
+pub struct XdbcTypeInfoDataBuilder {
+    infos: Vec<XdbcTypeInfo>,
+}
+
+impl Default for XdbcTypeInfoDataBuilder {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+/// A builder for [`XdbcTypeInfoData`] which is used to create [`CommandGetXdbcTypeInfo`] responses.
+///
+/// # Example
+/// ```
+/// use arrow_flight::sql::{Nullable, Searchable, XdbcDataType};
+/// use arrow_flight::sql::metadata::{XdbcTypeInfo, XdbcTypeInfoDataBuilder};
+/// // Create the list of metadata describing the server. Since this would not change at
+/// // runtime, using once_cell::Lazy or similar patterns to constuct the list is a common approach.
+/// let mut builder = XdbcTypeInfoDataBuilder::new();
+/// builder.append(XdbcTypeInfo {
+///     type_name: "INTEGER".into(),
+///     data_type: XdbcDataType::XdbcInteger,
+///     column_size: Some(32),
+///     literal_prefix: None,
+///     literal_suffix: None,
+///     create_params: None,
+///     nullable: Nullable::NullabilityNullable,
+///     case_sensitive: false,
+///     searchable: Searchable::Full,
+///     unsigned_attribute: Some(false),
+///     fixed_prec_scale: false,
+///     auto_increment: Some(false),
+///     local_type_name: Some("INTEGER".into()),
+///     minimum_scale: None,
+///     maximum_scale: None,
+///     sql_data_type: XdbcDataType::XdbcInteger,
+///     datetime_subcode: None,
+///     num_prec_radix: Some(2),
+///     interval_precision: None,
+/// });
+/// let info_list = builder.build().unwrap();
+///
+/// // to access the underlying record batch
+/// let batch = info_list.record_batch(None);
+/// ```
+impl XdbcTypeInfoDataBuilder {
+    /// Create a new instance of [`XdbcTypeInfoDataBuilder`].
+    pub fn new() -> Self {
+        Self { infos: Vec::new() }
+    }
+
+    /// Append a new row
+    pub fn append(&mut self, info: XdbcTypeInfo) {
+        self.infos.push(info);
+    }
+
+    /// Create helper structure for handling xdbc metadata requests.
+    pub fn build(self) -> Result<XdbcTypeInfoData> {
+        let mut type_name_builder = StringBuilder::new();
+        let mut data_type_builder = Int32Builder::new();
+        let mut column_size_builder = Int32Builder::new();
+        let mut literal_prefix_builder = StringBuilder::new();
+        let mut literal_suffix_builder = StringBuilder::new();
+        let mut create_params_builder = ListBuilder::new(StringBuilder::new());
+        let mut nullable_builder = Int32Builder::new();
+        let mut case_sensitive_builder = BooleanBuilder::new();
+        let mut searchable_builder = Int32Builder::new();
+        let mut unsigned_attribute_builder = BooleanBuilder::new();
+        let mut fixed_prec_scale_builder = BooleanBuilder::new();
+        let mut auto_increment_builder = BooleanBuilder::new();
+        let mut local_type_name_builder = StringBuilder::new();
+        let mut minimum_scale_builder = Int32Builder::new();
+        let mut maximum_scale_builder = Int32Builder::new();
+        let mut sql_data_type_builder = Int32Builder::new();
+        let mut datetime_subcode_builder = Int32Builder::new();
+        let mut num_prec_radix_builder = Int32Builder::new();
+        let mut interval_precision_builder = Int32Builder::new();
+
+        self.infos.into_iter().for_each(|info| {
+            type_name_builder.append_value(info.type_name);
+            data_type_builder.append_value(info.data_type as i32);
+            column_size_builder.append_option(info.column_size);
+            literal_prefix_builder.append_option(info.literal_prefix);
+            literal_suffix_builder.append_option(info.literal_suffix);
+            if let Some(params) = info.create_params {
+                if !params.is_empty() {
+                    for param in params {
+                        create_params_builder.values().append_value(param);
+                    }
+                    create_params_builder.append(true);
+                } else {
+                    create_params_builder.append_null();
+                }
+            } else {
+                create_params_builder.append_null();
+            }
+            nullable_builder.append_value(info.nullable as i32);
+            case_sensitive_builder.append_value(info.case_sensitive);
+            searchable_builder.append_value(info.searchable as i32);
+            unsigned_attribute_builder.append_option(info.unsigned_attribute);
+            fixed_prec_scale_builder.append_value(info.fixed_prec_scale);
+            auto_increment_builder.append_option(info.auto_increment);
+            local_type_name_builder.append_option(info.local_type_name);
+            minimum_scale_builder.append_option(info.minimum_scale);
+            maximum_scale_builder.append_option(info.maximum_scale);
+            sql_data_type_builder.append_value(info.sql_data_type as i32);
+            datetime_subcode_builder
+                .append_option(info.datetime_subcode.map(|code| code as i32));
+            num_prec_radix_builder.append_option(info.num_prec_radix);
+            interval_precision_builder.append_option(info.interval_precision);
+        });
+
+        let type_name = Arc::new(type_name_builder.finish());
+        let data_type = Arc::new(data_type_builder.finish());
+        let column_size = Arc::new(column_size_builder.finish());
+        let literal_prefix = Arc::new(literal_prefix_builder.finish());
+        let literal_suffix = Arc::new(literal_suffix_builder.finish());
+        let (field, offsets, values, nulls) = create_params_builder.finish().into_parts();
+        // Re-defined the field to be non-nullable
+        let new_field = Arc::new(field.as_ref().clone().with_nullable(false));
+        let create_params =
+            Arc::new(ListArray::new(new_field, offsets, values, nulls)) as ArrayRef;
+        let nullable = Arc::new(nullable_builder.finish());
+        let case_sensitive = Arc::new(case_sensitive_builder.finish());
+        let searchable = Arc::new(searchable_builder.finish());
+        let unsigned_attribute = Arc::new(unsigned_attribute_builder.finish());
+        let fixed_prec_scale = Arc::new(fixed_prec_scale_builder.finish());
+        let auto_increment = Arc::new(auto_increment_builder.finish());
+        let local_type_name = Arc::new(local_type_name_builder.finish());
+        let minimum_scale = Arc::new(minimum_scale_builder.finish());
+        let maximum_scale = Arc::new(maximum_scale_builder.finish());
+        let sql_data_type = Arc::new(sql_data_type_builder.finish());
+        let datetime_subcode = Arc::new(datetime_subcode_builder.finish());
+        let num_prec_radix = Arc::new(num_prec_radix_builder.finish());
+        let interval_precision = Arc::new(interval_precision_builder.finish());
+
+        let batch = RecordBatch::try_new(
+            Arc::clone(&GET_XDBC_INFO_SCHEMA),
+            vec![
+                type_name,
+                data_type,
+                column_size,
+                literal_prefix,
+                literal_suffix,
+                create_params,
+                nullable,
+                case_sensitive,
+                searchable,
+                unsigned_attribute,
+                fixed_prec_scale,
+                auto_increment,
+                local_type_name,
+                minimum_scale,
+                maximum_scale,
+                sql_data_type,
+                datetime_subcode,
+                num_prec_radix,
+                interval_precision,
+            ],
+        )?;
+
+        // Order batch by data_type and then by type_name
+        let sort_cols = batch.project(&[1, 0])?;
+        let indices = lexsort_to_indices(sort_cols.columns());
+        let columns = batch
+            .columns()
+            .iter()
+            .map(|c| take(c, &indices, None))
+            .collect::<std::result::Result<Vec<_>, _>>()?;
+
+        Ok(XdbcTypeInfoData {
+            batch: RecordBatch::try_new(batch.schema(), columns)?,
+        })
+    }
+
+    /// Return the [`Schema`] for a GetSchema RPC call with [`CommandGetXdbcTypeInfo`]
+    pub fn schema(&self) -> SchemaRef {
+        Arc::clone(&GET_XDBC_INFO_SCHEMA)
+    }
+}
+
+/// A builder for a [`CommandGetXdbcTypeInfo`] response.
+pub struct GetXdbcTypeInfoBuilder<'a> {
+    data_type: Option<i32>,
+    infos: &'a XdbcTypeInfoData,
+}
+
+impl CommandGetXdbcTypeInfo {
+    /// Create a builder suitable for constructing a response
+    pub fn into_builder(self, infos: &XdbcTypeInfoData) -> GetXdbcTypeInfoBuilder {
+        GetXdbcTypeInfoBuilder {
+            data_type: self.data_type,
+            infos,
+        }
+    }
+}
+
+impl GetXdbcTypeInfoBuilder<'_> {
+    /// Builds a `RecordBatch` with the correct schema for a [`CommandGetXdbcTypeInfo`] response
+    pub fn build(self) -> Result<RecordBatch> {
+        self.infos.record_batch(self.data_type)
+    }
+
+    /// Return the schema of the RecordBatch that will be returned
+    /// from [`CommandGetXdbcTypeInfo`]
+    pub fn schema(&self) -> SchemaRef {
+        self.infos.schema()
+    }
+}
+
+/// The schema for GetXdbcTypeInfo
+static GET_XDBC_INFO_SCHEMA: Lazy<SchemaRef> = Lazy::new(|| {
+    Arc::new(Schema::new(vec![
+        Field::new("type_name", DataType::Utf8, false),
+        Field::new("data_type", DataType::Int32, false),
+        Field::new("column_size", DataType::Int32, true),
+        Field::new("literal_prefix", DataType::Utf8, true),
+        Field::new("literal_suffix", DataType::Utf8, true),
+        Field::new(
+            "create_params",
+            DataType::List(Arc::new(Field::new("item", DataType::Utf8, false))),
+            true,
+        ),
+        Field::new("nullable", DataType::Int32, false),
+        Field::new("case_sensitive", DataType::Boolean, false),
+        Field::new("searchable", DataType::Int32, false),
+        Field::new("unsigned_attribute", DataType::Boolean, true),
+        Field::new("fixed_prec_scale", DataType::Boolean, false),
+        Field::new("auto_increment", DataType::Boolean, true),
+        Field::new("local_type_name", DataType::Utf8, true),
+        Field::new("minimum_scale", DataType::Int32, true),
+        Field::new("maximum_scale", DataType::Int32, true),
+        Field::new("sql_data_type", DataType::Int32, false),
+        Field::new("datetime_subcode", DataType::Int32, true),
+        Field::new("num_prec_radix", DataType::Int32, true),
+        Field::new("interval_precision", DataType::Int32, true),
+    ]))
+});
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::sql::metadata::tests::assert_batches_eq;
+
+    #[test]
+    fn test_create_batch() {
+        let mut builder = XdbcTypeInfoDataBuilder::new();
+        builder.append(XdbcTypeInfo {
+            type_name: "VARCHAR".into(),
+            data_type: XdbcDataType::XdbcVarchar,
+            column_size: Some(i32::MAX),
+            literal_prefix: Some("'".into()),
+            literal_suffix: Some("'".into()),
+            create_params: Some(vec!["length".into()]),
+            nullable: Nullable::NullabilityNullable,
+            case_sensitive: true,
+            searchable: Searchable::Full,
+            unsigned_attribute: None,
+            fixed_prec_scale: false,
+            auto_increment: None,
+            local_type_name: Some("VARCHAR".into()),
+            minimum_scale: None,
+            maximum_scale: None,
+            sql_data_type: XdbcDataType::XdbcVarchar,
+            datetime_subcode: None,
+            num_prec_radix: None,
+            interval_precision: None,
+        });
+        builder.append(XdbcTypeInfo {
+            type_name: "INTEGER".into(),
+            data_type: XdbcDataType::XdbcInteger,
+            column_size: Some(32),
+            literal_prefix: None,
+            literal_suffix: None,
+            create_params: None,
+            nullable: Nullable::NullabilityNullable,
+            case_sensitive: false,
+            searchable: Searchable::Full,
+            unsigned_attribute: Some(false),
+            fixed_prec_scale: false,
+            auto_increment: Some(false),
+            local_type_name: Some("INTEGER".into()),
+            minimum_scale: None,
+            maximum_scale: None,
+            sql_data_type: XdbcDataType::XdbcInteger,
+            datetime_subcode: None,
+            num_prec_radix: Some(2),
+            interval_precision: None,
+        });
+        builder.append(XdbcTypeInfo {
+            type_name: "INTERVAL".into(),
+            data_type: XdbcDataType::XdbcInterval,
+            column_size: Some(i32::MAX),
+            literal_prefix: Some("'".into()),
+            literal_suffix: Some("'".into()),
+            create_params: None,
+            nullable: Nullable::NullabilityNullable,
+            case_sensitive: false,
+            searchable: Searchable::Full,
+            unsigned_attribute: None,
+            fixed_prec_scale: false,
+            auto_increment: None,
+            local_type_name: Some("INTERVAL".into()),
+            minimum_scale: None,
+            maximum_scale: None,
+            sql_data_type: XdbcDataType::XdbcInterval,
+            datetime_subcode: Some(XdbcDatetimeSubcode::XdbcSubcodeUnknown),
+            num_prec_radix: None,
+            interval_precision: None,
+        });
+        let infos = builder.build().unwrap();
+
+        let batch = infos.record_batch(None).unwrap();
+        let expected = vec![
+            "+-----------+-----------+-------------+----------------+----------------+---------------+----------+----------------+------------+--------------------+------------------+----------------+-----------------+---------------+---------------+---------------+------------------+----------------+--------------------+",
+            "| type_name | data_type | column_size | literal_prefix | literal_suffix | create_params | nullable | case_sensitive | searchable | unsigned_attribute | fixed_prec_scale | auto_increment | local_type_name | minimum_scale | maximum_scale | sql_data_type | datetime_subcode | num_prec_radix | interval_precision |",
+            "+-----------+-----------+-------------+----------------+----------------+---------------+----------+----------------+------------+--------------------+------------------+----------------+-----------------+---------------+---------------+---------------+------------------+----------------+--------------------+",
+            "| INTEGER   | 4         | 32          |                |                |               | 1        | false          | 3          | false              | false            | false          | INTEGER         |               |               | 4             |                  | 2              |                    |",
+            "| INTERVAL  | 10        | 2147483647  | '              | '              |               | 1        | false          | 3          |                    | false            |                | INTERVAL        |               |               | 10            | 0                |                |                    |",
+            "| VARCHAR   | 12        | 2147483647  | '              | '              | [length]      | 1        | true           | 3          |                    | false            |                | VARCHAR         |               |               | 12            |                  |                |                    |",
+            "+-----------+-----------+-------------+----------------+----------------+---------------+----------+----------------+------------+--------------------+------------------+----------------+-----------------+---------------+---------------+---------------+------------------+----------------+--------------------+",
+        ];
+        assert_batches_eq(&[batch], &expected);
+
+        let batch = infos.record_batch(Some(10)).unwrap();
+        let expected = vec![
+            "+-----------+-----------+-------------+----------------+----------------+---------------+----------+----------------+------------+--------------------+------------------+----------------+-----------------+---------------+---------------+---------------+------------------+----------------+--------------------+",
+            "| type_name | data_type | column_size | literal_prefix | literal_suffix | create_params | nullable | case_sensitive | searchable | unsigned_attribute | fixed_prec_scale | auto_increment | local_type_name | minimum_scale | maximum_scale | sql_data_type | datetime_subcode | num_prec_radix | interval_precision |",
+            "+-----------+-----------+-------------+----------------+----------------+---------------+----------+----------------+------------+--------------------+------------------+----------------+-----------------+---------------+---------------+---------------+------------------+----------------+--------------------+",
+            "| INTERVAL  | 10        | 2147483647  | '              | '              |               | 1        | false          | 3          |                    | false            |                | INTERVAL        |               |               | 10            | 0                |                |                    |",
+            "+-----------+-----------+-------------+----------------+----------------+---------------+----------+----------------+------------+--------------------+------------------+----------------+-----------------+---------------+---------------+---------------+------------------+----------------+--------------------+",
+        ];
+        assert_batches_eq(&[batch], &expected);
+    }
+}