Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,14 @@ SQL three-valued logic; date columns compare against `'yyyy-MM-dd'` or
a stable in-memory sort of the matching rows; `TOP`/`LIMIT` applies after the sort.

When a sidecar compound index (`file.cdx`) exists next to the table, queries use it
automatically: equality, range, `BETWEEN` and prefix `LIKE` predicates on indexed
character columns become index seeks, and an `ORDER BY` matching an index tag reads in
index order instead of sorting. This applies to SQL text and to the `Query<T>` builder
alike. The planner is conservative — index tags with dBASE `UNIQUE` or `FOR` filters,
descending or non-character keys, expression keys, or non-ASCII search values fall back
to a full table scan, and the full `WHERE` clause is always re-applied to every row an
index returns. Set `UseIndexes=false` in the connection string (or call
automatically: equality, range and `BETWEEN` predicates on indexed character, integer,
numeric, double and date columns (plus prefix `LIKE` on character columns) become index
seeks, and an `ORDER BY` matching an index tag reads in index order instead of sorting.
This applies to SQL text and to the `Query<T>` builder alike. The planner is
conservative — index tags with dBASE `UNIQUE` or `FOR` filters, descending keys,
expression keys, unsupported key types (datetime, currency), or non-ASCII character
search values fall back to a full table scan, and the full `WHERE` clause is always
re-applied to every row an index returns. Set `UseIndexes=false` in the connection string (or call
`.WithoutIndexes()` on the builder) to force scans, and use
`DbfDbCommand.ExplainPlan()` or `DbfQuery<T>.ExplainPlan()` to see which path a query
takes:
Expand Down
10 changes: 8 additions & 2 deletions src/DbfDataReader/Cdx/CdxKeyComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@ namespace DbfDataReader.Cdx
{
internal static class CdxKeyComparer
{
private const byte Pad = 0x20;
private const byte CharacterPad = 0x20;

// Compares a stored index key against a full-length target key. Stored leaf keys have
// their trailing padding trimmed, so missing trailing bytes compare as the pad byte -
// otherwise a stored key that is a strict prefix of the target would compare as equal.
// Character keys pad with spaces; binary keys (integer, double, date) pad with zeros.
public static int Compare(byte[] storedKey, byte[] targetKey)
{
return Compare(storedKey, targetKey, CharacterPad);
}

public static int Compare(byte[] storedKey, byte[] targetKey, byte pad)
{
for (var i = 0; i < targetKey.Length; i++)
{
var storedByte = i < storedKey.Length ? storedKey[i] : Pad;
var storedByte = i < storedKey.Length ? storedKey[i] : pad;
var cmp = storedByte.CompareTo(targetKey[i]);
if (cmp != 0) return cmp;
}
Expand Down
87 changes: 87 additions & 0 deletions src/DbfDataReader/Cdx/CdxKeyEncoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Buffers.Binary;

namespace DbfDataReader.Cdx
{
// Encodes search values into the binary key formats Visual FoxPro uses for
// non-character index tags, where unsigned byte-wise comparison matches value
// order: integer keys are big-endian with the sign bit flipped, and double-based
// keys (numeric, float, double, date) are big-endian IEEE 754 doubles with the
// sign bit flipped for non-negatives and every bit flipped for negatives. The
// integer format is verified against the tags in test/fixtures/foxprodb.
internal static class CdxKeyEncoder
{
public const int IntegerKeyLength = 4;
public const int DoubleKeyLength = 8;

// days from 0001-01-01 to the start of the Julian period, matching the
// DateTime interpretation in DbfValueDateTime
private const int JulianDayOfDayOne = 1721426;

private static readonly DateTime DayOne = new DateTime(1, 1, 1, 0, 0, 0, DateTimeKind.Unspecified);

public static byte[] EncodeInteger(int value)
{
var key = new byte[IntegerKeyLength];
BinaryPrimitives.WriteInt32BigEndian(key, value);
key[0] ^= 0x80;

return key;
}

public static int DecodeInteger(ReadOnlySpan<byte> key)
{
Span<byte> bytes = stackalloc byte[IntegerKeyLength];
key.CopyTo(bytes);
bytes[0] ^= 0x80;

return BinaryPrimitives.ReadInt32BigEndian(bytes);
}

public static byte[] EncodeDouble(double value)
{
var key = new byte[DoubleKeyLength];
BinaryPrimitives.WriteInt64BigEndian(key, BitConverter.DoubleToInt64Bits(value));

if ((key[0] & 0x80) == 0)
{
key[0] ^= 0x80;
}
else
{
for (var i = 0; i < key.Length; i++) key[i] ^= 0xFF;
}

return key;
}

public static double DecodeDouble(ReadOnlySpan<byte> key)
{
Span<byte> bytes = stackalloc byte[DoubleKeyLength];
key.CopyTo(bytes);

if ((bytes[0] & 0x80) != 0)
{
bytes[0] ^= 0x80;
}
else
{
for (var i = 0; i < bytes.Length; i++) bytes[i] ^= 0xFF;
}

return BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64BigEndian(bytes));
}

// date and datetime keys are Julian day numbers (with the time of day as the
// fractional part) in the double key format
public static double ToJulianDay(DateTime value)
{
return (value.Date - DayOne).Days + JulianDayOfDayOne + value.TimeOfDay.TotalDays;
}

public static byte[] EncodeDate(DateTime value)
{
return EncodeDouble(ToJulianDay(value));
}
}
}
Loading