Skip to content

Commit d53cd59

Browse files
committed
Release v2.0.0 — typed DataTables, streaming object[], fluent query API
1 parent 40b1324 commit d53cd59

24 files changed

Lines changed: 2642 additions & 117 deletions

CHANGELOG.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Changelog
2+
3+
All notable changes to `JetDatabaseReader` are documented here.
4+
This project follows [Semantic Versioning](https://semver.org/).
5+
6+
---
7+
8+
## [2.0.0] — 2026-03-28
9+
10+
### ⚠️ Breaking Changes
11+
12+
| Area | v1 behaviour | v2 behaviour |
13+
|------|-------------|-------------|
14+
| **Constructor** | `new JetDatabaseReader(path)` | `AccessReader.Open(path)` — factory method required |
15+
| **`ReadTable()`** | Returned `(headers, rows, schema)` tuple | **Renamed** to `ReadTablePreview()` |
16+
| **`ReadTableAsDataTable()`** | Returned `DataTable` with `string` columns | **Renamed** to `ReadTableAsStringDataTable()` |
17+
| **`StreamRows()`** | Returned `IEnumerable<string[]>` | Now returns `IEnumerable<object[]>` with native CLR types |
18+
| **`ReadAllTables()`** | Returned `DataTable` with `string` columns | Now returns `DataTable` with typed CLR columns |
19+
| **`ReadAllTablesAsync()`** | Same string behaviour | Now returns typed CLR columns |
20+
21+
### ✨ New Methods
22+
23+
| Method | Description |
24+
|--------|-------------|
25+
| `ReadTable()` | Primary read method — typed `DataTable` (replaces `ReadTableAsDataTableTyped`) |
26+
| `ReadTableAsync()` | Async typed `DataTable` |
27+
| `StreamRowsAsStrings()` | Compatibility streaming — `IEnumerable<string[]>` |
28+
| `ReadAllTablesAsStrings()` | Bulk read with string columns |
29+
| `ReadAllTablesAsStringsAsync()` | Async bulk read with string columns |
30+
| `TableQuery.Where(Func<object[], bool>)` | Typed row predicate |
31+
| `TableQuery.WhereAsStrings(Func<string[], bool>)` | String row predicate |
32+
| `TableQuery.Execute()` | Returns `IEnumerable<object[]>` |
33+
| `TableQuery.ExecuteAsStrings()` | Returns `IEnumerable<string[]>` |
34+
| `TableQuery.FirstOrDefault()` | Returns first `object[]` or null |
35+
| `TableQuery.FirstOrDefaultAsStrings()` | Returns first `string[]` or null |
36+
| `TableQuery.Count()` / `CountAsStrings()` | Count per chain |
37+
| `GetColumnMetadata()` | Rich per-column metadata with CLR type |
38+
| `GetStatistics()` / `GetStatisticsAsync()` | Database-level statistics + cache hit rate |
39+
40+
### 🔧 Improvements
41+
42+
- **`FileShare` default changed to `FileShare.Read`** — other processes may read but not write while the database is open; pass `FileShare.ReadWrite` explicitly when Microsoft Access has the file open
43+
- LRU page cache (256-page default, ~1 MB for Jet4 pages)
44+
- Parallel page reads option (`ParallelPageReadsEnabled`)
45+
- `AccessReaderOptions` configuration object (`PageCacheSize`, `FileAccess`, `FileShare`, `ValidateOnOpen`)
46+
- `DatabaseStatistics` and `ColumnMetadata` types
47+
- `IAccessReader` interface — fully testable and mockable
48+
- Full XML documentation on all public members
49+
50+
### 📦 Migration Guide
51+
52+
```csharp
53+
// ── Open ────────────────────────────────────────────────────────────
54+
// v1
55+
using var r = new JetDatabaseReader("db.mdb");
56+
// v2
57+
using var r = AccessReader.Open("db.mdb");
58+
59+
// ── Read typed DataTable ─────────────────────────────────────────────
60+
// v1 — no equivalent (all columns were strings)
61+
// v2
62+
DataTable dt = r.ReadTable("Orders");
63+
int id = (int)dt.Rows[0]["OrderID"];
64+
65+
// ── Read string DataTable (compatibility) ────────────────────────────
66+
// v1
67+
DataTable dt = r.ReadTableAsDataTable("Orders");
68+
// v2
69+
DataTable dt = r.ReadTableAsStringDataTable("Orders");
70+
71+
// ── Preview with schema ──────────────────────────────────────────────
72+
// v1
73+
var (h, rows, schema) = r.ReadTable("Orders", maxRows: 10);
74+
// v2
75+
var (h, rows, schema) = r.ReadTablePreview("Orders", maxRows: 10);
76+
77+
// ── Stream rows (typed) ──────────────────────────────────────────────
78+
// v1
79+
foreach (string[] row in r.StreamRows("Orders")) { ... }
80+
// v2 — typed
81+
foreach (object[] row in r.StreamRows("Orders")) { int id = (int)row[0]; }
82+
// v2 — compat
83+
foreach (string[] row in r.StreamRowsAsStrings("Orders")) { ... }
84+
85+
// ── Bulk read ────────────────────────────────────────────────────────
86+
// v1 — returned string columns
87+
var tables = r.ReadAllTables();
88+
// v2 — returns typed columns
89+
var tables = r.ReadAllTables();
90+
// v2 — compat strings
91+
var tables = r.ReadAllTablesAsStrings();
92+
```
93+
94+
---
95+
96+
## [1.0.0] — 2026-03-27
97+
98+
- Pure-managed JET3/Jet4 reader (no OleDB/ODBC/ACE)
99+
- All standard column types (Text, Integer, Currency, GUID, MEMO, OLE)
100+
- Multi-page LVAL chain support
101+
- OLE Object magic-byte detection (JPEG, PNG, PDF, ZIP, DOC, RTF)
102+
- Compressed Unicode (Jet4) decoding
103+
- Code page auto-detection (non-Western text)
104+
- Encryption detection (`NotSupportedException`)
105+
- Streaming API (`StreamRows`)
106+
- `IProgress<int>` callbacks
107+
- 256-page LRU cache
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
using System.Collections.Generic;
2+
using System.Data;
3+
using System.Threading.Tasks;
4+
using FluentAssertions;
5+
using Xunit;
6+
7+
namespace JetDatabaseReader.Tests
8+
{
9+
/// <summary>
10+
/// Tests for all async methods:
11+
/// ListTablesAsync, ReadTableAsync, GetStatisticsAsync, ReadAllTablesAsync, ReadAllTablesAsStringsAsync.
12+
/// </summary>
13+
public class AccessReaderAsyncTests
14+
{
15+
// ── ListTablesAsync ───────────────────────────────────────────────
16+
17+
[Theory]
18+
[MemberData(nameof(TestDatabases.All), MemberType = typeof(TestDatabases))]
19+
public async Task ListTablesAsync_ReturnsNonEmptyList(string path)
20+
{
21+
using var reader = TestDatabases.Open(path);
22+
23+
List<string> tables = await reader.ListTablesAsync();
24+
25+
tables.Should().NotBeNullOrEmpty();
26+
}
27+
28+
[Theory]
29+
[MemberData(nameof(TestDatabases.All), MemberType = typeof(TestDatabases))]
30+
public async Task ListTablesAsync_MatchesSyncListTables(string path)
31+
{
32+
using var reader = TestDatabases.Open(path);
33+
34+
List<string> sync = reader.ListTables();
35+
List<string> async_ = await reader.ListTablesAsync();
36+
37+
async_.Should().BeEquivalentTo(sync);
38+
}
39+
40+
// ── ReadTableAsync ────────────────────────────────────────────────
41+
42+
[Theory]
43+
[MemberData(nameof(TestDatabases.All), MemberType = typeof(TestDatabases))]
44+
public async Task ReadTableAsync_ReturnsNonNullDataTable(string path)
45+
{
46+
using var reader = TestDatabases.Open(path);
47+
string table = reader.ListTables()[0];
48+
49+
DataTable dt = await reader.ReadTableAsync(table);
50+
51+
dt.Should().NotBeNull();
52+
}
53+
54+
[Theory]
55+
[MemberData(nameof(TestDatabases.All), MemberType = typeof(TestDatabases))]
56+
public async Task ReadTableAsync_ColumnTypes_AreTyped(string path)
57+
{
58+
using var reader = TestDatabases.Open(path);
59+
string table = reader.ListTables()[0];
60+
var meta = reader.GetColumnMetadata(table);
61+
62+
DataTable dt = await reader.ReadTableAsync(table);
63+
64+
for (int i = 0; i < meta.Count; i++)
65+
dt.Columns[i].DataType.Should().Be(meta[i].ClrType);
66+
}
67+
68+
[Theory]
69+
[MemberData(nameof(TestDatabases.All), MemberType = typeof(TestDatabases))]
70+
public async Task ReadTableAsync_RowCount_MatchesSyncReadTable(string path)
71+
{
72+
using var reader = TestDatabases.Open(path);
73+
string table = reader.ListTables()[0];
74+
75+
DataTable syncDt = reader.ReadTable(table);
76+
DataTable asyncDt = await reader.ReadTableAsync(table);
77+
78+
asyncDt.Rows.Count.Should().Be(syncDt.Rows.Count);
79+
}
80+
81+
// ── GetStatisticsAsync ────────────────────────────────────────────
82+
83+
[Theory]
84+
[MemberData(nameof(TestDatabases.All), MemberType = typeof(TestDatabases))]
85+
public async Task GetStatisticsAsync_MatchesSyncGetStatistics(string path)
86+
{
87+
using var reader = TestDatabases.Open(path);
88+
89+
DatabaseStatistics sync = reader.GetStatistics();
90+
DatabaseStatistics async_ = await reader.GetStatisticsAsync();
91+
92+
async_.TotalPages.Should().Be(sync.TotalPages);
93+
async_.TableCount.Should().Be(sync.TableCount);
94+
async_.Version.Should().Be(sync.Version);
95+
}
96+
97+
// ── ReadAllTablesAsync ────────────────────────────────────────────
98+
99+
[Theory]
100+
[MemberData(nameof(TestDatabases.Small), MemberType = typeof(TestDatabases))]
101+
public async Task ReadAllTablesAsync_ContainsAllTableNames(string path)
102+
{
103+
using var reader = TestDatabases.Open(path);
104+
List<string> expected = reader.ListTables();
105+
106+
Dictionary<string, DataTable> all = await reader.ReadAllTablesAsync();
107+
108+
all.Keys.Should().BeEquivalentTo(expected);
109+
}
110+
111+
[Theory]
112+
[MemberData(nameof(TestDatabases.Small), MemberType = typeof(TestDatabases))]
113+
public async Task ReadAllTablesAsync_RowCounts_MatchSyncReadAllTables(string path)
114+
{
115+
using var reader = TestDatabases.Open(path);
116+
117+
Dictionary<string, DataTable> sync = reader.ReadAllTables();
118+
Dictionary<string, DataTable> async_ = await reader.ReadAllTablesAsync();
119+
120+
foreach (string name in sync.Keys)
121+
async_[name].Rows.Count.Should().Be(sync[name].Rows.Count,
122+
because: $"table '{name}' row count should match");
123+
}
124+
125+
// ── ReadAllTablesAsStringsAsync ────────────────────────────────────
126+
127+
[Theory]
128+
[MemberData(nameof(TestDatabases.Small), MemberType = typeof(TestDatabases))]
129+
public async Task ReadAllTablesAsStringsAsync_AllColumns_AreStringType(string path)
130+
{
131+
using var reader = TestDatabases.Open(path);
132+
133+
Dictionary<string, DataTable> all = await reader.ReadAllTablesAsStringsAsync();
134+
135+
foreach (var (_, dt) in all)
136+
foreach (DataColumn col in dt.Columns)
137+
col.DataType.Should().Be(typeof(string));
138+
}
139+
140+
[Theory]
141+
[MemberData(nameof(TestDatabases.Small), MemberType = typeof(TestDatabases))]
142+
public async Task ReadAllTablesAsStringsAsync_RowCounts_MatchReadAllTablesAsync(string path)
143+
{
144+
using var reader = TestDatabases.Open(path);
145+
146+
Dictionary<string, DataTable> typed = await reader.ReadAllTablesAsync();
147+
Dictionary<string, DataTable> strings = await reader.ReadAllTablesAsStringsAsync();
148+
149+
foreach (string name in typed.Keys)
150+
strings[name].Rows.Count.Should().Be(typed[name].Rows.Count);
151+
}
152+
}
153+
}

0 commit comments

Comments
 (0)