qvd 0.7.0

High-performance library for reading, writing and converting Qlik QVD files with Parquet/Arrow/DataFusion support
Documentation
# TypeScript / Node.js Examples

> Requires Node.js 22+. All I/O operations are async by default (run on libuv thread pool, never block the event loop).

## Install

```bash
npm install qvdrs
```

## Read and Write QVD

```typescript
import { readQvd, saveQvd } from 'qvdrs'

const table = await readQvd('data.qvd')
console.log(`Rows: ${table.numRows}, Cols: ${table.numCols}`)
console.log(`Table: ${table.tableName}`)
console.log(`Columns: ${table.columns}`)

// Save (byte-identical roundtrip)
await saveQvd(table, 'copy.qvd')
```

## Sync Variants (for scripts/CLI)

```typescript
import { readQvdSync, saveQvdSync } from 'qvdrs'

const table = readQvdSync('data.qvd')
saveQvdSync(table, 'copy.qvd')
```

## Access Cell Values

```typescript
const table = await readQvd('data.qvd')

// By index
const val = table.get(0, 0) // row 0, col 0

// By column name
const name = table.getByName(0, 'ClientName')

// Entire column
const ids = table.columnValuesByName('ClientID')
```

## Convert to JSON

```typescript
const table = await readQvd('data.qvd')

// All rows
const allRows = table.toJson()
// [{col1: "val1", col2: "val2"}, ...]

// First 5 rows
const preview = table.head(5)
```

## EXISTS() — O(1) Lookup

```typescript
import { readQvd, JsExistsIndex, filterExists } from 'qvdrs'

// Build index from a table column
const clients = await readQvd('clients.qvd')
const index = JsExistsIndex.fromColumn(clients, 'ClientID')

// Or from explicit values
const index2 = JsExistsIndex.fromValues(['100', '200', '300'])

// O(1) lookup
console.log(index.exists('12345')) // true/false
console.log(index.len)             // number of unique values

// Filter rows — returns matching row indices
const facts = await readQvd('facts.qvd')
const matchingRows = filterExists(facts, 'ClientID', index)
const filtered = facts.subsetRows(matchingRows)
```

## Streaming Filtered Read

```typescript
import { readQvdFiltered, JsExistsIndex } from 'qvdrs'

// Reads only matching rows — memory-efficient for large files
const table = await readQvdFiltered(
  'large_file.qvd',
  'ActionID',                         // filter column
  ['7', '9'],                         // values to match
  ['ClientID', 'Date', 'ActionID'],   // select columns (optional)
  65536                               // chunk size (optional)
)
```

## Filter by Values

```typescript
const table = await readQvd('data.qvd')
const filtered = table.filterByValues('Status', ['Active', 'Pending'])
console.log(filtered.numRows)
```

## Concatenate — Pure Append

```typescript
import { readQvd, saveQvd } from 'qvdrs'

const jan = await readQvd('sales_jan.qvd')
const feb = await readQvd('sales_feb.qvd')

// Strict mode (default) — columns must match
const merged = jan.concatenate(feb)
await saveQvd(merged, 'sales_q1.qvd')

// Union mode — missing columns filled with NULL
const merged2 = jan.concatenate(feb, 'union')
```

### File-Level Concatenation

```typescript
import { concatenateQvd } from 'qvdrs'

// Read + merge + write in one async operation
await concatenateQvd('jan.qvd', 'feb.qvd', 'all.qvd', 'strict')
```

## Concatenate with PK — Upsert/Dedup

```typescript
const master = await readQvd('master.qvd')
const delta = await readQvd('delta.qvd')

// Replace: new rows win on PK collision (upsert)
const updated = master.concatenatePk(delta, ['ID'], 'replace')

// Skip: existing rows kept on collision
const safe = master.concatenatePk(delta, ['ID'], 'skip')

// Error: throw if any PK collision
const strict = master.concatenatePk(delta, ['ID'], 'error')

// Composite PK
const multi = master.concatenatePk(delta, ['RegionID', 'ProductID'], 'replace')
```

### File-Level PK Merge

```typescript
import { concatenatePkQvd } from 'qvdrs'

await concatenatePkQvd(
  'master.qvd',
  'delta.qvd',
  'result.qvd',
  ['ID'],
  'replace',  // on_conflict
  'strict'    // schema
)
```

## Symbols (Distinct Values)

```typescript
const table = await readQvd('data.qvd')

const symbols = table.symbols('Status')
// ["Active", "Pending", "Closed"]

const count = table.numSymbols('Status')
// 3
```

## Normalize for Qlik Compatibility

```typescript
const table = await readQvd('data.qvd')
table.normalize() // Converts DualInt→Int, sets proper NumberFormat/Tags
await saveQvd(table, 'normalized.qvd')
```

## Error Handling

```typescript
import { readQvd } from 'qvdrs'

try {
  const table = await readQvd('nonexistent.qvd')
} catch (e) {
  console.error(e.message) // "No such file or directory..."
}
```

## Typical ETL Pipeline

```typescript
import { readQvd, saveQvd, JsExistsIndex, readQvdFiltered } from 'qvdrs'

// 1. Load dimension table
const clients = await readQvd('dim_clients.qvd')
const clientIndex = JsExistsIndex.fromColumn(clients, 'ClientID')

// 2. Stream-filter fact table (only matching clients)
const facts = await readQvdFiltered(
  'fact_transactions.qvd',
  'ClientID',
  [...Array.from({ length: clientIndex.len }, (_, i) => String(i))],
  ['ClientID', 'Amount', 'Date']
)

// 3. Save filtered result
await saveQvd(facts, 'fact_filtered.qvd')
console.log(`Filtered: ${facts.numRows} rows`)
```