npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

paradox-reader

v1.8.0

Published

Read-only, zero-dependency Paradox database (.DB/.MB) reader with SQL query support, low-memory streaming scan, and multi-file TsiLang SIB translation resolution

Readme

paradox-reader

Read-only, zero-dependency Paradox database (.DB) reader for Node.js.

Reads .DB files and their companion .MB memo/blob files entirely in memory. No file handles are kept open, no temp files are created, and the original files are never modified.

Quick start

const paradox = require('./paradox-reader');

// Read a single table
const { header, schema, records } = paradox.read('path/to/Table.DB');

// Memory-efficient streaming scan (for large files)
paradox.scan('path/to/History.DB', {
  filter: row => row.ItemKey === 34,
  onRow: (row) => process.stdout.write(JSON.stringify(row) + '\n'),
});

// SQL query across multiple tables
const rows = paradox.query(
  `SELECT I.Name, C.CatKey FROM "Items.DB" AS I, "Categories.DB" AS C
   WHERE I.CatKey = C.CatKey`,
  { basePath: 'path/to/tables/' }
);

// Pivot EAV tables into objects
const data = paradox.scan('Settings.DB');
const groups = paradox.pivot(data); // Map<GroupName, { prop: value, ... }>

// Export
const json = paradox.toJSON(rows);       // always an array
const csv  = paradox.toCSV(data);        // with BOM + CRLF

API

paradox.read(dbPath, [options])

Reads and parses a single Paradox table. Loads all records into memory. Best for small/medium tables.

| Parameter | Type | Description | |-----------|------|-------------| | dbPath | string | Path to the .DB file | | options.encoding | string | Text encoding for alpha fields (default: 'latin1') | | options.mbPath | string | Explicit path to .MB file (auto-detected if omitted) |

Returns { header, schema, records }

  • header — Table metadata:

    • tableName, version, fileType, recordSize, headerSize
    • blockSize, numRecords, numFields, fileBlocks
    • firstBlock, lastBlock, encrypted, sortOrder
  • schema — Array of field descriptors:

    • name — Field name
    • type — Human-readable type ('alpha', 'long', 'memo', etc.)
    • typeCode — Raw Paradox type byte
    • size — Field size in bytes
  • records — Array of plain objects keyed by field name.

paradox.scan(dbPath, [options])

Memory-efficient streaming reader. Processes block-by-block and never accumulates all records unless you want it to. Ideal for large files (e.g. 40-90MB historical/config tables) on memory-constrained machines.

| Parameter | Type | Description | |-----------|------|-------------| | dbPath | string | Path to the .DB file | | options.filter | function | fn(row) → boolean — only matching rows are kept/emitted | | options.columns | string[] | Only parse these columns (skip the rest for speed) | | options.onRow | function | fn(row, index) — streaming callback; rows are NOT accumulated | | options.encoding | string | Text encoding for alpha fields (default: 'latin1') | | options.mbPath | string | Explicit path to .MB file (auto-detected if omitted) |

Returns { header, schema, records }records is [] if onRow was provided.

// Stream 30 days of 50MB encrypted files — heap stays ~4.5MB
const results = [];
for (const file of dayFiles) {
  paradox.scan(file, {
    filter: row => row.ItemKey === 34,
    columns: ['ItemKey', 'EntryTime', 'Value'],
    onRow: (row) => results.push(row),
  });
}

// Or accumulate only filtered rows
const { records } = paradox.scan('BigTable.DB', {
  filter: row => row.CategoryKey > 100 && row.CategoryKey < 200,
});

paradox.query(sql, [options])

Execute a BDE-compatible SQL query across one or more Paradox tables. Tables referenced in FROM are loaded from basePath.

Automatic optimization: For tables >5MB with pushable WHERE filters, query() automatically uses scan() with filter pushdown instead of loading the entire table. Small tables use read() with caching.

| Parameter | Type | Description | |-----------|------|-------------| | sql | string | SQL query string | | options.basePath | string | Directory containing the .DB files | | options.encoding | string | Text encoding (default: 'latin1') | | options.noCache | boolean | Skip cache and always re-read from disk |

Returns object[] — Always an array. Empty result = [], single row = [{...}].

Supported SQL:

SELECT alias.Column AS OutputName, 0 AS LiteralCol
FROM "TableName.DB" AS Alias, "Other.DB" AS B
WHERE Alias.Col = B.Col AND Alias.Type = 'value'
  • SELECT with column references, aliases, numeric/string literals, *
  • FROM with quoted table paths (.DB), aliases, comma-joins
  • WHERE with AND, comparison operators (=, <>, <, >, <=, >=)
  • DISTINCT
  • Case-insensitive column matching and BDE-compatible type coercion
  • Hash joins for multi-table queries

paradox.pivot(data, [options])

Transform Entity-Attribute-Value rows into one object per group. Designed for EAV/pivot tables where each logical "row" is spread across many records (GroupName=entity, PropName=attribute, PropValue=value).

| Parameter | Type | Description | |-----------|------|-------------| | data | object[] or { records } | Rows from read(), scan(), or query() | | options.groupBy | string | Column to group by (default: 'GroupName') | | options.nameCol | string | Column containing property names (default: 'PropName') | | options.valueCol | string | Column containing values (default: 'PropValue') | | options.typeCol | string | Column with type hints for coercion (default: 'PropType') | | options.coerce | boolean | Coerce values using typeCol (default: true) |

Returns Map<string, object> — Each key is a group value, each value is { propName1: val1, propName2: val2, ... } with types coerced from PropType.

// Get all config for group "10" as a single object
const data = paradox.scan('Settings.DB', {
  filter: row => row.GroupName === '10',
});
const groups = paradox.pivot(data);
const config = groups.get('10');
// config = { "Threshold": 5, "Timeout": 10080, "Mode": "A", ... }

// Full table pivot for mapping
const all = paradox.pivot(paradox.scan('Settings.DB'));
// all.size = 167 groups, each with hundreds of typed properties

Type coercion (when coerce: true):

| PropType | JS type | |----------|---------| | Float | number | | Integer | number (parseInt) | | Boolean | boolean | | String | string | | DateTime | string (as-is) |

paradox.toJSON(data, [pretty])

Convert query results or read() output to a JSON string.

paradox.toJSON(queryResults)         // → '[{"Col":1}, ...]'
paradox.toJSON(paradox.read(path))   // → '[{"Col":1}, ...]' (extracts .records)
paradox.toJSON([])                   // → '[]'
paradox.toJSON(results, true)        // → pretty-printed with 2-space indent

paradox.toCSV(data)

Converts the output of read() or scan() into a CSV string with BOM and CRLF endings.

const data = paradox.read('Table.DB');
require('fs').writeFileSync('Table.csv', paradox.toCSV(data), 'utf8');

paradox.clearCache()

Clear the internal table cache used by query(). Call this if the underlying .DB files have changed and you want to force a re-read.

paradox.setCacheTTL(ms)

Set the cache time-to-live in milliseconds. Cached tables that haven't been accessed within this window are automatically evicted. Default is 60000 (1 minute). Set to 0 to disable caching entirely.

// Keep cached tables for 5 minutes
paradox.setCacheTTL(300000);

// Disable caching (every query reads from disk)
paradox.setCacheTTL(0);

// Restore default (1 minute)
paradox.setCacheTTL(60000);

TsiLang SIB Translation Resolution

Some Paradox-based systems store translatable strings as encoded references in alpha fields rather than plain text. This module can resolve those references back to human-readable strings using .sib files from a Translations directory.

Translation reference formats

| Pattern | Example | Resolves to | |---------|---------|-------------| | Simple | [~T~][~Categories\DB_5~] | Voltage | | Multi-file (string key) | [~T:AppLib\TSilData_App~]~Zone A\[~strStatusFan~] | ~Zone A\Fan Output | | Parametric (%s substitution) | [~TF:AppLib\TSilData_App~][~P:1~]~Zone A\[~strStage~] | ~Zone A\Stage 1 | | Inline mixed | [~T~]~Room 3, [~Alerts\DB_42~] | ~Room 3, Door Ajar | | Cross-file 3-part path | [~T~][~Lib\TSilData_Lib\strCategory~] | Refrigeration |

paradox.loadTranslations(configPath, [opts])

Load all .sib files from a Translations directory and return a resolver object. Auto-discovers the directory by searching relative to configPath, or use opts.translationsDir for an explicit path.

| Parameter | Type | Description | |-----------|------|-------------| | configPath | string | Base path used for auto-discovery (searches for a sibling/ancestor Translations/ folder) | | opts.translationsDir | string | Explicit path to the directory containing .sib files (skips auto-discovery) | | opts.sibPath | string | Explicit path to a single .sib file (fallback when no directory is found) | | opts.lang | string | Language column: 'English' (default), 'French', or 'Spanish' |

Returns { tables, resolve, resolveAll } or null if no SIB files are found.

  • resolve(value) — Resolve a single string. Returns the original value if it contains no translation references.
  • resolveAll(records) — Resolve all string fields in an array of row objects (mutates in-place, returns same array).
  • tables — The internal lookup structure (for advanced use with resolveRef / resolveAllRefs).
const paradox = require('paradox-reader');

// Auto-discover: looks for Translations/ relative to the given path
const t = paradox.loadTranslations('/path/to/Config');

// Explicit directory
const t = paradox.loadTranslations('/any/path', {
  translationsDir: '/path/to/Translations',
});

// Single SIB file fallback
const t = paradox.loadTranslations('/any/path', {
  sibPath: '/path/to/Translations/DB.sib',
});

// Use French translations
const t = paradox.loadTranslations('/path/to/Config', { lang: 'French' });

Resolving query results

const paradox = require('paradox-reader');

const translations = paradox.loadTranslations('/path/to/Config');

const rows = paradox.query(
  'SELECT Name, TypeKey FROM Items.DB',
  { basePath: '/path/to/Config' }
);

// Resolve all [~...~] references in every row
if (translations) translations.resolveAll(rows);

// Or resolve a single value
if (translations) {
  const name = translations.resolve('[~T~][~Categories\\DB_5~]');
  // → "Voltage"
}

Auto-discovery logic

When opts.translationsDir is not provided, loadTranslations searches for a Translations/ directory at these paths relative to configPath:

  1. configPath/../../Translations/
  2. configPath/../Translations/
  3. configPath/Translations/

The first path that exists is used. All .sib files in that directory are parsed and indexed by filename (e.g., AppLib.sib → file key AppLib).

paradox.parseSib(sibPath)

Low-level: parse a single .sib file. Returns a Map<sectionName, Map<key, { English, French, Spanish }>>. Keys are numeric for DB_N entries and strings for named entries.

paradox.resolveRef(value, tables, [lang])

Low-level: resolve a single translation reference using a tables object.

paradox.resolveAllRefs(records, tables, [lang])

Low-level: resolve all string fields in an array of records using a tables object.

Supported field types

| Type | Code | JS value | |------|------|----------| | Alpha | 0x01 | string or null | | Date | 0x02 | "YYYY-MM-DD" string or null | | Short | 0x03 | number or null | | Long | 0x04 | number or null | | Currency | 0x05 | number or null | | Number | 0x06 | number or null | | Logical | 0x09 | true, false, or null | | Memo | 0x0C | string or null | | Blob | 0x0D | string or null | | FmtMemo | 0x0E | string or null | | OLE | 0x10 | Buffer or null | | Graphic | 0x11 | Buffer or null | | Time | 0x14 | "HH:MM:SS" string or null | | Timestamp | 0x15 | "YYYY-MM-DDTHH:MM:SS" string or null | | AutoInc | 0x16 | number or null | | BCD | 0x17 | number or null | | Bytes | 0x18 | hex string or null |

How it works

  1. fs.readFileSync() loads the .DB (and .MB if present) into memory buffers. File handles are closed immediately by Node — nothing is kept open.
  2. The header is parsed to extract field descriptors, names, and block layout.
  3. Records are read by walking the data block chain (block-by-block).
  4. Memo/blob fields are resolved from the in-memory .MB buffer using the pxlib-compatible sub-allocator algorithm (type 2 and type 3 blocks).
  5. Encrypted tables are auto-detected and decrypted transparently using the pxlib-compatible chunk-based permutation cipher.

read() vs scan():

  • read() loads ALL records into an array — convenient for small/medium tables.
  • scan() processes block-by-block with optional filter pushdown — memory stays flat (~4.5MB heap) regardless of table size. Use for large files.

query() smart loading:

  • Small tables (<5MB): loaded with read(), cached by mtime for reuse. Cache entries are auto-evicted after 1 minute of no access (configurable via setCacheTTL()).
  • Large tables (>5MB) with WHERE filters: loaded with scan() + filter pushdown. Only matching rows enter memory.
  • Pass noCache: true to skip caching for one-off queries (e.g. historical day files).

Performance

| Scenario | Size | Records | Operation | Time | Heap | |----------|------|---------|-----------|------|------| | Encrypted daily log | 40MB | 1.2M | read() | ~700ms | ~160MB | | Encrypted daily log | 40MB | 1.2M | scan() with onRow | ~500ms | ~4.5MB | | 8 daily logs | 320MB | 9M | scan() sequentially | ~3.8s | ~4.5MB | | 30 daily logs | 1.2GB | 36M | scan() (projected) | ~14s | ~4.5MB | | Large EAV config | 87MB | 103K | scan() + pivot() | ~136ms | ~27MB | | Large EAV config | 87MB | 103K | scan(filter) + pivot() | ~98ms | ~4.3MB | | Large EAV config | 87MB | 103K | SQL with pushdown | ~95ms | ~4.6MB | | Small lookup table | <1MB | 5.3K | read() (cached) | ~3ms | cached |

Supported versions

  • Paradox 3.x, 4.x, 5.x, 7.x table formats
  • Encrypted and unencrypted tables
  • Memo (.MB) blob files with type 2 (single-block) and type 3 (sub-allocated) blocks

Safety guarantee

This module only reads. It uses fs.readFileSync which:

  • Opens the file read-only
  • Copies the contents into a Node.js Buffer
  • Closes the file handle immediately
  • Never writes, truncates, or modifies the source file

You can safely point it at live/production database files.