Repository: Seddryck/Packata
Pull request #151: feat: read parquet resources
Author: login: Seddryck
Labels: name: new-feature
Created: 2025-04-20T13:22:33Z | Merged: 2025-04-20T14:10:02Z

Close #149 

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added support for reading Parquet files as tabular data sources, enabling integration with APIs expecting tabular formats.
  - Introduced automatic detection and handling of Parquet resources in tabular reader selection.

- **Bug Fixes**
  - Improved handling of embedded Parquet resources to ensure correct loading and reading.

- **Tests**
  - Added comprehensive tests for Parquet reading, including successful data reading, error handling, and reader selection logic.

- **Refactor**
  - Enhanced the logic for inferring tabular reader types based on resource format and dialect.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Linked issue #149: Read parquet files
Parquet.Net should be the underlying reader but we still need to provide a wrapper around it for exposing an IDataReader

Diff:
diff --git a/src/Packata.ResourceReaders.Testing/Packata.ResourceReaders.Testing.csproj b/src/Packata.ResourceReaders.Testing/Packata.ResourceReaders.Testing.csproj
index 356cd82..197d9db 100644
--- a/src/Packata.ResourceReaders.Testing/Packata.ResourceReaders.Testing.csproj
+++ b/src/Packata.ResourceReaders.Testing/Packata.ResourceReaders.Testing.csproj
@@ -1,8 +1,10 @@
-<Project Sdk="Microsoft.NET.Sdk">
+﻿<Project Sdk="Microsoft.NET.Sdk">
   <ItemGroup>
+    <None Remove="Resources\iris.parquet" />
     <None Remove="Resources\my-book.xlsx" />
   </ItemGroup>
   <ItemGroup>
+    <EmbeddedResource Include="Resources\iris.parquet" />
     <EmbeddedResource Include="Resources\my-book.xlsx" />
   </ItemGroup>
   <ItemGroup>
diff --git a/src/Packata.ResourceReaders.Testing/Resources/iris.parquet b/src/Packata.ResourceReaders.Testing/Resources/iris.parquet
new file mode 100644
index 0000000..cb94493
Binary files /dev/null and b/src/Packata.ResourceReaders.Testing/Resources/iris.parquet differ
diff --git a/src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderBuilderTests.cs b/src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderBuilderTests.cs
new file mode 100644
index 0000000..0be7408
--- /dev/null
+++ b/src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderBuilderTests.cs
@@ -0,0 +1,67 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using Moq;
+using NUnit.Framework;
+using Packata.Core;
+using Packata.Core.PathHandling;
+using Packata.Core.Testing.PathHandling;
+using Packata.ResourceReaders.Tabular;
+
+namespace Packata.ResourceReaders.Testing.Tabular;
+public class ParquetReaderBuilderTests
+{
+    private static LocalPath GetPath()
+    {
+        using var stream = Assembly.GetExecutingAssembly()
+            .GetManifestResourceStream($"{typeof(ResourceTests).Namespace}.Resources.iris.parquet")
+            ?? throw new FileNotFoundException("Resource not found", $"{typeof(ResourceTests).Namespace}.Resources.iris.parquet");
+
+        var fileStream = new MemoryStream();
+        stream.CopyTo(fileStream);
+        var fileSystem = new Mock<IFileSystem>();
+        fileSystem.Setup(x => x.Exists("my-resource-path")).Returns(true);
+        fileSystem.Setup(x => x.OpenRead("my-resource-path")).Returns(fileStream);
+        return new LocalPath(fileSystem.Object, "", "my-resource-path");
+    }
+
+    [Test]
+    public void ToDataReader_ExistingLocalResource_ReturnsIDataReader()
+    {
+        var resource = new Resource
+        {
+            Paths = [GetPath()],
+            Type = "table",
+            Name = "my-resource",
+            Format = "parquet"
+        };
+        var builder = new ParquetReaderBuilder();
+        builder.Configure(resource);
+        var reader = builder.Build();
+
+        using var dataReader = reader.ToDataReader(resource);
+
+        Assert.That(dataReader, Is.Not.Null);
+        Assert.That(dataReader.Read(), Is.True);
+
+        using (Assert.EnterMultipleScope())
+        {
+            Assert.That(dataReader[0], Is.EqualTo(5.10));
+            Assert.That(dataReader["Sepal.Length"], Is.EqualTo(5.10));
+            Assert.That(dataReader.GetValue(0), Is.EqualTo(5.10));
+            Assert.That(dataReader.GetDouble(0), Is.EqualTo(5.10));
+            Assert.That(dataReader.GetName(0), Is.EqualTo("Sepal.Length"));
+            Assert.That(dataReader.GetOrdinal("Sepal.Length"), Is.EqualTo(0));
+
+            Assert.That(dataReader[4], Is.EqualTo("setosa"));
+            Assert.That(dataReader["Species"], Is.EqualTo("setosa"));
+            Assert.That(dataReader.GetValue(4), Is.EqualTo("setosa"));
+            Assert.That(dataReader.GetString(4), Is.EqualTo("setosa"));
+            Assert.That(dataReader.GetName(4), Is.EqualTo("Species"));
+            Assert.That(dataReader.GetOrdinal("Species"), Is.EqualTo(4));
+        }
+    }
+}
diff --git a/src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderTests.cs b/src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderTests.cs
new file mode 100644
index 0000000..a64c469
--- /dev/null
+++ b/src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderTests.cs
@@ -0,0 +1,113 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using Moq;
+using NUnit.Framework;
+using Packata.Core;
+using Packata.Core.PathHandling;
+using Packata.Core.Testing.PathHandling;
+using Packata.ResourceReaders.Tabular;
+
+namespace Packata.ResourceReaders.Testing.Tabular;
+public class ParquetReaderTests
+{
+    private static IEnumerable<IPath> GetPaths()
+    {
+        using var stream = Assembly.GetExecutingAssembly()
+            .GetManifestResourceStream($"{typeof(ResourceTests).Namespace}.Resources.iris.parquet")
+            ?? throw new FileNotFoundException("Resource not found", $"{typeof(ResourceTests).Namespace}.Resources.iris.parquet");
+
+        using var tmp = new MemoryStream();
+        stream.CopyTo(tmp);
+        var parquetBytes = tmp.ToArray();// cache the bytes once
+        
+        var fileSystem = new Mock<IFileSystem>();
+        fileSystem.Setup(fs => fs.Exists("my-resource-path")).Returns(true);
+        fileSystem.Setup(fs => fs.OpenRead("my-resource-path"))
+                          .Returns(() => new MemoryStream(parquetBytes, writable: false)); // fresh stream
+        yield return new LocalPath(fileSystem.Object, "", "my-resource-path");
+    }
+
+    [Test]
+    [TestCaseSource(nameof(GetPaths))]
+    public void ToDataReader_ExistingLocalResource_ReturnsIDataReader(IPath path)
+    {
+        var resource = new Resource() { Paths = [path], Type = "table", Name = "my-resource" };
+        var wrapper = new ParquetReaderWrapper();
+        var reader = new ParquetReader(wrapper);
+        var dataReader = reader.ToDataReader(resource);
+
+        Assert.That(dataReader, Is.Not.Null);
+        Assert.That(dataReader.Read(), Is.True);
+        using (Assert.EnterMultipleScope())
+        {
+            Assert.That(dataReader[0], Is.EqualTo(5.10));
+            Assert.That(dataReader[4], Is.EqualTo("setosa"));
+        }
+        Assert.That(dataReader.Read(), Is.True);
+        using (Assert.EnterMultipleScope())
+        {
+            Assert.That(dataReader[0], Is.EqualTo(4.90));
+            Assert.That(dataReader[4], Is.EqualTo("setosa"));
+        }
+        for (int i = 0; i < 148; i++)
+            Assert.That(dataReader.Read(), Is.True);
+        Assert.That(dataReader.Read(), Is.False);
+    }
+
+    [Test]
+    public void ToDataReader_NoResource_Throws()
+    {
+        var resource = new Resource() { Paths = [], Type = "table", Name = "my-resource" };
+        var wrapper = new ParquetReaderWrapper();
+        var reader = new ParquetReader(wrapper);
+        Assert.Throws<InvalidOperationException>(() => reader.ToDataReader(resource));
+    }
+
+    [Test]
+    [TestCaseSource(nameof(GetPaths))]
+    public void ToDataReader_TwoResources_DataReader(IPath path)
+    {
+        var resource = new Resource() { Paths = [path, path], Type = "table", Name = "my-resource" };
+        var wrapper = new ParquetReaderWrapper();
+        var reader = new ParquetReader(wrapper);
+        var dr = reader.ToDataReader(resource);
+        Assert.That(dr, Is.Not.Null);
+        Assert.That(dr, Is.TypeOf<ParquetDataReader>());
+    }
+
+    [Test]
+    public void ToDataReader_FileNotFound_ThrowsFileNotFoundException()
+    {
+        // Setup a path that doesn't exist
+        var fileSystem = new Mock<IFileSystem>();
+        fileSystem.Setup(x => x.Exists("non-existent-path")).Returns(false);
+        var path = new LocalPath(fileSystem.Object, "", "non-existent-path");
+
+        var resource = new Resource() { Paths = [path], Type = "table", Name = "my-resource" };
+        var wrapper = new ParquetReaderWrapper();
+        var reader = new ParquetReader(wrapper);
+
+        Assert.Throws<FileNotFoundException>(() => reader.ToDataReader(resource));
+    }
+
+    [Test]
+    public void ToDataReader_IOError_ThrowsIOException()
+    {
+        // Setup a path that throws an IO exception when opened
+        var fileSystem = new Mock<IFileSystem>();
+        fileSystem.Setup(x => x.Exists("error-path")).Returns(true);
+        fileSystem.Setup(x => x.OpenRead("error-path")).Throws<IOException>();
+        var path = new LocalPath(fileSystem.Object, "", "error-path");
+
+        var resource = new Resource() { Paths = [path], Type = "table", Name = "my-resource" };
+        var wrapper = new ParquetReaderWrapper();
+        var reader = new ParquetReader(wrapper);
+
+        Assert.Throws<IOException>(() => reader.ToDataReader(resource));
+    }
+}
diff --git a/src/Packata.ResourceReaders.Testing/TabularReaderFactoryTests.cs b/src/Packata.ResourceReaders.Testing/TabularReaderFactoryTests.cs
index 163154c..bb3c727 100644
--- a/src/Packata.ResourceReaders.Testing/TabularReaderFactoryTests.cs
+++ b/src/Packata.ResourceReaders.Testing/TabularReaderFactoryTests.cs
@@ -55,4 +55,33 @@ public void SetHeuristic_UseDialectProperties_ApplyHeuristics()
         reader = factory.Create(new Resource() { Type = "table" });
         Assert.That(reader, Is.EqualTo(structuredReader.Object));
     }
+
+    [Test]
+    [TestCase("delimited", typeof(DelimitedReader))]
+    [TestCase("spreadsheet", typeof(SpreadsheetReader))]
+    [TestCase("database", typeof(DatabaseReader))]
+    public void Create_WithTableDialectType_ReturnsDelimitedReader(string dialectType, Type expected)
+    {
+        var factory = new TabularReaderFactory();
+        var reader = factory.Create(new Resource() { Type = "table", Dialect = new TableDelimitedDialect() { Type = dialectType } });
+        Assert.That(reader, Is.InstanceOf(expected));
+    }
+
+    [Test]
+    [TestCase("csv", typeof(DelimitedReader))]
+    [TestCase("tsv", typeof(DelimitedReader))]
+    [TestCase("psv", typeof(DelimitedReader))]
+    [TestCase("csv.gz", typeof(DelimitedReader))]
+    [TestCase("tsv.gz", typeof(DelimitedReader))]
+    [TestCase("psv.gz", typeof(DelimitedReader))]
+    [TestCase("parquet", typeof(ParquetReader))]
+    [TestCase("pqt", typeof(ParquetReader))]
+    [TestCase("xls", typeof(SpreadsheetReader))]
+    [TestCase("xlsx", typeof(SpreadsheetReader))]
+    public void Create_WithTableFormat_ReturnsExpectedReader(string format, Type expected)
+    {
+        var factory = new TabularReaderFactory();
+        var reader = factory.Create(new Resource() { Type = "table", Format = format });
+        Assert.That(reader, Is.InstanceOf(expected));
+    }
 }
diff --git a/src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs b/src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs
index 510ee45..802a8c2 100644
--- a/src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs
+++ b/src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs
@@ -15,18 +15,23 @@ public override bool TryInfer(Resource resource, out TableDelimitedDialect? dial
         if (string.IsNullOrEmpty(mediaType))
             return false;
 
-        if (!mediaType.StartsWith("text/", StringComparison.OrdinalIgnoreCase))
-            return false;
-
-        mediaType = mediaType.ToLowerInvariant().Substring(5).Split([';'])[0];
-        var format = mediaType switch
+        string? format;
+        if (mediaType.StartsWith("text/", StringComparison.OrdinalIgnoreCase))
         {
-            "csv" => "csv",
-            "tsv" => "tsv",
-            "tab-separated-values" => "tsv",
-            "psv" => "psv",
-            _ => null
-        };
+            mediaType = mediaType.ToLowerInvariant().Substring(5).Split([';'])[0];
+            format = mediaType switch
+            {
+                "csv" => "csv",
+                "tsv" => "tsv",
+                "tab-separated-values" => "tsv",
+                "psv" => "psv",
+                _ => null
+            };
+        }
+        else
+        {
+            return false;
+        }
 
         return TryInferFromFormat(format, out dialect);
     }
diff --git a/src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs b/src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs
new file mode 100644
index 0000000..727a305
--- /dev/null
+++ b/src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs
@@ -0,0 +1,168 @@
+﻿using Parquet;
+using Parquet.Data;
+using Parquet.Schema;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Packata.ResourceReaders.Tabular;
+public class ParquetDataReader : System.Data.IDataReader
+{
+    private readonly List<Parquet.ParquetReader> _readers = new();
+    private readonly List<Stream> _streams = new(); // to dispose later
+    private readonly List<DataField[]> _schemas = new();
+    private readonly List<object[]> _rows = new();
+
+    private int _fieldCount = -1;
+    private int _currentRowIndex = -1;
+    private DataField[] _dataFields = [];
+
+    private ParquetDataReader() { }
+
+    public static async Task<ParquetDataReader> CreateAsync(IEnumerable<Stream> streams)
+    {
+        var reader = new ParquetDataReader();
+        foreach (var stream in streams)
+        {
+            var parquetReader = await Parquet.ParquetReader.CreateAsync(stream);
+            var dataFields = parquetReader.Schema.GetDataFields();
+
+            if (reader._fieldCount == -1)
+            {
+                reader._fieldCount = dataFields.Length;
+                reader._dataFields = dataFields;
+            }
+            else
+            {
+                // Ensure schemas are consistent across files
+                if (!reader.AreSchemasEqual(reader._dataFields, dataFields))
+                    throw new InvalidOperationException("Inconsistent schema across parquet files.");
+            }
+
+            reader._streams.Add(stream);
+            reader._readers.Add(parquetReader);
+            await reader.LoadRowsFromReaderAsync(parquetReader);
+        }
+
+        return reader;
+    }
+
+    private bool AreSchemasEqual(DataField[] a, DataField[] b)
+    {
+        if (a.Length != b.Length) return false;
+        for (int i = 0; i < a.Length; i++)
+        {
+            if (a[i].Name != b[i].Name || a[i].ClrType != b[i].ClrType)
+                return false;
+        }
+        return true;
+    }
+
+    private async Task LoadRowsFromReaderAsync(Parquet.ParquetReader reader)
+    {
+        for (int i = 0; i < reader.RowGroupCount; i++)
+        {
+            using var groupReader = reader.OpenRowGroupReader(i);
+            var columns = new DataColumn[_dataFields.Length];
+
+            for (int j = 0; j < _dataFields.Length; j++)
+            {
+                columns[j] = await groupReader.ReadColumnAsync(_dataFields[j]);
+            }
+
+            int rowCount = columns[0].Data.Length;
+            for (int row = 0; row < rowCount; row++)
+            {
+                var values = new object[_dataFields.Length];
+                for (int col = 0; col < _dataFields.Length; col++)
+                {
+                    values[col] = columns[col].Data.GetValue(row)!;
+                }
+                _rows.Add(values);
+            }
+        }
+    }
+
+    public bool Read()
+    {
+        _currentRowIndex++;
+        return _currentRowIndex < _rows.Count;
+    }
+
+    public int FieldCount => _dataFields.Length;
+
+    public object GetValue(int i) => _rows[_currentRowIndex][i];
+
+    public string GetName(int i) => _dataFields[i].Name;
+    public string GetDataTypeName(int i) => _dataFields[i].SchemaType.ToString();
+    public Type GetFieldType(int i) => _dataFields[i].ClrType;
+    public int GetOrdinal(string name)
+    {
+        for (int i = 0; i < _dataFields.Length; i++)
+            if (_dataFields[i].Name == name)
+                return i;
+        throw new ArgumentException($"Column name '{name}' not found", nameof(name));
+    }
+
+    public bool IsDBNull(int i) => GetValue(i) == null;
+
+    public int GetInt32(int i) => (int)GetValue(i);
+    public long GetInt64(int i) => (long)GetValue(i);
+    public string GetString(int i) => (string)GetValue(i);
+    public bool GetBoolean(int i) => (bool)GetValue(i);
+    public DateTime GetDateTime(int i) => (DateTime)GetValue(i);
+
+    // Not implemented or rarely used
+    public object this[int i] => GetValue(i);
+    public object this[string name] => GetValue(GetOrdinal(name));
+    public int Depth => 0;
+    public bool IsClosed => _disposed;
+    public int RecordsAffected => -1;
+    public void Close() => Dispose();
+    public System.Data.DataTable GetSchemaTable() => throw new NotSupportedException();
+    public bool NextResult() => false;
+
+    private bool _disposed = false;
+
+    public void Dispose()
+    {
+        Dispose(true);
+        GC.SuppressFinalize(this);
+    }
+
+    protected virtual void Dispose(bool disposing)
+    {
+        if (_disposed) return;
+        if (disposing)
+        {
+            foreach (var reader in _readers)
+                reader.Dispose();
+            foreach (var stream in _streams)
+                stream.Dispose();
+        }
+        _disposed = true;
+    }
+
+    public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => throw new NotSupportedException();
+    public long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) => throw new NotSupportedException();
+    public char GetChar(int i) => (char)GetValue(i);
+    public Guid GetGuid(int i) => (Guid)GetValue(i);
+    public short GetInt16(int i) => (short)GetValue(i);
+    public float GetFloat(int i) => (float)GetValue(i);
+    public double GetDouble(int i) => (double)GetValue(i);
+    public decimal GetDecimal(int i) => (decimal)GetValue(i);
+    public string GetDataTypeName(string columnName) => GetDataTypeName(GetOrdinal(columnName));
+    public System.Data.IDataReader GetData(int i) => throw new NotSupportedException();
+    System.Data.DataTable? System.Data.IDataReader.GetSchemaTable() => throw new NotImplementedException();
+    public byte GetByte(int i) => throw new NotImplementedException();
+    System.Data.IDataReader System.Data.IDataRecord.GetData(int i) => throw new NotImplementedException();
+    public int GetValues(object[] values)
+    {
+        if (_currentRowIndex< 0 || _currentRowIndex >= _rows.Count)
+            return 0;
+        var current = _rows[_currentRowIndex];
+        var len = Math.Min(values.Length, current.Length);
+        Array.Copy(current, values, len);
+        return len;
+    }
+}
diff --git a/src/Packata.ResourceReaders/Tabular/ParquetReader.cs b/src/Packata.ResourceReaders/Tabular/ParquetReader.cs
new file mode 100644
index 0000000..905ce5f
--- /dev/null
+++ b/src/Packata.ResourceReaders/Tabular/ParquetReader.cs
@@ -0,0 +1,28 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Packata.Core;
+using Packata.Core.ResourceReading;
+
+namespace Packata.ResourceReaders.Tabular;
+internal class ParquetReader : IResourceReader
+{
+    private ParquetReaderWrapper Reader { get; }
+
+    public ParquetReader(ParquetReaderWrapper reader)
+        => Reader = reader;
+
+    public IDataReader ToDataReader(Resource resource)
+    {
+        if (resource.Paths.Count == 0)
+            throw new InvalidOperationException(
+                "The resource does not contain any paths, but at least one is required to create a DataReader.");
+
+        return resource.Paths.Count > 1
+            ? Reader.ToDataReader(resource.Paths.Select(p => p.ToStream()))
+            : Reader.ToDataReader(resource.Paths[0].ToStream());
+    }
+}
diff --git a/src/Packata.ResourceReaders/Tabular/ParquetReaderBuilder.cs b/src/Packata.ResourceReaders/Tabular/ParquetReaderBuilder.cs
new file mode 100644
index 0000000..5b84957
--- /dev/null
+++ b/src/Packata.ResourceReaders/Tabular/ParquetReaderBuilder.cs
@@ -0,0 +1,22 @@
+﻿using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using DubUrl.Querying.Dialects;
+using Packata.Core;
+using Packata.Core.ResourceReading;
+using Packata.Core.Validation;
+
+namespace Packata.ResourceReaders.Tabular;
+public class ParquetReaderBuilder : IResourceReaderBuilder
+{
+    public void Configure(Resource resource)
+    { }
+
+    public IResourceReader Build()
+    {
+        var wrapper = new ParquetReaderWrapper();
+        return new ParquetReader(wrapper);
+    }
+}
diff --git a/src/Packata.ResourceReaders/Tabular/ParquetReaderWrapper.cs b/src/Packata.ResourceReaders/Tabular/ParquetReaderWrapper.cs
new file mode 100644
index 0000000..2076303
--- /dev/null
+++ b/src/Packata.ResourceReaders/Tabular/ParquetReaderWrapper.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using ExcelDataReader;
+using Packata.Core;
+
+namespace Packata.ResourceReaders.Tabular;
+internal class ParquetReaderWrapper
+{
+    public IDataReader ToDataReader(Stream stream)
+    {
+        var reader = ParquetDataReader.CreateAsync(new[] { stream }).Result;
+        return reader;
+    }
+
+    public IDataReader ToDataReader(IEnumerable<Stream> streams)
+    {
+        var reader = ParquetDataReader.CreateAsync(streams)
+            .ConfigureAwait(false).GetAwaiter().GetResult();
+        return reader;
+    }
+}
diff --git a/src/Packata.ResourceReaders/TabularReaderFactory.cs b/src/Packata.ResourceReaders/TabularReaderFactory.cs
index d2a4b5d..a928232 100644
--- a/src/Packata.ResourceReaders/TabularReaderFactory.cs
+++ b/src/Packata.ResourceReaders/TabularReaderFactory.cs
@@ -13,6 +13,7 @@ internal class TabularReaderFactory : IResourceReaderFactory
     public const string Delimited = "delimited";
     public const string Structured = "structured";
     public const string Spreadsheet = "spreadsheet";
+    public const string Parquet = "parquet";
     public const string Database = "database";
 
     private Func<Resource, string> Heuristic { get; set; }
@@ -33,7 +34,8 @@ public TabularReaderFactory()
         AddOrReplaceReader(Delimited, new DelimitedReaderBuilder());
         AddOrReplaceReader(Database, new DatabaseReaderBuilder());
         AddOrReplaceReader(Spreadsheet, new SpreadsheetReaderBuilder());
-        Heuristic = resource => resource.Dialect?.Type ?? Delimited;
+        AddOrReplaceReader(Parquet, new ParquetReaderBuilder());
+        Heuristic = TabularHeuristic;
     }
 
     public IResourceReader Create(Resource resource)
@@ -47,4 +49,26 @@ public IResourceReader Create(Resource resource)
         builder.Configure(resource);
         return builder.Build();
     }
+
+    private static string TabularHeuristic(Resource resource)
+    {
+        if (resource.Dialect?.Type is not null)
+            return resource.Dialect.Type;
+
+        if (resource.Format is not null)
+        {
+            var format = resource.Format.ToLowerInvariant();
+            if (format.EndsWith(".gz"))
+                format = format[0..^3];
+
+            return (format) switch
+            {
+                "csv" or "tsv" or "psv" or "txt" => Delimited,
+                "xlsx" or "xls" => Spreadsheet,
+                "parquet" or "pqt" => Parquet,
+                _ => Delimited,
+            };
+        }
+        return Delimited;
+    }
 }


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 6**

<details>
<summary>🧹 Nitpick comments (10)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetReaderBuilder.cs (1)</summary><blockquote>

`14-15`: **Consider documenting the empty Configure method**

The `Configure` method is implemented with an empty body. If no configuration is needed for Parquet readers, consider adding a comment explaining why this method is empty to improve code clarity.


```diff
public void Configure(Resource resource)
-{ }
+{
+    // No configuration needed for Parquet readers
+}
```

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetReaderWrapper.cs (1)</summary><blockquote>

`7-7`: **Remove unused import**

The `ExcelDataReader` import is not used in this class and should be removed.


```diff
-using ExcelDataReader;
```

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders/TabularReaderFactory.cs (1)</summary><blockquote>

`53-73`: **Consider adding documentation for the TabularHeuristic method**

The implementation of the TabularHeuristic method is well-structured and handles various file formats appropriately. Consider adding XML documentation to explain its purpose and behavior, especially since it's a key method for determining the correct reader type.


```diff
+    /// <summary>
+    /// Determines the appropriate reader type based on resource dialect type or format.
+    /// First checks if the resource has a dialect with a specified type, then falls back to the format.
+    /// Handles gzipped formats by removing the .gz extension before determining the reader type.
+    /// </summary>
+    /// <param name="resource">The resource to analyze</param>
+    /// <returns>The determined reader type</returns>
    private static string TabularHeuristic(Resource resource)
    {
        if (resource.Dialect?.Type is not null)
            return resource.Dialect.Type;
```

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderBuilderTests.cs (2)</summary><blockquote>

`17-29`: **Consider extracting resource loading to a reusable helper**

The `GetPath()` method loads an embedded Parquet resource file and sets up a mock file system. This pattern might be reused in other test files. Consider extracting this to a shared helper class to avoid code duplication.


```diff
+// In a shared test helper class:
+public static class ParquetTestHelpers
+{
+    public static LocalPath GetTestParquetPath(string resourceName = "iris.parquet", string pathName = "my-resource-path")
+    {
+        using var stream = Assembly.GetExecutingAssembly()
+            .GetManifestResourceStream($"{typeof(ResourceTests).Namespace}.Resources.{resourceName}")
+            ?? throw new FileNotFoundException("Resource not found", $"{typeof(ResourceTests).Namespace}.Resources.{resourceName}");
+
+        var fileStream = new MemoryStream();
+        stream.CopyTo(fileStream);
+        var fileSystem = new Mock<IFileSystem>();
+        fileSystem.Setup(x => x.Exists(pathName)).Returns(true);
+        fileSystem.Setup(x => x.OpenRead(pathName)).Returns(fileStream);
+        return new LocalPath(fileSystem.Object, "", pathName);
+    }
+}
+
+// In the test class:
+private static LocalPath GetPath()
+{
+    return ParquetTestHelpers.GetTestParquetPath();
+}
```

---

`20-21`: **Fix potential resource leak with the Assembly stream**

When getting the manifest resource stream, the stream is properly wrapped in a using statement. However, if the stream is null, a FileNotFoundException is thrown, which could bypass the dispose of the stream. Consider using the null-conditional operator with the using statement.


```diff
-        using var stream = Assembly.GetExecutingAssembly()
-            .GetManifestResourceStream($"{typeof(ResourceTests).Namespace}.Resources.iris.parquet")
-            ?? throw new FileNotFoundException("Resource not found", $"{typeof(ResourceTests).Namespace}.Resources.iris.parquet");
+        var resourceName = $"{typeof(ResourceTests).Namespace}.Resources.iris.parquet";
+        using var stream = Assembly.GetExecutingAssembly()
+            .GetManifestResourceStream(resourceName);
+        
+        if (stream == null)
+            throw new FileNotFoundException("Resource not found", resourceName);
```

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderTests.cs (2)</summary><blockquote>

`46-54`: **Avoid exact equality on floating‑point numbers in assertions**  
Relying on `Is.EqualTo(5.10)` can lead to flaky tests due to binary floating‑point representation. Instead compare within a tolerance.

```diff
-            Assert.That(dataReader[0], Is.EqualTo(5.10));
+            Assert.That((double)dataReader[0], Is.EqualTo(5.10).Within(1e-12));
```
Repeat for line 52.

---

`55-57`: **Micro‑optimisation: use `Assert.Multiple` instead of manual loop**  
The loop performs 148 iterations of `Assert.That`, producing 149 separate failures for one broken condition. Wrapping the count check in a single assertion is clearer and faster.

```diff
-        for (int i = 0; i < 148; i++)
-            Assert.That(dataReader.Read(), Is.True);
-        Assert.That(dataReader.Read(), Is.False);
+        for (var i = 0; i < 148; i++)
+            Assert.That(dataReader.Read(), Is.True, $"Row {i + 3} should exist");
+        Assert.That(dataReader.Read(), Is.False, "Reader should be exhausted after 150 rows");
```

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs (3)</summary><blockquote>

`16-18`: **Redundant `_fieldCount` & unused `_schemas` – simplify internal state**  
`_fieldCount` and `_schemas` are never consumed after assignment; the authoritative value is `_dataFields.Length`. Keeping dead fields increases maintenance cost.

```diff
-    private readonly List<DataField[]> _schemas = new();
-    private int _fieldCount = -1;
+    // _schemas removed – schema consistency is verified on the fly
```

---

`30-34`: **`_fieldCount` is only used as “unset” sentinel – refactor for clarity**  
Instead of an extra variable use `_dataFields.Length == 0` to detect first file.

```diff
-            if (reader._fieldCount == -1)
+            if (reader._dataFields.Length == 0)
```
(This change is naturally required if you remove `_fieldCount` as suggested above.)

---

`61-84`: **Full materialisation of all rows can exhaust memory for large datasets**  
`LoadRowsFromReaderAsync` eagerly loads *every* row of *every* file into `_rows`. A single 1 GB Parquet file will blow up memory usage and GC pressure.

If performance allows, expose a streaming implementation instead:

1. Keep a reference to the current row‑group reader.
2. Lazily read the next row group when `Read` exhausts the current one.
3. Cache only a single row group at a time.

This change would also allow you to drop the `_rows` list entirely.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 721f0ad9a13fbed93fc5e20eb304fa8f75b7db5a and ce247a9a8f21e7185faaafcdc5513d8d9eca5117.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `src/Packata.ResourceReaders.Testing/Resources/iris.parquet` is excluded by `!**/*.parquet`

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `src/Packata.ResourceReaders.Testing/Packata.ResourceReaders.Testing.csproj` (1 hunks)
* `src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderBuilderTests.cs` (1 hunks)
* `src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderTests.cs` (1 hunks)
* `src/Packata.ResourceReaders.Testing/TabularReaderFactoryTests.cs` (1 hunks)
* `src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs` (1 hunks)
* `src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs` (1 hunks)
* `src/Packata.ResourceReaders/Tabular/ParquetReader.cs` (1 hunks)
* `src/Packata.ResourceReaders/Tabular/ParquetReaderBuilder.cs` (1 hunks)
* `src/Packata.ResourceReaders/Tabular/ParquetReaderWrapper.cs` (1 hunks)
* `src/Packata.ResourceReaders/TabularReaderFactory.cs` (3 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetReaderBuilder.cs (4)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs (4)</summary>

* `System` (123-123)
* `System` (143-143)
* `System` (144-144)
* `System` (146-146)

</details>
<details>
<summary>src/Packata.ResourceReaders/TabularReaderFactory.cs (1)</summary>

* `IResourceReader` (41-51)

</details>
<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetReaderWrapper.cs (1)</summary>

* `ParquetReaderWrapper` (11-24)

</details>
<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetReader.cs (2)</summary>

* `ParquetReader` (11-28)
* `ParquetReader` (15-16)

</details>

</blockquote></details>
<details>
<summary>src/Packata.ResourceReaders/TabularReaderFactory.cs (2)</summary><blockquote>

<details>
<summary>src/Packata.Core/ResourceReading/IResourceReaderFactory.cs (1)</summary>

* `AddOrReplaceReader` (10-10)

</details>
<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetReaderBuilder.cs (1)</summary>

* `ParquetReaderBuilder` (12-22)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders.Testing/Packata.ResourceReaders.Testing.csproj (1)</summary>

`3-3`: **Properly configured Parquet test resource**

Adding the iris.parquet file as an embedded resource follows the same pattern used for other test resources like my-book.xlsx. This ensures the test file will be accessible during test execution.



Also applies to: 7-7

</details>
<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetReaderBuilder.cs (1)</summary>

`17-21`: **Implementation looks good**

The builder pattern implementation is clean and follows the expected structure for reader builders in the codebase.

</details>
<details>
<summary>src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs (1)</summary>

`18-34`: **Code refactoring improves readability**

The refactoring of the `TryInfer` method uses a more explicit if-else structure and cleaner variable declarations, making the code more readable and maintainable. The switch expression for mapping media types is clear and concise.

</details>
<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetReader.cs (2)</summary>

`1-28`: **LGTM: Clean implementation of IResourceReader for Parquet files**

The implementation is straightforward and follows the pattern used by other resource readers in the project. It correctly delegates to `ParquetReaderWrapper` for the actual reading logic and appropriately validates that at least one path is provided.

---

`18-27`: <details>
<summary>❓ Verification inconclusive</summary>

**Consider adding Stream disposal**

The `ToDataReader` method correctly handles both single and multiple paths, but it doesn't manage the disposal of streams created by the `ToStream()` method. Ensure that the `ParquetReaderWrapper` takes responsibility for disposing these streams, or consider wrapping the stream creation in a try-finally block to ensure proper resource cleanup.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if ParquetReaderWrapper handles stream disposal
echo "Checking ParquetReaderWrapper implementation for stream disposal:"
rg -A 10 "ToDataReader.*Stream" --include "ParquetReaderWrapper.cs"
```

Length of output: 650

---

🏁 Script executed:

```shell
#!/bin/bash
set -e

echo "1) Locate ParquetReaderWrapper declaration:"
files=$(rg -l 'class\s\+ParquetReaderWrapper')
if [ -z "$files" ]; then
  echo "No `ParquetReaderWrapper` class found. Searching any file named *ParquetReaderWrapper.cs*..."
  files=$(fd ParquetReaderWrapper --extension cs || true)
fi

if [ -z "$files" ]; then
  echo "❌ No ParquetReaderWrapper implementation found in the repo."
  exit 1
fi

echo "✔ Found implementation file(s):"
echo "$files"
echo

for file in $files; do
  echo "----- Inspecting $file -----"
  echo "2) Extracting `ToDataReader` overloads:"
  rg 'public\s\+IDataReader\s\+ToDataReader' -n -A 20 "$file" || true
  echo
done
```

Length of output: 155

---

```bash
#!/bin/bash
set -e

# Locate ParquetReader.cs
file=$(fd ParquetReader.cs --extension cs | head -n1)
if [ -z "$file" ]; then
  echo "❌ Could not find ParquetReader.cs"
  exit 1
fi

echo "Inspecting $file"
echo "---------- Class declaration ----------"
rg -n "class\s\+ParquetReader" -n -A 5 "$file"

echo
echo "---------- Reader field/property ----------"
rg -n "Reader" -n -A 5 "$file"
```

---



</details>

**Ensure proper disposal of streams in ParquetReader.ToDataReader**

The `ToDataReader` overload in `src/Packata.ResourceReaders/Tabular/ParquetReader.cs` (lines 18–27) calls `p.ToStream()` for each path without disposing the resulting `Stream` objects. You should verify whether your `Reader.ToDataReader(...)` implementation takes ownership of and disposes these streams. If it does not, wrap the stream creation calls in `using` blocks or a `try/finally` to guarantee cleanup:

• File: `src/Packata.ResourceReaders/Tabular/ParquetReader.cs`  
  Method: `ToDataReader(Resource resource)` (lines 18–27)  
  - Single‑path case: `Reader.ToDataReader(resource.Paths[0].ToStream())`  
  - Multi‑path case: `Reader.ToDataReader(resource.Paths.Select(p => p.ToStream()))`  

Action items:  
1. Confirm that `Reader.ToDataReader(Stream…)` or your Parquet reader wrapper disposes the input streams.  
2. If not, refactor to ensure each `Stream` returned by `ToStream()` is properly disposed after use—either by delegating disposal to the reader or by wrapping in `using`/`try‑finally`.

</details>
<details>
<summary>src/Packata.ResourceReaders/TabularReaderFactory.cs (3)</summary>

`16-16`: **LGTM: Clear constant definition for Parquet reader type**

Adding a constant for the Parquet reader type maintains consistency with other reader types in the factory.

---

`37-38`: **LGTM: Proper registration of ParquetReaderBuilder**

The ParquetReaderBuilder is correctly registered with the factory and the heuristic is updated to use the new TabularHeuristic method.

---

`68-68`: **LGTM: Proper mapping of Parquet file extensions**

The method correctly maps both "parquet" and "pqt" file extensions to the Parquet reader type.

</details>
<details>
<summary>src/Packata.ResourceReaders.Testing/TabularReaderFactoryTests.cs (2)</summary>

`59-68`: **LGTM: Comprehensive test for dialect-based reader selection**

The test effectively verifies that the factory correctly creates the appropriate reader based on the dialect type. It covers all the supported dialect types and ensures they map to the expected reader implementations.

---

`70-86`: **LGTM: Thorough testing of format-based reader selection**

The test properly verifies that the factory creates the correct reader based on the file format. It covers various formats including CSV, TSV, PSV (with and without gzip), Parquet formats, and Excel formats, ensuring they all map to the expected reader types.

</details>
<details>
<summary>src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderBuilderTests.cs (1)</summary>

`31-66`: **LGTM: Comprehensive testing of ParquetReader functionality**

The test thoroughly validates that the ParquetReader correctly reads data from a Parquet file and exposes it through the IDataReader interface. It verifies various access methods including indexers, GetValue, typed getters, and column metadata methods. The assertions are well-structured using NUnit's EnterMultipleScope for better error reporting.

</details>
<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs (1)</summary>

`50-59`: **Schema comparison ignores nullability, precision, logical types**  
Simply matching `Name` and `ClrType` is insufficient; `decimal(10,2)` vs `decimal(12,2)` or a nullable column would pass this check but yield runtime errors further down.

Consider comparing `DataField.SchemaType`, `IsNullable`, and for decimals `Precision`/`Scale`.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-20T13:25:24Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs (6)</summary><blockquote>

`1-7`: **Add XML documentation for the class and public members**

The class lacks XML documentation comments explaining its purpose and usage. Since this is a public class implementing a standard interface, proper documentation would help users understand how it fits into the overall architecture.

Add XML documentation for the class:

```diff
+/// <summary>
+/// Provides a System.Data.IDataReader implementation for reading data from Parquet files.
+/// This reader loads all rows into memory for performant access.
+/// </summary>
 public class ParquetDataReader : System.Data.IDataReader
```

---

`61-84`: **LoadRowsFromReaderAsync could be optimized for memory usage**

The method loads all rows into memory, which could lead to high memory consumption for large files. Additionally, the code has a nested loop with multiple array allocations that could impact performance.


Consider using a more memory-efficient approach by:
1. Implementing lazy loading with a cursor
2. Adding options to limit the number of rows loaded
3. Using a more efficient data structure for the rows

For now, at minimum, pre-allocate the values array once per group:

```diff
 private async Task LoadRowsFromReaderAsync(Parquet.ParquetReader reader)
 {
     for (int i = 0; i < reader.RowGroupCount; i++)
     {
         using var groupReader = reader.OpenRowGroupReader(i);
         var columns = new DataColumn[_dataFields.Length];

         for (int j = 0; j < _dataFields.Length; j++)
         {
             columns[j] = await groupReader.ReadColumnAsync(_dataFields[j]);
         }

         int rowCount = columns[0].Data.Length;
+        var values = new object[_dataFields.Length]; // Pre-allocate once
         for (int row = 0; row < rowCount; row++)
         {
-            var values = new object[_dataFields.Length];
             for (int col = 0; col < _dataFields.Length; col++)
             {
                 values[col] = columns[col].Data.GetValue(row)!;
             }
             _rows.Add(values.ToArray()); // Clone the array for each row
         }
     }
 }
```

---

`120-121`: **Implement RecordsAffected more appropriately**

The `RecordsAffected` property is returning -1, but could be more useful if it returned the total row count since all rows are loaded in memory.


```diff
-public int RecordsAffected => -1;
+public int RecordsAffected => _rows.Count;
```

---

`147-154`: **Improve error messages in unsupported methods**

Methods that throw NotSupportedException or NotImplementedException should include descriptive error messages explaining why they're not supported.


```diff
-public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => throw new NotSupportedException();
+public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => 
+    throw new NotSupportedException("GetBytes is not supported by the ParquetDataReader implementation");
```

Apply similar patterns to other methods throwing exceptions.

---

`86-93`: **Consider adding validation in Read() method**

The `Read()` method advances the row cursor but doesn't check for invalid state like if the reader has been disposed.


```diff
 public bool Read()
 {
+    if (_disposed)
+        throw new ObjectDisposedException(nameof(ParquetDataReader));
+        
     _currentRowIndex++;
     return _currentRowIndex < _rows.Count;
 }
```

---

`50-59`: **Consider enhancing schema comparison**

The schema comparison only checks for name and CLR type, but you might want to also check field attributes, nullable status, or other schema properties for a more complete comparison.


Additionally, consider making this method publicly accessible as it could be useful for consumers who need to compare Parquet schemas outside this class.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ce247a9a8f21e7185faaafcdc5513d8d9eca5117 and 266353843a69371aedee0c7493df8773907e1ae6.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderTests.cs` (1 hunks)
* `src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs` (1 hunks)
* `src/Packata.ResourceReaders/Tabular/ParquetReaderWrapper.cs` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary>

* src/Packata.ResourceReaders/Tabular/ParquetReaderWrapper.cs
* src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderTests.cs

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs (4)</summary><blockquote>

<details>
<summary>src/Packata.Core/Schema.cs (1)</summary>

* `Schema` (10-51)

</details>
<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetReaderWrapper.cs (2)</summary>

* `IDataReader` (13-17)
* `IDataReader` (19-24)

</details>
<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetReader.cs (3)</summary>

* `IDataReader` (18-27)
* `ParquetReader` (11-28)
* `ParquetReader` (15-16)

</details>
<details>
<summary>src/Packata.ResourceReaders/Tabular/ExcelDataReader.cs (1)</summary>

* `DataTable` (44-45)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs (4)</summary>

`160-168`: **Implementation of GetValues matches the previous review suggestion**

The implementation of the `GetValues` method matches the suggestion from a previous review comment. Good job on implementing this correctly!

---

`126-145`: **Well-implemented IDisposable pattern with proper state tracking**

The IDisposable implementation follows best practices with:
1. Tracking disposed state
2. Virtual Dispose(bool) method
3. SuppressFinalize call
4. Protection against multiple dispose calls

This is well-implemented and avoids resource leaks.

---

`155-159`: _⚠️ Potential issue_

**Redundant method declarations**

There are redundant implementations/declarations of methods like `GetSchemaTable` and `GetData` with slightly different signatures or behaviors.


```diff
-public string GetDataTypeName(string columnName) => GetDataTypeName(GetOrdinal(columnName));
-public System.Data.IDataReader GetData(int i) => throw new NotSupportedException();
-System.Data.DataTable? System.Data.IDataReader.GetSchemaTable() => throw new NotImplementedException();
-public byte GetByte(int i) => throw new NotImplementedException();
-System.Data.IDataReader System.Data.IDataRecord.GetData(int i) => throw new NotImplementedException();
+public string GetDataTypeName(string columnName) => GetDataTypeName(GetOrdinal(columnName));
+public System.Data.IDataReader GetData(int i) => throw new NotSupportedException();
+public byte GetByte(int i) => throw new NotImplementedException();
```



> Likely an incorrect or invalid review comment.

---

`16-18`: _🛠️ Refactor suggestion_

**Field _dataFields is initialized with empty array but could be null**

The `_dataFields` field is initialized with an empty array, but there's no defensive check in methods like `GetName` to prevent NullReferenceException if used before initialization. This could happen if no streams are provided to `CreateAsync`.


```diff
-private DataField[] _dataFields = [];
+private DataField[] _dataFields = Array.Empty<DataField>();
```

Also consider adding validation in the `CreateAsync` method to ensure at least one stream is provided.



> Likely an incorrect or invalid review comment.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-20T13:50:43Z

Review comments:
diff_hunk: @@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using ExcelDataReader;
+using Packata.Core;
+
+namespace Packata.ResourceReaders.Tabular;
+internal class ParquetReaderWrapper
+{
+    public IDataReader ToDataReader(Stream stream)
+    {
+        var reader = ParquetDataReader.CreateAsync(new[] { stream }).Result;
+        return reader;
+    }
path: src/Packata.ResourceReaders/Tabular/ParquetReaderWrapper.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Avoid blocking on async operations**

Using `.Result` to synchronously wait for an asynchronous operation can lead to deadlocks in certain contexts, particularly in UI or ASP.NET applications. Consider one of these approaches:

1. Make these methods async and use `await`:
```csharp
public async Task<IDataReader> ToDataReaderAsync(Stream stream)
{
    var reader = await ParquetDataReader.CreateAsync(new[] { stream });
    return reader;
}
```

2. If you must provide synchronous methods, use a safe sync-over-async pattern:
```csharp
public IDataReader ToDataReader(Stream stream)
{
    // ConfigureAwait(false) + GetAwaiter().GetResult() reduces deadlock risk
    var reader = ParquetDataReader.CreateAsync(new[] { stream })
        .ConfigureAwait(false).GetAwaiter().GetResult();
    return reader;
}
```

Also, consider adding exception handling to gracefully manage any errors during Parquet reading.



Also applies to: 19-23

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T13:25:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,111 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using Moq;
+using NUnit.Framework;
+using Packata.Core;
+using Packata.Core.PathHandling;
+using Packata.Core.Testing.PathHandling;
+using Packata.ResourceReaders.Tabular;
+
+namespace Packata.ResourceReaders.Testing.Tabular;
+public class ParquetReaderTests
+{
+    
+    private static IEnumerable<IPath> GetPaths()
+    {
+        using var stream = Assembly.GetExecutingAssembly()
+            .GetManifestResourceStream($"{typeof(ResourceTests).Namespace}.Resources.iris.parquet")
+            ?? throw new FileNotFoundException("Resource not found", $"{typeof(ResourceTests).Namespace}.Resources.iris.parquet");
path: src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderTests.cs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Suspicious use of `typeof(ResourceTests)` – possible `NullReferenceException` at runtime**  
`ResourceTests` is not declared in this file and may not exist in the testing assembly, which will make the fully‑qualified resource name incorrect and inevitably return `null`. That will surface as a `FileNotFoundException` (or `NullReferenceException` before that).

```diff
-            .GetManifestResourceStream($"{typeof(ResourceTests).Namespace}.Resources.iris.parquet")
+            .GetManifestResourceStream($"{typeof(ParquetReaderTests).Namespace}.Resources.iris.parquet")
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        using var stream = Assembly.GetExecutingAssembly()
            .GetManifestResourceStream($"{typeof(ParquetReaderTests).Namespace}.Resources.iris.parquet")
            ?? throw new FileNotFoundException("Resource not found", $"{typeof(ParquetReaderTests).Namespace}.Resources.iris.parquet");
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T13:25:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,111 @@
+﻿using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using Moq;
+using NUnit.Framework;
+using Packata.Core;
+using Packata.Core.PathHandling;
+using Packata.Core.Testing.PathHandling;
+using Packata.ResourceReaders.Tabular;
+
+namespace Packata.ResourceReaders.Testing.Tabular;
+public class ParquetReaderTests
+{
+    
+    private static IEnumerable<IPath> GetPaths()
+    {
+        using var stream = Assembly.GetExecutingAssembly()
+            .GetManifestResourceStream($"{typeof(ResourceTests).Namespace}.Resources.iris.parquet")
+            ?? throw new FileNotFoundException("Resource not found", $"{typeof(ResourceTests).Namespace}.Resources.iris.parquet");
+
+        var fileStream = new MemoryStream();
+        stream.CopyTo(fileStream);
+        var fileSystem = new Mock<IFileSystem>();
+        fileSystem.Setup(x => x.Exists("my-resource-path")).Returns(true);
+        fileSystem.Setup(x => x.OpenRead("my-resource-path")).Returns(fileStream);
+        yield return new LocalPath(fileSystem.Object, "", "my-resource-path");
path: src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderTests.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Memory stream returned at EOF & reused across tests – Parquet reader will fail**  
`CopyTo` leaves `fileStream.Position` at the end of the stream and the same disposed instance is later reused by every `OpenRead` call.  
1.  The very first read starts at EOF → Parquet cannot read headers.  
2.  After `Dispose` is called in one test the same disposed `MemoryStream` is returned to the next test → `ObjectDisposedException`.

Fix: return a *fresh* stream on every `OpenRead` and make sure its position is reset.

```diff
-        var fileStream = new MemoryStream();
-        stream.CopyTo(fileStream);
-        var fileSystem = new Mock<IFileSystem>();
-        fileSystem.Setup(x => x.Exists("my-resource-path")).Returns(true);
-        fileSystem.Setup(x => x.OpenRead("my-resource-path")).Returns(fileStream);
+        using var tmp = new MemoryStream();
+        stream.CopyTo(tmp);
+        var parquetBytes = tmp.ToArray();        // cache the bytes once
+
+        var fileSystem = new Mock<IFileSystem>();
+        fileSystem.Setup(fs => fs.Exists("my-resource-path")).Returns(true);
+        fileSystem.Setup(fs => fs.OpenRead("my-resource-path"))
+                  .Returns(() => new MemoryStream(parquetBytes, writable: false)); // fresh stream
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T13:25:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,148 @@
+﻿using Parquet;
+using Parquet.Data;
+using Parquet.Schema;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+
+public class ParquetDataReader : System.Data.IDataReader
+{
+    private readonly List<ParquetReader> _readers = new();
+    private readonly List<Stream> _streams = new(); // to dispose later
+    private readonly List<DataField[]> _schemas = new();
+    private readonly List<object[]> _rows = new();
+
+    private int _fieldCount = -1;
+    private int _currentRowIndex = -1;
+    private DataField[] _dataFields = [];
+
+    private ParquetDataReader() { }
+
+    public static async Task<ParquetDataReader> CreateAsync(IEnumerable<Stream> streams)
+    {
+        var reader = new ParquetDataReader();
+        foreach (var stream in streams)
+        {
+            var parquetReader = await ParquetReader.CreateAsync(stream);
+            var dataFields = parquetReader.Schema.GetDataFields();
+
+            if (reader._fieldCount == -1)
+            {
+                reader._fieldCount = dataFields.Length;
+                reader._dataFields = dataFields;
+            }
+            else
+            {
+                // Ensure schemas are consistent across files
+                if (!reader.AreSchemasEqual(reader._dataFields, dataFields))
+                    throw new InvalidOperationException("Inconsistent schema across parquet files.");
+            }
+
+            reader._streams.Add(stream);
+            reader._readers.Add(parquetReader);
+            await reader.LoadRowsFromReaderAsync(parquetReader);
+        }
+
+        return reader;
+    }
+
+    private bool AreSchemasEqual(DataField[] a, DataField[] b)
+    {
+        if (a.Length != b.Length) return false;
+        for (int i = 0; i < a.Length; i++)
+        {
+            if (a[i].Name != b[i].Name || a[i].ClrType != b[i].ClrType)
+                return false;
+        }
+        return true;
+    }
+
+    private async Task LoadRowsFromReaderAsync(ParquetReader reader)
+    {
+        for (int i = 0; i < reader.RowGroupCount; i++)
+        {
+            using var groupReader = reader.OpenRowGroupReader(i);
+            var columns = new DataColumn[_dataFields.Length];
+
+            for (int j = 0; j < _dataFields.Length; j++)
+            {
+                columns[j] = await groupReader.ReadColumnAsync(_dataFields[j]);
+            }
+
+            int rowCount = columns[0].Data.Length;
+            for (int row = 0; row < rowCount; row++)
+            {
+                var values = new object[_dataFields.Length];
+                for (int col = 0; col < _dataFields.Length; col++)
+                {
+                    values[col] = columns[col].Data.GetValue(row)!;
+                }
+                _rows.Add(values);
+            }
+        }
+    }
+
+    public bool Read()
+    {
+        _currentRowIndex++;
+        return _currentRowIndex < _rows.Count;
+    }
+
+    public int FieldCount => _dataFields.Length;
+
+    public object GetValue(int i) => _rows[_currentRowIndex][i];
+
+    public string GetName(int i) => _dataFields[i].Name;
+    public string GetDataTypeName(int i) => _dataFields[i].SchemaType.ToString();
+    public Type GetFieldType(int i) => _dataFields[i].ClrType;
+    public int GetOrdinal(string name)
+    {
+        for (int i = 0; i < _dataFields.Length; i++)
+        {
+            if (_dataFields[i].Name == name) return i;
+        }
+        return -1;
+    }
+
+    public bool IsDBNull(int i) => GetValue(i) == null;
+
+    public int GetInt32(int i) => (int)GetValue(i);
+    public long GetInt64(int i) => (long)GetValue(i);
+    public string GetString(int i) => (string)GetValue(i);
+    public bool GetBoolean(int i) => (bool)GetValue(i);
+    public DateTime GetDateTime(int i) => (DateTime)GetValue(i);
+
+    // Not implemented or rarely used
+    public object this[int i] => GetValue(i);
+    public object this[string name] => GetValue(GetOrdinal(name));
+    public int Depth => 0;
+    public bool IsClosed => false;
+    public int RecordsAffected => -1;
+    public void Close() => Dispose();
+    public System.Data.DataTable GetSchemaTable() => throw new NotSupportedException();
+    public bool NextResult() => false;
+
+    public void Dispose()
+    {
+        foreach (var reader in _readers)
+            reader.Dispose();
+        foreach (var stream in _streams)
+            stream.Dispose();
+    }
+
+    public long GetBytes(int i, long fieldOffset, byte[]? buffer, int bufferoffset, int length) => throw new NotSupportedException();
+    public long GetChars(int i, long fieldoffset, char[]? buffer, int bufferoffset, int length) => throw new NotSupportedException();
+    public char GetChar(int i) => (char)GetValue(i);
+    public Guid GetGuid(int i) => (Guid)GetValue(i);
+    public short GetInt16(int i) => (short)GetValue(i);
+    public float GetFloat(int i) => (float)GetValue(i);
+    public double GetDouble(int i) => (double)GetValue(i);
+    public decimal GetDecimal(int i) => (decimal)GetValue(i);
+    public string GetDataTypeName(string columnName) => GetDataTypeName(GetOrdinal(columnName));
+    public System.Data.IDataReader GetData(int i) => throw new NotSupportedException();
+    System.Data.DataTable? System.Data.IDataReader.GetSchemaTable() => throw new NotImplementedException();
+    public byte GetByte(int i) => throw new NotImplementedException();
+    System.Data.IDataReader System.Data.IDataRecord.GetData(int i) => throw new NotImplementedException();
+    public int GetValues(object[] values) => throw new NotImplementedException();
path: src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**`GetValues` is required by `IDataRecord` – implement instead of throwing**  
Consumers such as bulk‑copy helpers rely on this method.

```diff
-    public int GetValues(object[] values) => throw new NotImplementedException();
+    public int GetValues(object[] values)
+    {
+        if (_currentRowIndex < 0 || _currentRowIndex >= _rows.Count)
+            return 0;
+        var current = _rows[_currentRowIndex];
+        var len = Math.Min(values.Length, current.Length);
+        Array.Copy(current, values, len);
+        return len;
+    }
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    public int GetValues(object[] values)
    {
        if (_currentRowIndex < 0 || _currentRowIndex >= _rows.Count)
            return 0;
        var current = _rows[_currentRowIndex];
        var len = Math.Min(values.Length, current.Length);
        Array.Copy(current, values, len);
        return len;
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T13:25:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,148 @@
+﻿using Parquet;
+using Parquet.Data;
+using Parquet.Schema;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+
+public class ParquetDataReader : System.Data.IDataReader
path: src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Missing namespace – class is leaking into the global namespace**  
Every other tabular reader class lives under `Packata.ResourceReaders.Tabular`. Omitting the namespace will pollute the root namespace and make the public API inconsistent.

```diff
-﻿using Parquet;
+﻿namespace Packata.ResourceReaders.Tabular;
+
+using Parquet;
```
(Remember to wrap the entire file body in the new namespace block.)



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
namespace Packata.ResourceReaders.Tabular;

using Parquet;
using Parquet.Data;
using Parquet.Schema;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;

public class ParquetDataReader : System.Data.IDataReader
{
    // … rest of the implementation …
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T13:25:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,148 @@
+﻿using Parquet;
+using Parquet.Data;
+using Parquet.Schema;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+
+public class ParquetDataReader : System.Data.IDataReader
+{
+    private readonly List<ParquetReader> _readers = new();
+    private readonly List<Stream> _streams = new(); // to dispose later
+    private readonly List<DataField[]> _schemas = new();
+    private readonly List<object[]> _rows = new();
+
+    private int _fieldCount = -1;
+    private int _currentRowIndex = -1;
+    private DataField[] _dataFields = [];
+
+    private ParquetDataReader() { }
+
+    public static async Task<ParquetDataReader> CreateAsync(IEnumerable<Stream> streams)
+    {
+        var reader = new ParquetDataReader();
+        foreach (var stream in streams)
+        {
+            var parquetReader = await ParquetReader.CreateAsync(stream);
+            var dataFields = parquetReader.Schema.GetDataFields();
+
+            if (reader._fieldCount == -1)
+            {
+                reader._fieldCount = dataFields.Length;
+                reader._dataFields = dataFields;
+            }
+            else
+            {
+                // Ensure schemas are consistent across files
+                if (!reader.AreSchemasEqual(reader._dataFields, dataFields))
+                    throw new InvalidOperationException("Inconsistent schema across parquet files.");
+            }
+
+            reader._streams.Add(stream);
+            reader._readers.Add(parquetReader);
+            await reader.LoadRowsFromReaderAsync(parquetReader);
+        }
+
+        return reader;
+    }
+
+    private bool AreSchemasEqual(DataField[] a, DataField[] b)
+    {
+        if (a.Length != b.Length) return false;
+        for (int i = 0; i < a.Length; i++)
+        {
+            if (a[i].Name != b[i].Name || a[i].ClrType != b[i].ClrType)
+                return false;
+        }
+        return true;
+    }
+
+    private async Task LoadRowsFromReaderAsync(ParquetReader reader)
+    {
+        for (int i = 0; i < reader.RowGroupCount; i++)
+        {
+            using var groupReader = reader.OpenRowGroupReader(i);
+            var columns = new DataColumn[_dataFields.Length];
+
+            for (int j = 0; j < _dataFields.Length; j++)
+            {
+                columns[j] = await groupReader.ReadColumnAsync(_dataFields[j]);
+            }
+
+            int rowCount = columns[0].Data.Length;
+            for (int row = 0; row < rowCount; row++)
+            {
+                var values = new object[_dataFields.Length];
+                for (int col = 0; col < _dataFields.Length; col++)
+                {
+                    values[col] = columns[col].Data.GetValue(row)!;
+                }
+                _rows.Add(values);
+            }
+        }
+    }
+
+    public bool Read()
+    {
+        _currentRowIndex++;
+        return _currentRowIndex < _rows.Count;
+    }
+
+    public int FieldCount => _dataFields.Length;
+
+    public object GetValue(int i) => _rows[_currentRowIndex][i];
+
+    public string GetName(int i) => _dataFields[i].Name;
+    public string GetDataTypeName(int i) => _dataFields[i].SchemaType.ToString();
+    public Type GetFieldType(int i) => _dataFields[i].ClrType;
+    public int GetOrdinal(string name)
+    {
+        for (int i = 0; i < _dataFields.Length; i++)
+        {
+            if (_dataFields[i].Name == name) return i;
+        }
+        return -1;
+    }
+
+    public bool IsDBNull(int i) => GetValue(i) == null;
+
+    public int GetInt32(int i) => (int)GetValue(i);
+    public long GetInt64(int i) => (long)GetValue(i);
+    public string GetString(int i) => (string)GetValue(i);
+    public bool GetBoolean(int i) => (bool)GetValue(i);
+    public DateTime GetDateTime(int i) => (DateTime)GetValue(i);
+
+    // Not implemented or rarely used
+    public object this[int i] => GetValue(i);
+    public object this[string name] => GetValue(GetOrdinal(name));
+    public int Depth => 0;
+    public bool IsClosed => false;
+    public int RecordsAffected => -1;
+    public void Close() => Dispose();
+    public System.Data.DataTable GetSchemaTable() => throw new NotSupportedException();
path: src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`IsClosed` never changes & `Dispose` does not guard multiple calls**  
Calling `Dispose()`/`Close()` sets no flag, so consumers can mistakenly keep using the reader. Tracking this state prevents undefined behaviour.

```diff
-    public bool IsClosed => false;
+    private bool _isClosed;
+    public bool IsClosed => _isClosed;
+
+    public void Close() => Dispose();
+
     public void Dispose()
     {
-        foreach (var reader in _readers)
-            reader.Dispose();
-        foreach (var stream in _streams)
-            stream.Dispose();
+        if (_isClosed) return;
+        foreach (var reader in _readers) reader.Dispose();
+        foreach (var stream in _streams) stream.Dispose();
+        _isClosed = true;
     }
```



> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T13:25:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,169 @@
+﻿using Parquet;
+using Parquet.Data;
+using Parquet.Schema;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Packata.ResourceReaders.Tabular;
+public class ParquetDataReader : System.Data.IDataReader
+{
+    private readonly List<Parquet.ParquetReader> _readers = new();
+    private readonly List<Stream> _streams = new(); // to dispose later
+    private readonly List<DataField[]> _schemas = new();
+    private readonly List<object[]> _rows = new();
+
+    private int _fieldCount = -1;
+    private int _currentRowIndex = -1;
+    private DataField[] _dataFields = [];
+
+    private ParquetDataReader() { }
+
+    public static async Task<ParquetDataReader> CreateAsync(IEnumerable<Stream> streams)
+    {
+        var reader = new ParquetDataReader();
+        foreach (var stream in streams)
+        {
+            var parquetReader = await Parquet.ParquetReader.CreateAsync(stream);
+            var dataFields = parquetReader.Schema.GetDataFields();
+
+            if (reader._fieldCount == -1)
+            {
+                reader._fieldCount = dataFields.Length;
+                reader._dataFields = dataFields;
+            }
+            else
+            {
+                // Ensure schemas are consistent across files
+                if (!reader.AreSchemasEqual(reader._dataFields, dataFields))
+                    throw new InvalidOperationException("Inconsistent schema across parquet files.");
+            }
+
+            reader._streams.Add(stream);
+            reader._readers.Add(parquetReader);
+            await reader.LoadRowsFromReaderAsync(parquetReader);
+        }
+
+        return reader;
+    }
+
+    private bool AreSchemasEqual(DataField[] a, DataField[] b)
+    {
+        if (a.Length != b.Length) return false;
+        for (int i = 0; i < a.Length; i++)
+        {
+            if (a[i].Name != b[i].Name || a[i].ClrType != b[i].ClrType)
+                return false;
+        }
+        return true;
+    }
+
+    private async Task LoadRowsFromReaderAsync(Parquet.ParquetReader reader)
+    {
+        for (int i = 0; i < reader.RowGroupCount; i++)
+        {
+            using var groupReader = reader.OpenRowGroupReader(i);
+            var columns = new DataColumn[_dataFields.Length];
+
+            for (int j = 0; j < _dataFields.Length; j++)
+            {
+                columns[j] = await groupReader.ReadColumnAsync(_dataFields[j]);
+            }
+
+            int rowCount = columns[0].Data.Length;
+            for (int row = 0; row < rowCount; row++)
+            {
+                var values = new object[_dataFields.Length];
+                for (int col = 0; col < _dataFields.Length; col++)
+                {
+                    values[col] = columns[col].Data.GetValue(row)!;
+                }
+                _rows.Add(values);
+            }
+        }
+    }
+
+    public bool Read()
+    {
+        _currentRowIndex++;
+        return _currentRowIndex < _rows.Count;
+    }
+
+    public int FieldCount => _dataFields.Length;
+
+    public object GetValue(int i) => _rows[_currentRowIndex][i];
+
+    public string GetName(int i) => _dataFields[i].Name;
+    public string GetDataTypeName(int i) => _dataFields[i].SchemaType.ToString();
+    public Type GetFieldType(int i) => _dataFields[i].ClrType;
+    public int GetOrdinal(string name)
+    {
+        for (int i = 0; i < _dataFields.Length; i++)
+        {
+            if (_dataFields[i].Name == name) return i;
+        }
+        return -1;
+    }
+
+    public bool IsDBNull(int i) => GetValue(i) == null;
+
+    public int GetInt32(int i) => (int)GetValue(i);
+    public long GetInt64(int i) => (long)GetValue(i);
+    public string GetString(int i) => (string)GetValue(i);
+    public bool GetBoolean(int i) => (bool)GetValue(i);
+    public DateTime GetDateTime(int i) => (DateTime)GetValue(i);
+
path: src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add type safety to Get* methods**

The Get* methods perform direct casts which will throw InvalidCastException if the underlying type doesn't match. Consider adding type checking or using conversion methods to handle common type mismatches.


For example:

```diff
-public int GetInt32(int i) => (int)GetValue(i);
+public int GetInt32(int i)
+{
+    var value = GetValue(i);
+    if (value == null)
+        throw new InvalidCastException("Cannot convert null to Int32");
+    if (value is int intValue)
+        return intValue;
+    return Convert.ToInt32(value);
+}
```

Apply similar patterns to the other Get* methods.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T13:50:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,169 @@
+﻿using Parquet;
+using Parquet.Data;
+using Parquet.Schema;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Packata.ResourceReaders.Tabular;
+public class ParquetDataReader : System.Data.IDataReader
+{
+    private readonly List<Parquet.ParquetReader> _readers = new();
+    private readonly List<Stream> _streams = new(); // to dispose later
+    private readonly List<DataField[]> _schemas = new();
+    private readonly List<object[]> _rows = new();
+
+    private int _fieldCount = -1;
+    private int _currentRowIndex = -1;
+    private DataField[] _dataFields = [];
+
+    private ParquetDataReader() { }
+
+    public static async Task<ParquetDataReader> CreateAsync(IEnumerable<Stream> streams)
+    {
+        var reader = new ParquetDataReader();
+        foreach (var stream in streams)
+        {
+            var parquetReader = await Parquet.ParquetReader.CreateAsync(stream);
+            var dataFields = parquetReader.Schema.GetDataFields();
+
+            if (reader._fieldCount == -1)
+            {
+                reader._fieldCount = dataFields.Length;
+                reader._dataFields = dataFields;
+            }
+            else
+            {
+                // Ensure schemas are consistent across files
+                if (!reader.AreSchemasEqual(reader._dataFields, dataFields))
+                    throw new InvalidOperationException("Inconsistent schema across parquet files.");
+            }
+
+            reader._streams.Add(stream);
+            reader._readers.Add(parquetReader);
+            await reader.LoadRowsFromReaderAsync(parquetReader);
+        }
+
+        return reader;
+    }
+
+    private bool AreSchemasEqual(DataField[] a, DataField[] b)
+    {
+        if (a.Length != b.Length) return false;
+        for (int i = 0; i < a.Length; i++)
+        {
+            if (a[i].Name != b[i].Name || a[i].ClrType != b[i].ClrType)
+                return false;
+        }
+        return true;
+    }
+
+    private async Task LoadRowsFromReaderAsync(Parquet.ParquetReader reader)
+    {
+        for (int i = 0; i < reader.RowGroupCount; i++)
+        {
+            using var groupReader = reader.OpenRowGroupReader(i);
+            var columns = new DataColumn[_dataFields.Length];
+
+            for (int j = 0; j < _dataFields.Length; j++)
+            {
+                columns[j] = await groupReader.ReadColumnAsync(_dataFields[j]);
+            }
+
+            int rowCount = columns[0].Data.Length;
+            for (int row = 0; row < rowCount; row++)
+            {
+                var values = new object[_dataFields.Length];
+                for (int col = 0; col < _dataFields.Length; col++)
+                {
+                    values[col] = columns[col].Data.GetValue(row)!;
+                }
+                _rows.Add(values);
+            }
+        }
+    }
+
+    public bool Read()
+    {
+        _currentRowIndex++;
+        return _currentRowIndex < _rows.Count;
+    }
+
+    public int FieldCount => _dataFields.Length;
+
+    public object GetValue(int i) => _rows[_currentRowIndex][i];
+
+    public string GetName(int i) => _dataFields[i].Name;
+    public string GetDataTypeName(int i) => _dataFields[i].SchemaType.ToString();
+    public Type GetFieldType(int i) => _dataFields[i].ClrType;
+    public int GetOrdinal(string name)
+    {
+        for (int i = 0; i < _dataFields.Length; i++)
+        {
+            if (_dataFields[i].Name == name) return i;
+        }
+        return -1;
+    }
+
+    public bool IsDBNull(int i) => GetValue(i) == null;
path: src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**IsDBNull implementation could throw NullReferenceException**

The implementation of `IsDBNull` compares the value to null, but doesn't check if the current row index is valid, which could lead to exceptions.


```diff
-public bool IsDBNull(int i) => GetValue(i) == null;
+public bool IsDBNull(int i)
+{
+    if (_currentRowIndex < 0 || _currentRowIndex >= _rows.Count)
+        throw new InvalidOperationException("No current row");
+    if (i < 0 || i >= _dataFields.Length)
+        throw new IndexOutOfRangeException($"Column index {i} is out of range");
+    return _rows[_currentRowIndex][i] == null;
+}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    public bool IsDBNull(int i)
    {
        if (_currentRowIndex < 0 || _currentRowIndex >= _rows.Count)
            throw new InvalidOperationException("No current row");
        if (i < 0 || i >= _dataFields.Length)
            throw new IndexOutOfRangeException($"Column index {i} is out of range");
        return _rows[_currentRowIndex][i] == null;
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T13:50:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,169 @@
+﻿using Parquet;
+using Parquet.Data;
+using Parquet.Schema;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Packata.ResourceReaders.Tabular;
+public class ParquetDataReader : System.Data.IDataReader
+{
+    private readonly List<Parquet.ParquetReader> _readers = new();
+    private readonly List<Stream> _streams = new(); // to dispose later
+    private readonly List<DataField[]> _schemas = new();
+    private readonly List<object[]> _rows = new();
+
+    private int _fieldCount = -1;
+    private int _currentRowIndex = -1;
+    private DataField[] _dataFields = [];
+
+    private ParquetDataReader() { }
+
+    public static async Task<ParquetDataReader> CreateAsync(IEnumerable<Stream> streams)
+    {
+        var reader = new ParquetDataReader();
+        foreach (var stream in streams)
+        {
+            var parquetReader = await Parquet.ParquetReader.CreateAsync(stream);
+            var dataFields = parquetReader.Schema.GetDataFields();
+
+            if (reader._fieldCount == -1)
+            {
+                reader._fieldCount = dataFields.Length;
+                reader._dataFields = dataFields;
+            }
+            else
+            {
+                // Ensure schemas are consistent across files
+                if (!reader.AreSchemasEqual(reader._dataFields, dataFields))
+                    throw new InvalidOperationException("Inconsistent schema across parquet files.");
+            }
+
+            reader._streams.Add(stream);
+            reader._readers.Add(parquetReader);
+            await reader.LoadRowsFromReaderAsync(parquetReader);
+        }
+
+        return reader;
+    }
path: src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Improve error handling in CreateAsync method**

The `CreateAsync` method has several potential edge cases that aren't being handled:

1. Empty input collection of streams
2. Stream errors or invalid Parquet format
3. No explicit propagation of exceptions from Parquet.ParquetReader.CreateAsync


Add validation and improve error handling:

```diff
 public static async Task<ParquetDataReader> CreateAsync(IEnumerable<Stream> streams)
 {
+    if (streams == null)
+        throw new ArgumentNullException(nameof(streams));
+        
     var reader = new ParquetDataReader();
+    var streamsList = streams.ToList();
+    
+    if (streamsList.Count == 0)
+        throw new ArgumentException("At least one stream must be provided", nameof(streams));
         
-    foreach (var stream in streams)
+    foreach (var stream in streamsList)
     {
         var parquetReader = await Parquet.ParquetReader.CreateAsync(stream);
         var dataFields = parquetReader.Schema.GetDataFields();
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    public static async Task<ParquetDataReader> CreateAsync(IEnumerable<Stream> streams)
    {
        if (streams == null)
            throw new ArgumentNullException(nameof(streams));

        var reader = new ParquetDataReader();
        var streamsList = streams.ToList();

        if (streamsList.Count == 0)
            throw new ArgumentException("At least one stream must be provided", nameof(streams));

        foreach (var stream in streamsList)
        {
            var parquetReader = await Parquet.ParquetReader.CreateAsync(stream);
            var dataFields = parquetReader.Schema.GetDataFields();

            if (reader._fieldCount == -1)
            {
                reader._fieldCount = dataFields.Length;
                reader._dataFields = dataFields;
            }
            else
            {
                // Ensure schemas are consistent across files
                if (!reader.AreSchemasEqual(reader._dataFields, dataFields))
                    throw new InvalidOperationException("Inconsistent schema across parquet files.");
            }

            reader._streams.Add(stream);
            reader._readers.Add(parquetReader);
            await reader.LoadRowsFromReaderAsync(parquetReader);
        }

        return reader;
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T13:50:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,169 @@
+﻿using Parquet;
+using Parquet.Data;
+using Parquet.Schema;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Packata.ResourceReaders.Tabular;
+public class ParquetDataReader : System.Data.IDataReader
+{
+    private readonly List<Parquet.ParquetReader> _readers = new();
+    private readonly List<Stream> _streams = new(); // to dispose later
+    private readonly List<DataField[]> _schemas = new();
+    private readonly List<object[]> _rows = new();
+
+    private int _fieldCount = -1;
+    private int _currentRowIndex = -1;
+    private DataField[] _dataFields = [];
+
+    private ParquetDataReader() { }
+
+    public static async Task<ParquetDataReader> CreateAsync(IEnumerable<Stream> streams)
+    {
+        var reader = new ParquetDataReader();
+        foreach (var stream in streams)
+        {
+            var parquetReader = await Parquet.ParquetReader.CreateAsync(stream);
+            var dataFields = parquetReader.Schema.GetDataFields();
+
+            if (reader._fieldCount == -1)
+            {
+                reader._fieldCount = dataFields.Length;
+                reader._dataFields = dataFields;
+            }
+            else
+            {
+                // Ensure schemas are consistent across files
+                if (!reader.AreSchemasEqual(reader._dataFields, dataFields))
+                    throw new InvalidOperationException("Inconsistent schema across parquet files.");
+            }
+
+            reader._streams.Add(stream);
+            reader._readers.Add(parquetReader);
+            await reader.LoadRowsFromReaderAsync(parquetReader);
+        }
+
+        return reader;
+    }
+
+    private bool AreSchemasEqual(DataField[] a, DataField[] b)
+    {
+        if (a.Length != b.Length) return false;
+        for (int i = 0; i < a.Length; i++)
+        {
+            if (a[i].Name != b[i].Name || a[i].ClrType != b[i].ClrType)
+                return false;
+        }
+        return true;
+    }
+
+    private async Task LoadRowsFromReaderAsync(Parquet.ParquetReader reader)
+    {
+        for (int i = 0; i < reader.RowGroupCount; i++)
+        {
+            using var groupReader = reader.OpenRowGroupReader(i);
+            var columns = new DataColumn[_dataFields.Length];
+
+            for (int j = 0; j < _dataFields.Length; j++)
+            {
+                columns[j] = await groupReader.ReadColumnAsync(_dataFields[j]);
+            }
+
+            int rowCount = columns[0].Data.Length;
+            for (int row = 0; row < rowCount; row++)
+            {
+                var values = new object[_dataFields.Length];
+                for (int col = 0; col < _dataFields.Length; col++)
+                {
+                    values[col] = columns[col].Data.GetValue(row)!;
+                }
+                _rows.Add(values);
+            }
+        }
+    }
+
+    public bool Read()
+    {
+        _currentRowIndex++;
+        return _currentRowIndex < _rows.Count;
+    }
+
+    public int FieldCount => _dataFields.Length;
+
+    public object GetValue(int i) => _rows[_currentRowIndex][i];
+
+    public string GetName(int i) => _dataFields[i].Name;
+    public string GetDataTypeName(int i) => _dataFields[i].SchemaType.ToString();
+    public Type GetFieldType(int i) => _dataFields[i].ClrType;
+    public int GetOrdinal(string name)
+    {
+        for (int i = 0; i < _dataFields.Length; i++)
+        {
+            if (_dataFields[i].Name == name) return i;
+        }
+        return -1;
+    }
path: src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Improve GetOrdinal error handling**

The `GetOrdinal` method returns -1 when a column name is not found, but the standard behavior for IDataReader is to throw an exception. This can lead to unexpected behavior when used with common data access patterns.


```diff
 public int GetOrdinal(string name)
 {
     for (int i = 0; i < _dataFields.Length; i++)
     {
         if (_dataFields[i].Name == name) return i;
     }
-    return -1;
+    throw new ArgumentException($"Column name '{name}' not found", nameof(name));
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    public int GetOrdinal(string name)
    {
        for (int i = 0; i < _dataFields.Length; i++)
        {
            if (_dataFields[i].Name == name) return i;
        }
        throw new ArgumentException($"Column name '{name}' not found", nameof(name));
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T13:50:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,169 @@
+﻿using Parquet;
+using Parquet.Data;
+using Parquet.Schema;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Packata.ResourceReaders.Tabular;
+public class ParquetDataReader : System.Data.IDataReader
+{
+    private readonly List<Parquet.ParquetReader> _readers = new();
+    private readonly List<Stream> _streams = new(); // to dispose later
+    private readonly List<DataField[]> _schemas = new();
+    private readonly List<object[]> _rows = new();
+
+    private int _fieldCount = -1;
+    private int _currentRowIndex = -1;
+    private DataField[] _dataFields = [];
+
+    private ParquetDataReader() { }
+
+    public static async Task<ParquetDataReader> CreateAsync(IEnumerable<Stream> streams)
+    {
+        var reader = new ParquetDataReader();
+        foreach (var stream in streams)
+        {
+            var parquetReader = await Parquet.ParquetReader.CreateAsync(stream);
+            var dataFields = parquetReader.Schema.GetDataFields();
+
+            if (reader._fieldCount == -1)
+            {
+                reader._fieldCount = dataFields.Length;
+                reader._dataFields = dataFields;
+            }
+            else
+            {
+                // Ensure schemas are consistent across files
+                if (!reader.AreSchemasEqual(reader._dataFields, dataFields))
+                    throw new InvalidOperationException("Inconsistent schema across parquet files.");
+            }
+
+            reader._streams.Add(stream);
+            reader._readers.Add(parquetReader);
+            await reader.LoadRowsFromReaderAsync(parquetReader);
+        }
+
+        return reader;
+    }
+
+    private bool AreSchemasEqual(DataField[] a, DataField[] b)
+    {
+        if (a.Length != b.Length) return false;
+        for (int i = 0; i < a.Length; i++)
+        {
+            if (a[i].Name != b[i].Name || a[i].ClrType != b[i].ClrType)
+                return false;
+        }
+        return true;
+    }
+
+    private async Task LoadRowsFromReaderAsync(Parquet.ParquetReader reader)
+    {
+        for (int i = 0; i < reader.RowGroupCount; i++)
+        {
+            using var groupReader = reader.OpenRowGroupReader(i);
+            var columns = new DataColumn[_dataFields.Length];
+
+            for (int j = 0; j < _dataFields.Length; j++)
+            {
+                columns[j] = await groupReader.ReadColumnAsync(_dataFields[j]);
+            }
+
+            int rowCount = columns[0].Data.Length;
+            for (int row = 0; row < rowCount; row++)
+            {
+                var values = new object[_dataFields.Length];
+                for (int col = 0; col < _dataFields.Length; col++)
+                {
+                    values[col] = columns[col].Data.GetValue(row)!;
+                }
+                _rows.Add(values);
+            }
+        }
+    }
+
+    public bool Read()
+    {
+        _currentRowIndex++;
+        return _currentRowIndex < _rows.Count;
+    }
+
+    public int FieldCount => _dataFields.Length;
+
+    public object GetValue(int i) => _rows[_currentRowIndex][i];
+
+    public string GetName(int i) => _dataFields[i].Name;
+    public string GetDataTypeName(int i) => _dataFields[i].SchemaType.ToString();
+    public Type GetFieldType(int i) => _dataFields[i].ClrType;
+    public int GetOrdinal(string name)
+    {
+        for (int i = 0; i < _dataFields.Length; i++)
+        {
+            if (_dataFields[i].Name == name) return i;
+        }
+        return -1;
+    }
+
+    public bool IsDBNull(int i) => GetValue(i) == null;
+
+    public int GetInt32(int i) => (int)GetValue(i);
+    public long GetInt64(int i) => (long)GetValue(i);
+    public string GetString(int i) => (string)GetValue(i);
+    public bool GetBoolean(int i) => (bool)GetValue(i);
+    public DateTime GetDateTime(int i) => (DateTime)GetValue(i);
+
+    // Not implemented or rarely used
+    public object this[int i] => GetValue(i);
+    public object this[string name] => GetValue(GetOrdinal(name));
+    public int Depth => 0;
+    public bool IsClosed => _disposed;
+    public int RecordsAffected => -1;
+    public void Close() => Dispose();
+    public System.Data.DataTable GetSchemaTable() => throw new NotSupportedException();
path: src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Inconsistent implementation of GetSchemaTable**

There are two implementations of `GetSchemaTable` in the file (lines 123 and 157), one throwing NotSupportedException and the other throwing NotImplementedException.


Choose one implementation and make it consistent:

```diff
-public System.Data.DataTable GetSchemaTable() => throw new NotSupportedException();
+public System.Data.DataTable? GetSchemaTable() => null; // or implement a proper schema table
```

And remove the duplicate implementation at line 157.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T13:50:42Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-20T13:22:39Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This change introduces Parquet file support to the resource reading infrastructure. It adds a new `ParquetDataReader` class that implements `IDataReader` for Parquet files, along with supporting classes: `ParquetReader`, `ParquetReaderBuilder`, and `ParquetReaderWrapper`. The `TabularReaderFactory` is updated to recognize Parquet formats and instantiate the appropriate reader. Unit tests are added to verify Parquet reading functionality and ensure correct integration with the factory and builder patterns. The project file is updated to embed a sample Parquet file for testing purposes.

## Changes

| File(s)                                                                                           | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
|-------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs`                                        | New class `ParquetDataReader` implementing `IDataReader` for reading and exposing data from one or more Parquet files, supporting schema validation, row loading, and standard data reader operations.                                                                                                                                                                                                                                                                                                                                                                                                                |
| `src/Packata.ResourceReaders/Tabular/ParquetReader.cs`,<br>`src/Packata.ResourceReaders/Tabular/ParquetReaderWrapper.cs` | Added `ParquetReader` (an `IResourceReader` implementation) and `ParquetReaderWrapper` (handles conversion from Parquet streams to `IDataReader`). These classes adapt resource paths to streams and wrap Parquet reading logic.                                                                                                                                                                                                                                                                                                                                                 |
| `src/Packata.ResourceReaders/Tabular/ParquetReaderBuilder.cs`                                     | Added `ParquetReaderBuilder` class implementing `IResourceReaderBuilder`, encapsulating construction logic for Parquet readers.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `src/Packata.ResourceReaders/TabularReaderFactory.cs`                                             | Added support for the "parquet" reader type. Updated heuristic logic to recognize Parquet formats and instantiate the correct reader. Registered `ParquetReaderBuilder` in the factory.                                                                                                                                                                                                                                                                                                                                                                                         |
| `src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderTests.cs`,<br>`src/Packata.ResourceReaders.Testing/Tabular/ParquetReaderBuilderTests.cs` | Added comprehensive unit tests for Parquet reading, covering successful reads, error conditions, and integration with builder and factory patterns.                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `src/Packata.ResourceReaders.Testing/TabularReaderFactoryTests.cs`                                | Added parameterized tests to verify correct reader instantiation for various dialect types and formats, including Parquet.                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `src/Packata.ResourceReaders.Testing/Packata.ResourceReaders.Testing.csproj`                      | Embedded `Resources/iris.parquet` as a resource for use in tests, mirroring handling of other test files.                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `src/Packata.ResourceReaders/Inference/MediaTypeBasedDialectInference.cs`                         | Refactored conditional logic for media type inference; no functional change, only restructuring for clarity.                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant TabularReaderFactory
    participant ParquetReaderBuilder
    participant ParquetReader
    participant ParquetReaderWrapper
    participant ParquetDataReader

    Client->>TabularReaderFactory: Create(Resource with format "parquet")
    TabularReaderFactory->>ParquetReaderBuilder: Build()
    ParquetReaderBuilder->>ParquetReader: (constructor)
    TabularReaderFactory-->>Client: ParquetReader

    Client->>ParquetReader: ToDataReader(Resource)
    ParquetReader->>ParquetReaderWrapper: ToDataReader(Stream(s))
    ParquetReaderWrapper->>ParquetDataReader: CreateAsync(Stream(s))
    ParquetDataReader-->>ParquetReaderWrapper: ParquetDataReader
    ParquetReaderWrapper-->>ParquetReader: IDataReader
    ParquetReader-->>Client: IDataReader
```

## Assessment against linked issues

| Objective                                                                                                       | Addressed | Explanation                                                                                 |
|-----------------------------------------------------------------------------------------------------------------|-----------|---------------------------------------------------------------------------------------------|
| Provide a wrapper around Parquet.Net to expose an IDataReader interface (#149)                                  | ✅        |                                                                                             |
| Ensure Parquet files can be read and integrated into the resource reading infrastructure (#149)                  | ✅        |                                                                                             |

## Suggested labels

`new-feature`

## Poem

> In the warren of code, a new path appears,  
> Parquet files now hop in, greeted with cheers!  
> With readers and builders, and tests all in tow,  
> Data flows freely, in neat bunny rows.  
> 🐇✨  
> From streams to the table, the journey is clear—  
> This rabbit says, “Parquet, you’re welcome here!”

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2eabc167eefea440b79abaa5acea19e741e277f6 and 6c4bbebcee21f0ff0f5f34331a6fbfb4388471ec.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* src/Packata.ResourceReaders/Tabular/ParquetDataReader.cs

</details>

</details>
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Seddryck/Packata&utm_content=151):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov-commenter
created_at: 2025-04-20T13:25:48Z
body: ## [Codecov](https://app.codecov.io/gh/Seddryck/Packata/pull/151?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier) Report
Attention: Patch coverage is `70.00000%` with `42 lines` in your changes missing coverage. Please review.
> Project coverage is 79.40%. Comparing base [(`721f0ad`)](https://app.codecov.io/gh/Seddryck/Packata/commit/721f0ad9a13fbed93fc5e20eb304fa8f75b7db5a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier) to head [(`6c4bbeb`)](https://app.codecov.io/gh/Seddryck/Packata/commit/6c4bbebcee21f0ff0f5f34331a6fbfb4388471ec?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier).

| [Files with missing lines](https://app.codecov.io/gh/Seddryck/Packata/pull/151?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier) | Patch % | Lines |
|---|---|---|
| [...ckata.ResourceReaders/Tabular/ParquetDataReader.cs](https://app.codecov.io/gh/Seddryck/Packata/pull/151?src=pr&el=tree&filepath=src%2FPackata.ResourceReaders%2FTabular%2FParquetDataReader.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier#diff-c3JjL1BhY2thdGEuUmVzb3VyY2VSZWFkZXJzL1RhYnVsYXIvUGFycXVldERhdGFSZWFkZXIuY3M=) | 58.33% | [35 Missing and 5 partials :warning: ](https://app.codecov.io/gh/Seddryck/Packata/pull/151?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier) |
| [...rc/Packata.ResourceReaders/TabularReaderFactory.cs](https://app.codecov.io/gh/Seddryck/Packata/pull/151?src=pr&el=tree&filepath=src%2FPackata.ResourceReaders%2FTabularReaderFactory.cs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier#diff-c3JjL1BhY2thdGEuUmVzb3VyY2VSZWFkZXJzL1RhYnVsYXJSZWFkZXJGYWN0b3J5LmNz) | 87.50% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/Seddryck/Packata/pull/151?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #151      +/-   ##
==========================================
- Coverage   80.51%   79.40%   -1.11%     
==========================================
  Files          84       88       +4     
  Lines        1324     1452     +128     
  Branches      220      242      +22     
==========================================
+ Hits         1066     1153      +87     
- Misses        189      225      +36     
- Partials       69       74       +5     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/Seddryck/Packata/pull/151?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=C%C3%A9dric+L.+Charlier).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-04-20T14:04:07Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=Seddryck_Packata&pullRequest=151) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [9 New issues](https://sonarcloud.io/project/issues?id=Seddryck_Packata&pullRequest=151&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=Seddryck_Packata&pullRequest=151&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=Seddryck_Packata&pullRequest=151&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=Seddryck_Packata&pullRequest=151&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=Seddryck_Packata&pullRequest=151&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Seddryck_Packata&pullRequest=151)


author_association: NONE