Skip to content

About

Reader and writer for QlikView / Qlik Sense QVD files in .NET - reverse-engineered format, full dual-value and NULL support, CLI included

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

6 Commits

Folders and files

Repository files navigation

qvd-dotnet

CI License: MIT .NET 8

A zero-dependency .NET library and command-line tool for reading and writing QlikView / Qlik Sense QVD files, based on my reverse engineering of the undocumented binary format.

QVD is Qlik's proprietary columnar storage format. There is no official specification and no official way to read one outside a Qlik product. This project covers the whole format:

  • All five symbol types — integers, doubles, strings, and both dual values (number + display text in one value, e.g. a date serial together with its formatted text), which many QVD readers skip or flatten.
  • NULL handling via the format's index bias mechanism.
  • Bit-packed records — rows are stored as little-endian bit-stuffed indexes into per-field symbol tables, at arbitrary bit widths (a year column takes 6 bits per row, a boolean 1 bit).
  • A writer, not just a reader — QvdWriter produces QVDs that QlikView itself loads (verified in QlikView desktop), including duals and NULLs, with automatic symbol deduplication and minimal bit widths. As far as I can tell it is the only QVD writer in the .NET ecosystem.

The full reverse-engineered format description lives in docs/qvd-file-format.md.

This project has a sequel: vortex-dotnet, a .NET reader for the Vortex columnar format, with a benchmark that puts QVD, Parquet and Vortex side by side on ten million rows of energy data.

Library

using Qvd;

var reader = QvdReader.Open("sales.qvd");

Console.WriteLine($"{reader.TableName}: {reader.RecordCount} records");

// Records decode to typed values, not strings
foreach (QvdValue[] record in reader.ReadRecords())
{
    QvdValue amount = record[reader.GetFieldIndex("Amount")];
    if (!amount.IsNull)
    {
        Console.WriteLine($"{amount.Number} (shown as '{amount.DisplayText}')");
    }
}

// Column-oriented access: a field's distinct values without touching the rows
IReadOnlyList<QvdValue> countries = reader.GetSymbolTable("Country");

// Or export everything
using var csv = new StreamWriter("sales.csv");
QvdCsv.Write(reader, csv);

QvdValue models Qlik's dual semantics directly:

Kind Number Text On disk
Integer ✔ — 0x01 + int32
Double ✔ — 0x02 + float64
Text — ✔ 0x04 + UTF-8, NUL-terminated
DualInteger ✔ ✔ 0x05 + int32 + text
DualDouble ✔ ✔ 0x06 + float64 + text
Null — — not in the symbol table (bias)

Symbol tables are decoded lazily per field and records on demand, so peeking at a few rows of a large file only pays for what you touch.

Writing

using Qvd;

var writer = new QvdWriter("Sales", "Country", "Amount", "Updated");

writer.AddRecord(
    QvdValue.FromText("Denmark"),
    QvdValue.FromDouble(1234.56),
    QvdValue.FromDateTime(new DateTime(2026, 7, 3, 14, 30, 0))); // dual: serial + text

writer.AddRecord(
    QvdValue.FromText("Norway"),
    QvdValue.Null,                                               // NULL (bias-encoded)
    QvdValue.FromDualInteger(2026, "Y2026"));                    // dual: int + text

writer.Save("sales.qvd");

The writer deduplicates values into symbol tables, computes the narrowest possible bit width per field (a constant column costs zero bits per row), and encodes NULLs with the same bias convention QlikView uses.

CLI

$ qvdtool info sales.qvd
File:     sales.qvd
Table:    Sales
Records:  56,758
Fields:   9
Record:   30 bytes, bit-packed

FIELD        TYPE       SYMBOLS  BITS  NULLS  TAGS
year         INTEGER         54     6         $numeric $integer
quarter      INTEGER          4     2         $numeric $integer
country      ASCII           83     7         $text
mtons        REAL         9,741    14  yes    $numeric
...

$ qvdtool head sales.qvd -n 5          # first rows as a table
$ qvdtool csv sales.qvd -o sales.csv   # full export
$ qvdtool symbols sales.qvd country    # a field's distinct values

Build

dotnet build
dotnet test

Requires the .NET 8 SDK. No runtime dependencies. No binary fixtures are checked in: reader tests decode files produced by an independent low-level byte encoder, and writer tests round-trip through the reader — so the two implementations cross-check each other.

Scope and limitations

  • Reads standard uncompressed QVDs (the only kind QlikView and Qlik Sense write). A non-empty Compression header is rejected explicitly rather than misread.
  • The file is buffered in memory, so files are limited to ~2 GB.
  • The writer produces standard uncompressed QVDs following the same layout QlikView writes; every write is round-trip tested against the reader, and generated files have been verified to load cleanly in QlikView desktop (duals, dates and NULLs intact).

How the format was reverse engineered

QVD files start with a plain XML header that freely gives away table and field metadata — including each field's bit offset, bit width, index bias, and symbol-table extents. The binary section behind it was worked out by inspecting files in a hex editor and cross-checking decoded output against the source tables in QlikView: first the symbol tables with their per-value type bytes, then the bit-stuffed record section, and finally the bias mechanism Qlik uses to represent NULLs as negative indexes. Details in docs/qvd-file-format.md.

Related projects

License

MIT — David Linton

About

Reader and writer for QlikView / Qlik Sense QVD files in .NET - reverse-engineered format, full dual-value and NULL support, CLI included

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages