PdxLib.Connector 1.0.0

dotnet add package PdxLib.Connector --version 1.0.0
                    
NuGet\Install-Package PdxLib.Connector -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PdxLib.Connector" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PdxLib.Connector" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="PdxLib.Connector" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PdxLib.Connector --version 1.0.0
                    
#r "nuget: PdxLib.Connector, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PdxLib.Connector@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PdxLib.Connector&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=PdxLib.Connector&version=1.0.0
                    
Install as a Cake Tool

PdxLib Connector

CI

Read Borland Paradox databases (.DB tables, .MB blob files, .PX indexes) from .NET.

Two parts:

  1. pxlib.dll — pxlib 0.6.8 built as a self-contained Windows x64 DLL with MSVC.
  2. PdxLib.Connector — a net9.0 class library binding it, exposing a read-only managed API plus a DbDataReader.
using PdxLib.Connector;

using var table = ParadoxTable.Open(@"C:\data\CUSTOMER.DB");

Console.WriteLine($"{table.TableName}: {table.RecordCount} rows, cp{table.CodePage}");

foreach (ParadoxField field in table.Fields)
    Console.WriteLine($"  {field.Name} {field.Type}({field.Length}) -> {field.ClrType.Name}");

foreach (ParadoxRecord row in table.Records)
    Console.WriteLine($"{row["CUSTNO"]}\t{row.GetString("NAME")}");

Feeding System.Data consumers:

using var table = ParadoxTable.Open(path);
using var reader = table.CreateDataReader();

var dt = new DataTable();
dt.Load(reader);                       // or SqlBulkCopy, Dapper, ...

Layout

pxlib-0.6.8/                    vendored upstream source (7 documented edits, see native/PATCHES.md)
native/
  generated/config.h            hand-written config.h for MSVC
  generated/paradox.h           paradox.h.in configured with iconv/recode off
  pxlib_ext.{h,c}               PX_malloc, PX_free, PX_free_record, px_fopen_utf8
  smoketest/pxsmoke.c           native round-trip test
  build-native.cmd              builds pxlib.dll
  build-smoketest.cmd           builds and runs pxsmoke.exe
  PATCHES.md                    every change made to upstream pxlib
artifacts/native/win-x64/       pxlib.dll, pxlib.lib, pxlib.pdb
src/PdxLib.Connector/           the class library
tests/PdxLib.Connector.Tests/   xunit suite

Building

Requires Visual Studio (or Build Tools) with the Desktop development with C++ workload, and the .NET 9 SDK.

native\build-native.cmd              # -> artifacts\native\win-x64\pxlib.dll
native\build-smoketest.cmd           # native round trip; must print SMOKE TEST PASSED
dotnet build
dotnet test

Build the native DLL first — the managed project copies it into its output directory, and without it every test fails with DllNotFoundException.

.github/workflows/ci.yml runs exactly that sequence on windows-latest for every push and pull request against main.

Releasing

.github/workflows/release.yml runs the sequence above on a Windows runner, packs, and publishes to nuget.org. Push a tag to release:

git tag v0.1.0
git push origin v0.1.0

The tag minus its leading v becomes the package version, so <Version> in the csproj is only a fallback for local packing. Running the workflow by hand from the Actions tab builds and packs without publishing unless publish is ticked — use that to check a candidate before tagging.

Publishing uses Trusted Publishing rather than a stored API key: the job exchanges a GitHub OIDC token for a nuget.org key valid for one hour, so no long-lived credential exists to leak. Setup, once:

  1. nuget.org → your username → Trusted Publishing → new policy, with repository owner Neciota, repository dotnet-paradox-connector, workflow file release.yml (the file name alone, no path), environment blank.
  2. Add the repository secret NUGET_USER: your nuget.org profile name, not the email address.

Renaming release.yml invalidates the policy — the workflow file name is part of what nuget.org matches on.

Design notes

Read-only. The native DLL exports pxlib's full API including the writers, but the managed layer binds and exposes only the read path. Writing to a live Paradox table risks corrupting it, and extraction is the use case here.

Text encoding is handled in managed code. The DLL is built with PX_USE_ICONV 0, so PX_set_targetencoding is inoperative and pxlib returns raw bytes. ParadoxTable reads the header's DOS codepage and decodes with CodePagesEncodingProvider — no libiconv to ship, and .NET's legacy-codepage support is more dependable. Override per table with ParadoxOpenOptions.Encoding when a table's header codepage is wrong.

No VC++ redistributable. Built with /MT, so pxlib.dll imports only KERNEL32.dll. The consequence is that its heap is private, which is why PX_free/PX_free_record had to be added — see native/PATCHES.md.

Read-only files and Unicode paths work. Upstream calls fopen(path, "rb+"), which fails on read-only media and cannot express a non-ANSI path. The patched build falls back to "rb" and opens via _wfopen.

Type mapping

Paradox CLR
Alpha string (trailing padding trimmed; disable with TrimAlphaPadding)
MemoBlob, FormattedMemoBlob string
Short short
Long, AutoInc int
Number double
Currency, Bcd decimal
Logical bool
Date, Timestamp DateTime
Time TimeSpan
Bytes, Blob, Ole, Graphic byte[]

NULL fields come back as null (DBNull.Value through ParadoxDataReader).

Paradox quirks worth knowing — all of these are properties of the on-disk format, and each has a test pinning it:

  • An empty Alpha value is indistinguishable from NULL. Paradox marks a value present by setting the high bit of its first byte, and an empty string leaves the field zeroed. It reads back as null, not "".
  • A Bytes value starting with 0x00 reads as NULL. There is no separate null flag for that type, so a leading zero byte means "absent".
  • short.MinValue and int.MinValue cannot be stored. Integers are held as value XOR sign-bit, and all-zero bytes mean NULL — so the minimum of each type maps exactly onto the null sentinel. Every other value, including 0 and both maxima, round-trips.
  • Dates and timestamps use an unusual origin. Date is a day count where 1 is 0001-01-01; Timestamp is milliseconds where 86_400_000 is 0001-01-01. The conversions are pinned against pxlib's own formatters in DateTimeConversionTests.

One upstream bug was fixed rather than worked around: pxlib returned a NULL Number, Currency or Timestamp as 0.0 with its null flag clear, making it indistinguishable from a real zero. The information was destroyed inside pxlib, so the fix had to be in C — see patch 5 in native/PATCHES.md.

Threading

ParadoxTable is not thread-safe — pxlib keeps one read-cache block and a file cursor per document. Open one table per thread.

Testing

dotnet test needs no sample data: the suite writes real Paradox files using pxlib's own writer and reads them back through the public API.

To also run against genuine tables:

set PXLIB_TEST_DB=C:\data\CUSTOMER.DB     # a single table
set PXLIB_TEST_DIR=C:\data                # or every .DB beneath a folder
dotnet test -v normal

RealDataTests then enumerates every record of every configured table. With neither variable set those two tests do nothing and say so in their output (xunit 2 has no dynamic skip, hence -v normal to see the message). Running these against real production tables is the most valuable check available — the generated fixture cannot cover real-world header variants, encryption, or unusual codepages.

Licence

LGPL-2.1-or-later — LICENSE.txt.

PdxLib.Connector links pxlib, which is itself LGPL-2.1-or-later. Note that the pxlib 0.6.8 tarball ships a COPYING containing the GPL v2 text even though its sources grant the Lesser GPL; the vendored copy is kept verbatim, and the licence actually relied on is in LICENSE.txt. For the modifications made to pxlib, the components it carries from other authors, and the LGPL §6 relinking position, see THIRD-PARTY-NOTICES.md and native/PATCHES.md.

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 131 7/27/2026