Skip to content

Legacy CHARSET NONE databases (the € problem) ​

A huge share of long-lived Firebird databases — especially those built by Delphi-era ERP and LOB software — declare their text columns CHARSET NONE and store raw Windows-1252 bytes in them. Modern drivers that assume UTF-8 turn every €, ç, or ã into mojibake.

fast-firebird ships a transcoding toolkit so these databases round-trip cleanly:

ts
const db = await connect({
  database: '/data/legacy.fdb',
  charset: 'NONE',
  charsetNoneEncoding: 'win1252',        // simple strategy
  // or full control (node-firebird2-compatible):
  // transcodeAdapter: { text: { fromDb: b => iconv.decode(b, 'win1252'),
  //                             toDb:  s => iconv.encode(s, 'win1252') } },
  // or per-field:
  // charsetOverrides: { 'HISTORY.MEMO': 'win1252' },
});

const rows = await db.query("select memo from history where memo like ?", ['%€%']);

Four levels of control:

  1. charsetNoneEncoding — one encoding name applied to every NONE column (and NONE-connection text). The right answer for most legacy Windows apps is 'win1252'.
  2. transliterations — pluggable codepage codecs (below), matched by charset name across the whole connection.
  3. transcodeAdapter — your own fromDb/toDb byte↔string functions, compatible with node-firebird2-style adapters. Beats everything else.
  4. charsetOverrides — per-TABLE.COLUMN encoding when one database mixes eras.

The transcoding applies to parameters, results, and text blobs alike — the driver's test suite asserts byte-exact € round-trips on every supported server version.

The built-in win1252 codec ​

win1252 (any spelling: cp1252, windows-1252) is served by a built-in zero-dependency codec — used automatically for charsetNoneEncoding: 'win1252' and for columns declared WIN1252 — that is byte-for-byte faithful to what Windows and Delphi actually do:

  • All 251 defined Windows-1252 bytes decode/encode exactly (€ ⇄ 0x80, smart quotes, em dashes, Š Œ Ž Ÿ ™ ‰ ƒ, …).
  • The five bytes Windows-1252 leaves undefined (0x81 0x8D 0x8F 0x90 0x9D) pass through as their C1 control code points — the Windows "best-fit" behavior — instead of an irreversible U+FFFD. Every possible byte sequence therefore round-trips unchanged, which is exactly what byte-faithful legacy databases need.
  • Clean cells (no byte in 0x80–0x9F) take a native fast path, so plain ASCII and accented Latin text decode at close to Buffer speed.

Mislabeled columns: declared ISO8859_1, actual win1252 ​

A nastier variant of the legacy problem: the database and columns are declared ISO8859_1, but the bytes were written by Windows software and are really win1252. Delphi appears to handle this "correctly" only because on Windows every AnsiString renders through CP1252 regardless of the DB label — the € stored as byte 0x80 was never valid ISO-8859-1 text at all.

charsetNoneEncoding does not apply here

charsetNoneEncoding only kicks in when a column's effective charset is NONE. A column declared ISO8859_1 keeps its declared charset — even over a charset: 'NONE' connection — and decodes as true Latin-1, silently turning € ‚ „ … † ‡ ˆ ‰ Š ‹ Œ Ž ' ' " " • – — ˜ ™ š › œ ž Ÿ into invisible C1 control characters (and encoding them as ? on the way in).

The fix is one line — an alias page that makes the ISO8859_1 name decode/encode as win1252:

ts
import { aliasPage } from '@fast-firebird/core/charsets';

const db = await connect({
  database: '/data/legacy.fdb',
  charset: 'NONE',                       // raw bytes, no server transliteration
  transliterations: [aliasPage('ISO8859_1', 'win1252')],
});

Every ISO8859_1-declared column now decodes and encodes as win1252 — Delphi-identical, parameters included — while UTF8/WIN1252/NONE columns keep their own behavior. The alias target resolves through the built-in pages first ('win1252' rides the byte-faithful built-in codec), then falls back to iconv-lite, so multi-byte mislabels work the same way: aliasPage('ISO8859_1', 'shiftjis') for columns really holding Shift-JIS. An unknown target throws immediately, naming both charsets — never a silent fallback.

Transliteration pages (pluggable codepages) ​

Any single-byte codepage — including ones the driver doesn't ship — can be supplied as a transliteration page: pure data, one character per byte value. The driver derives everything from the table — both directions, the native-speed fast paths, and strict-mode validation — so a page cannot get the subtle parts wrong:

ts
import { defineSingleBytePage } from '@fast-firebird/core/charsets';

// table256[b] = the character byte b decodes to; U+FFFD marks bytes the
// codepage leaves undefined (decode to U+FFFD, throw in strict mode).
const myPage = defineSingleBytePage(['MYPAGE', 'X-MYPAGE'], table256);

const db = await connect({
  database: '/data/legacy.fdb',
  charset: 'NONE',
  transliterations: [myPage],
});

Where pages apply, in resolution terms:

  • A page matching a declared column charset name replaces the driver's own codec for it. User pages win over built-ins (they are explicit per-connection config) — a page may even shadow a Buffer-native charset like ISO8859_1, which is how the mislabeled-column recipe works.
  • charsetNoneEncoding: 'MYPAGE' and charsetOverrides: { 'T.MEMO': 'MYPAGE' } both resolve through pages first, so an override may name a custom page that isn't a Firebird charset at all.
  • Names nothing claims fall back to the built-in pages, then iconv-lite.

Name matching is case-insensitive and ignores -, _, and spaces ('win-1252', 'CP_1252', 'windows 1252' all match the same page).

MIT Licensed. Contains data derived from the Firebird project under IDPL/IPL.