Skip to content

fix: preserve whitespace-only cells in the XLS reader#177

Merged
DaveRMaltby merged 2 commits into
mainfrom
investigate/preserve-spaces-in-xls
Jul 3, 2026
Merged

fix: preserve whitespace-only cells in the XLS reader#177
DaveRMaltby merged 2 commits into
mainfrom
investigate/preserve-spaces-in-xls

Conversation

@domEnriquez

Copy link
Copy Markdown
Collaborator

Summary

A cell containing only whitespace (e.g. two spaces) was read back as an empty string by the XLS provider.

Root cause: DataFrame.LoadCsv parses CSV via Microsoft.VisualBasic.FileIO.TextFieldParser, whose TrimWhiteSpace property is true, is not configurable through LoadCsv, and trims leading/trailing whitespace from every field — including quoted fields. So a whitespace-only cell reached the reader as "".

Fix: The XLS write path (CsvUtils.EscapeCsvValues) wraps a whitespace-only value in a Unicode private-use guard character (U+E000) so the field's edges are non-whitespace and survive TextFieldParser. CsvVirtualDataTable strips the guard after parsing — and only when the guarded content is itself whitespace-only, so a real value that merely began/ended with the guard character is never corrupted. Any value without the guard (e.g. every CSV-provider value) is returned unchanged, so the CSV provider is unaffected.

Tests

  • Added shared DataReaderTests.Reader_ShouldPreserveWhitespaceOnlyCell<TFileParameter> and a withSpaceCell.xlsx fixture, wired through XlsDataReaderTests (mirrors the existing delegation pattern).
  • Data.Xls.Tests: 35 passed, 5 skipped, 0 failed.
  • Data.Csv.Tests: 129 passed, 0 failed (guard decode is a no-op for CSV values).

🤖 Generated with Claude Code

DataFrame.LoadCsv parses via Microsoft.VisualBasic TextFieldParser, whose
TrimWhiteSpace is true and cannot be configured through LoadCsv, so it strips
whitespace from every field (even quoted). As a result a cell containing only
spaces was read back as an empty string.

The XLS write path (CsvUtils.EscapeCsvValues) now wraps a whitespace-only value in
a Unicode private-use guard character so the field's edges are non-whitespace and
survive parsing. CsvVirtualDataTable strips the guard afterwards, and only when the
guarded content is itself whitespace-only, so real values are never affected.
Values without the guard (e.g. every CSV-provider value) are returned unchanged.

Adds the shared Reader_ShouldPreserveWhitespaceOnlyCell test and a
withSpaceCell.xlsx fixture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@domEnriquez
domEnriquez requested a review from DaveRMaltby as a code owner July 3, 2026 05:29
- Move the guard sentinel to a shared Data.Common CsvWhitespaceGuard class (was a
  public const on the CsvVirtualDataTable paging read class).
- Gate guard stripping behind stripWhitespaceGuard, set only by the XLS reader, so
  other providers never strip a value that merely looks guard-shaped. This makes the
  "never corrupt data I did not encode" property structural rather than probabilistic.
- Parameterize the shared whitespace test by table name; add a focused CSV test proving
  a guard-shaped value is returned unchanged when stripping is disabled.
- Add a mixed numeric+whitespace fixture/test documenting that preserving a whitespace
  cell makes an otherwise-numeric column read as text (XLS read path only).
- Document the whitespace-only scope limitation in CsvUtils.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

Summary

Summary
Generated on: 07/03/2026 - 06:57:04
Coverage date: 07/03/2026 - 06:55:44 - 07/03/2026 - 06:56:58
Parser: MultiReport (8x Cobertura)
Assemblies: 10
Classes: 237
Files: 237
Line coverage: 80.7% (5639 of 6979)
Covered lines: 5639
Uncovered lines: 1340
Coverable lines: 6979
Total lines: 18980
Branch coverage: 66.8% (1717 of 2567)
Covered branches: 1717
Total branches: 2567
Method coverage: Feature is only available for sponsors

Coverage

Data.Common - 77.4%
Name Line Branch
Data.Common 77.4% 66.8%
Data.Common.DataSource.DataSourceEventArgs 0% 0%
Data.Common.DataSource.FileSystemDataSource 88.5% 68.1%
Data.Common.DataSource.StreamedDataSource 89.4% 50%
Data.Common.DataSource.TableStreamedDataSource 80% 50%
Data.Common.Extension.FileConnectionExtensions 90.9% 87.5%
Data.Common.Extension.FileConnectionStringTestsExtensions 91.6% 80%
Data.Common.Extension.FloatingPointDataTypeExtensions 66.6% 60%
Data.Common.Extension.IEnumerableExtensions 0% 0%
Data.Common.Extension.ObjectExtensions 50%
Data.Common.Extension.StringExtensions 0% 0%
Data.Common.Extension.VirtualDataTableExtensions 0% 0%
Data.Common.FileException.ColumnNotFoundException 0%
Data.Common.FileException.InvalidConnectionStringException 100%
Data.Common.FileException.InvalidFileException 0%
Data.Common.FileException.QueryNotSupportedException 0%
Data.Common.FileException.QuerySyntaxException 0%
Data.Common.FileException.TableNotFoundException 0%
Data.Common.FileException.ThrowHelper 60% 100%
Data.Common.FileIO.Delete.FileDeleteWriter 100% 85.7%
Data.Common.FileIO.FileWriter 100% 83.3%
Data.Common.FileIO.Read.FileEnumerator 90.6% 71.4%
Data.Common.FileIO.Read.FileReader 90.8% 84%
Data.Common.FileIO.SchemaAltering.FileAddColumnWriter 95.4% 60%
Data.Common.FileIO.SchemaAltering.FileCreateTableWriter 93.9% 71.4%
Data.Common.FileIO.SchemaAltering.FileDropColumnWriter 94.1% 66.6%
Data.Common.FileIO.SchemaAltering.FileDropTableWriter 77.2% 58.3%
Data.Common.FileIO.Write.FileInsertWriter 85.4% 80.6%
Data.Common.FileIO.Write.FileUpdateWriter 93.1% 68.1%
Data.Common.FileStatements.Field 0%
Data.Common.FileStatements.FileAddColumn 100%
Data.Common.FileStatements.FileAdminStatement`1 84.6% 71.4%
Data.Common.FileStatements.FileCreateDatabase`1 88.2% 80%
Data.Common.FileStatements.FileCreateTable 87.5% 50%
Data.Common.FileStatements.FileDelete 90.9% 66.6%
Data.Common.FileStatements.FileDropColumn 100%
Data.Common.FileStatements.FileDropDatabase`1 87.5% 81.2%
Data.Common.FileStatements.FileDropTable 83.3% 50%
Data.Common.FileStatements.FileInsert 83.3% 60%
Data.Common.FileStatements.FileSelect 85.7% 100%
Data.Common.FileStatements.FileStatement 100% 100%
Data.Common.FileStatements.FileStatementCreator 86.4% 75%
Data.Common.FileStatements.FileUpdate 92.3% 66.6%
Data.Common.Parsing.AdminGrammar 100%
Data.Common.Parsing.SqlGrammar 100%
Data.Common.Utils.BufferedResetStream 82.8% 76.7%
Data.Common.Utils.BuiltinFunctionProvider 53.3% 40%
Data.Common.Utils.ConcatStream 70.9% 61.9%
Data.Common.Utils.ConnectionString.AliasAttribute 100%
Data.Common.Utils.ConnectionString.FileConnectionString 82.4% 69.7%
Data.Common.Utils.ConnectionString.FileConnectionStringBuilder 31.8% 19.2%
Data.Common.Utils.DatabaseConnectionProvider 100%
Data.Common.Utils.LoggerServices 83.3% 46.1%
Data.Common.Utils.Result 87.1% 73.9%
Data.Common.Utils.StreamedTableManager 73.1% 61.7%
Data.Common.Utils.StreamHolder 100%
Data.Common.Utils.SubStream 70.2% 62.5%
Data.Common.Utils.SubStreamReader 100% 80%
Data.Common.Utils.TransactionLevelData 95% 78.5%
System.Data.FileClient.CloseConnectionDataReader 27.4% 66.6%
System.Data.FileClient.FileCommand`1 82% 82.2%
System.Data.FileClient.FileCommandBuilder`1 94.4% 100%
System.Data.FileClient.FileConnection`1 76.9% 84.3%
System.Data.FileClient.FileDataAdapter`1 83.1% 72.6%
System.Data.FileClient.FileDataReader 69.7% 40.4%
System.Data.FileClient.FileParameter`1 53.7% 25%
System.Data.FileClient.FileParameterCollection`1 15.4% 11.5%
System.Data.FileClient.FileTransaction`1 87.2% 62.5%
Data.Csv - 90.9%
Name Line Branch
Data.Csv 90.9% 88%
CsvTransformStream 88.1% 86.1%
Data.Csv.CsvIO.CsvDataSetWriter 87.1% 88.8%
Data.Csv.CsvIO.Delete.CsvDelete 100%
Data.Csv.CsvIO.Read.CsvReader 92.5% 100%
Data.Csv.CsvIO.SchemaAltering.CsvAddColumn 100%
Data.Csv.CsvIO.SchemaAltering.CsvCreateTable 100%
Data.Csv.CsvIO.SchemaAltering.CsvDropColumn 100%
Data.Csv.CsvIO.SchemaAltering.CsvDropTable 0%
Data.Csv.CsvIO.Write.CsvInsert 92.8% 91.6%
Data.Csv.CsvIO.Write.CsvUpdate 100%
Data.Csv.Utils.CsvSeparatorDetector 95.8% 71.4%
Data.Csv.Utils.CsvVirtualDataTable 95.8% 89.5%
Data.Csv.Utils.TypeGuesser 100% 94.4%
System.Data.CsvClient.CsvClientFactory 80%
System.Data.CsvClient.CsvCommand 85.7% 83.3%
System.Data.CsvClient.CsvCommandBuilder 100%
System.Data.CsvClient.CsvConnection 85.1% 75%
System.Data.CsvClient.CsvDataAdapter 94.1% 83.3%
System.Data.CsvClient.CsvDataReader 100%
System.Data.CsvClient.CsvParameter 60%
System.Data.CsvClient.CsvTransaction 100%
Data.Json - 81.5%
Name Line Branch
Data.Json 81.5% 71.6%
Data.Json.Extension.JsonElementExtensions 0% 0%
Data.Json.Extension.JsonValueKindExtensions 0% 0%
Data.Json.JsonException.InvalidJsonFileException 0%
Data.Json.JsonException.ThrowHelper 0% 0%
Data.Json.JsonIO.Delete.JsonDelete 100%
Data.Json.JsonIO.JsonDataSetWriter 87.5% 66%
Data.Json.JsonIO.Read.JsonReader 100% 75%
Data.Json.JsonIO.SchemaAltering.JsonAddColumn 100%
Data.Json.JsonIO.SchemaAltering.JsonCreateTable 100%
Data.Json.JsonIO.SchemaAltering.JsonDropColumn 100%
Data.Json.JsonIO.SchemaAltering.JsonDropTable 100%
Data.Json.JsonIO.Write.JsonInsert 87.5% 83.3%
Data.Json.JsonIO.Write.JsonUpdate 100%
Data.Json.Utils.BOMHandlingStream 80.6% 82.3%
Data.Json.Utils.JsonConnectionStringBuilder 83.3% 50%
Data.Json.Utils.JsonDatabaseStreamSplitter 92.6% 81.8%
Data.Json.Utils.JsonDatabaseVirtualDataSet 81.2% 90%
Data.Json.Utils.JsonVirtualDataTable 89.6% 81.3%
Data.Json.Utils.StreamJsonReader 78.9% 73.2%
System.Data.JsonClient.JsonClientFactory 80%
System.Data.JsonClient.JsonCommand 90.9% 88.8%
System.Data.JsonClient.JsonCommandBuilder 100%
System.Data.JsonClient.JsonConnection 87.5%
System.Data.JsonClient.JsonDataAdapter 72.7% 50%
System.Data.JsonClient.JsonDataReader 100%
System.Data.JsonClient.JsonParameter 66.6%
System.Data.JsonClient.JsonTransaction 100%
Data.Tests.Common - 88.6%
Name Line Branch
Data.Tests.Common 88.6% 71.1%
AutoGeneratedProgram 0%
Data.Tests.Common.AlterTableTests 100% 50%
Data.Tests.Common.AsyncTests 100% 100%
Data.Tests.Common.BufferedResetStreamTests 0% 0%
Data.Tests.Common.ClientFactoryTests 100%
Data.Tests.Common.CommandBuilderTests 100%
Data.Tests.Common.CommandTests 100% 100%
Data.Tests.Common.ConcurrencyTests 86.4% 66.6%
Data.Tests.Common.ConnectionStringBuilderTests 100%
Data.Tests.Common.ConnectionTests 100% 66.6%
Data.Tests.Common.CreateTableTests 100%
Data.Tests.Common.DeleteTests 100%
Data.Tests.Common.Extensions.FileConnectionExtensions 42.8% 33.3%
Data.Tests.Common.FileAsDatabase.DataAdapterTests 100% 80%
Data.Tests.Common.FileAsDatabase.DataReaderTests 100% 83.9%
Data.Tests.Common.FileSystemDataSourceTests 0%
Data.Tests.Common.InsertTests 84.8% 50%
Data.Tests.Common.POCOs.TestRecord 100%
Data.Tests.Common.SelectTests 0%
Data.Tests.Common.TransactionTests 97.5%
Data.Tests.Common.UpdateTests 100%
Data.Tests.Common.Utils.ConcatStreamTests 0%
Data.Tests.Common.Utils.ConnectionStringsBase 94.7%
Data.Tests.Common.Utils.CustomDataSourceFactory 100% 100%
Data.Tests.Common.Utils.DatabaseFullPaths 100% 50%
Data.Tests.Common.Utils.FileUtils 90% 83.3%
Data.Tests.Common.Utils.FormFileUtils 100%
Data.Tests.Common.Utils.GrammarParser 0% 0%
Data.Tests.Common.Utils.NonSeekableStream 0% 0%
Data.Tests.Common.Utils.UnendingStream 58.6% 76.9%
Data.Xls - 66.5%
Name Line Branch
Data.Xls 66.5% 50.6%
Data.Xls.DataSource.XlsStreamedDataSource 100% 100%
Data.Xls.Utils.CellExtensions 88.8% 66.6%
Data.Xls.Utils.CsvUtils 100% 100%
Data.Xls.Utils.TypeGuesser 0% 0%
Data.Xls.Utils.WorkbookExtensions 95.6% 75%
Data.Xls.Utils.XlsDatabaseStreamSplitter 100% 83.3%
Data.Xls.Utils.XlsSheetStream 73% 75%
Data.Xls.XlsIO.Read.XlsReader 63.6% 25%
System.Data.XlsClient.XlsClientFactory 50%
System.Data.XlsClient.XlsCommand 15% 5.5%
System.Data.XlsClient.XlsCommandBuilder 66.6%
System.Data.XlsClient.XlsConnection 86.2% 90%
System.Data.XlsClient.XlsDataReader 100%
System.Data.XlsClient.XlsParameter 0%
Data.Xml - 86.8%
Name Line Branch
Data.Xml 86.8% 80.3%
Data.Xml.XmlIO.Delete.XmlDelete 100%
Data.Xml.XmlIO.Read.XmlReader 97.6% 90%
Data.Xml.XmlIO.SchemaAltering.XmlAddColumn 100%
Data.Xml.XmlIO.SchemaAltering.XmlCreateTable 100%
Data.Xml.XmlIO.SchemaAltering.XmlDropColumn 100%
Data.Xml.XmlIO.SchemaAltering.XmlDropTable 0%
Data.Xml.XmlIO.Write.XmlInsert 73.6% 64.2%
Data.Xml.XmlIO.Write.XmlUpdate 100%
Data.Xml.XmlIO.XmlDataSetWriter 84.6% 100%
System.Data.XmlClient.XmlClientFactory 70%
System.Data.XmlClient.XmlCommand 86.3% 88.8%
System.Data.XmlClient.XmlCommandBuilder 100%
System.Data.XmlClient.XmlConnection 100%
System.Data.XmlClient.XmlDataAdapter 72.7% 50%
System.Data.XmlClient.XmlDataReader 100%
System.Data.XmlClient.XmlParameter 60%
System.Data.XmlClient.XmlTransaction 100%
EFCore.Common - 68.5%
Name Line Branch
EFCore.Common 68.5% 53%
EFCore.Common.Design.Internal.FileDesignTimeServices 100%
EFCore.Common.Infrastructure.Internal.FileOptionsExtensionInfo 92.3% 75%
EFCore.Common.Infrastructure.Internal.FileRelationalConnection 100% 100%
EFCore.Common.Query.Internal.FileMathMethodCallTranslator 8.1% 0%
EFCore.Common.Query.Internal.FileMemberTranslator 10.3% 0%
EFCore.Common.Query.Internal.FileMemberTranslatorProvider 100%
EFCore.Common.Query.Internal.FileMethodCallTranslatorProvider 100%
EFCore.Common.Query.Internal.FileStringMethodCallTranslator 60.9% 55.8%
EFCore.Common.Scaffolding.Internal.FileDatabaseModelFactory 100% 83.3%
EFCore.Common.Scaffolding.Internal.FileScaffoldingModelFactory 0%
EFCore.Common.Storage.Internal.FileDatabaseCreator 47.2% 16.6%
EFCore.Common.Storage.Internal.FileTypeMappingSource 96.6% 75%
EFCore.Common.Update.Internal.FileUpdateSqlGeneratorBase 98% 78.5%
EFCore.Common.Utils.OptionsExtensionsHelper 100% 100%
EFCore.Common.Utils.SharedTypeExtensions 100% 50%
EFCore.Csv.Infrastructure.Internal.FileOptionsExtension 85.1% 60%
EFCore.Csv - 68.2%
Name Line Branch
EFCore.Csv 68.2% 46.2%
EFCore.Csv.Design.Internal.CsvDesignTimeServices 100%
EFCore.Csv.Infrastructure.Internal.CsvOptionsExtension 88.8% 100%
EFCore.Csv.Infrastructure.Internal.CsvOptionsExtensionInfo 75%
EFCore.Csv.Metadata.Conventions.CsvConventionSetBuilder 100%
EFCore.Csv.Query.Internal.CsvQuerySqlGenerator 22.2% 20%
EFCore.Csv.Query.Internal.CsvQuerySqlGeneratorFactory 100%
EFCore.Csv.Scaffolding.Internal.CsvCodeGenerator 100% 50%
EFCore.Csv.Scaffolding.Internal.CsvDatabaseModelFactory 100%
EFCore.Csv.Storage.Internal.CsvDatabaseCreator 0% 0%
EFCore.Csv.Storage.Internal.CsvRelationalConnection 100% 100%
EFCore.Csv.Storage.Internal.CsvTypeMappingSource 64.2% 60.5%
EFCore.Csv.Update.Internal.CsvModificationCommandBatchFactory 100%
EFCore.Csv.Update.Internal.CsvUpdateSqlGenerator 100%
Microsoft.EntityFrameworkCore.CsvDbContextOptionsExtensions 43.3% 35.7%
Microsoft.Extensions.DependencyInjection.CsvServiceCollectionExtensions 96.4%
EFCore.Json - 71.8%
Name Line Branch
EFCore.Json 71.8% 57.5%
EFCore.Json.Design.Internal.JsonDesignTimeServices 100%
EFCore.Json.Infrastructure.Internal.JsonOptionsExtension 90% 100%
EFCore.Json.Infrastructure.Internal.JsonOptionsExtensionInfo 75%
EFCore.Json.Metadata.Conventions.JsonConventionSetBuilder 100%
EFCore.Json.Query.Internal.JsonQuerySqlGenerator 0% 0%
EFCore.Json.Query.Internal.JsonQuerySqlGeneratorFactory 60%
EFCore.Json.Scaffolding.Internal.JsonCodeGenerator 100% 50%
EFCore.Json.Scaffolding.Internal.JsonDatabaseModelFactory 100%
EFCore.Json.Storage.Internal.JsonDatabaseCreator 60% 50%
EFCore.Json.Storage.Internal.JsonRelationalConnection 100% 100%
EFCore.Json.Storage.Internal.JsonTypeMappingSource 92.6% 92.1%
EFCore.Json.Update.Internal.JsonModificationCommandBatchFactory 100%
EFCore.Json.Update.Internal.JsonUpdateSqlGenerator 100%
Microsoft.EntityFrameworkCore.JsonDbContextOptionsExtensions 43.3% 35.7%
Microsoft.Extensions.DependencyInjection.JsonServiceCollectionExtensions 96.4%
EFCore.Xml - 73%
Name Line Branch
EFCore.Xml 73% 51.2%
EFCore.Xml.Design.Internal.XmlDesignTimeServices 100%
EFCore.Xml.Infrastructure.Internal.XmlOptionsExtension 90% 100%
EFCore.Xml.Infrastructure.Internal.XmlOptionsExtensionInfo 75%
EFCore.Xml.Metadata.Conventions.XmlConventionSetBuilder 100%
EFCore.Xml.Query.Internal.XmlQuerySqlGenerator 44.4% 35%
EFCore.Xml.Query.Internal.XmlQuerySqlGeneratorFactory 100%
EFCore.Xml.Scaffolding.Internal.XmlCodeGenerator 100% 50%
EFCore.Xml.Scaffolding.Internal.XmlDatabaseModelFactory 100%
EFCore.Xml.Storage.Internal.XmlDatabaseCreator 60% 50%
EFCore.Xml.Storage.Internal.XmlRelationalConnection 100% 100%
EFCore.Xml.Storage.Internal.XmlTypeMappingSource 64.2% 60.5%
EFCore.Xml.Update.Internal.XmlModificationCommandBatchFactory 100%
EFCore.Xml.Update.Internal.XmlUpdateSqlGenerator 100%
Microsoft.EntityFrameworkCore.XmlDbContextOptionsExtensions 43.3% 35.7%
Microsoft.Extensions.DependencyInjection.XmlServiceCollectionExtensions 96.4%

@DaveRMaltby DaveRMaltby left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dominic walked me through all the changes and they look good. It is unfortunate that we have to use marked in the cell values that only have whitespace, but we cannot avoid the behavior of the DataFrame.

@DaveRMaltby
DaveRMaltby merged commit 0891f64 into main Jul 3, 2026
4 checks passed
@DaveRMaltby
DaveRMaltby deleted the investigate/preserve-spaces-in-xls branch July 3, 2026 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants