Skip to content
Open
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
20 changes: 20 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,26 @@ This package converts Pro Cycling Manager CDB binary database files to and from
- Low-level binary format handling lives in [src/reader.ts](src/reader.ts), [src/writer.ts](src/writer.ts), [src/types.ts](src/types.ts), and [src/tableMetadata.ts](src/tableMetadata.ts).
- Compression helpers live in [src/compression.ts](src/compression.ts).

### Terminology

Three words, deliberately not interchangeable:

- **CDB** — Cyanide's binary database format, the `.cdb` file. This is what the
library reads and writes, and it is always handled as a *buffer*, never a path:
the public API takes `cdbBuffer` / returns `Uint8Array`, and only the CLI in
[src/cli.ts](src/cli.ts) ever touches the filesystem. The format internals live
in [src/reader.ts](src/reader.ts), [src/writer.ts](src/writer.ts),
[src/compression.ts](src/compression.ts) and [src/tableMetadata.ts](src/tableMetadata.ts).
- **database** — on its own, always the *SQLite* side: a `sql.js` `Database`
instance (aliased `SqlDatabase`) or the `.sqlite` file it exports to. Never use
it bare for a `.cdb`; say "CDB" or "CDB database" when that is what you mean.
- **save** — a `.cdb` the *game itself wrote* as the player played, as opposed to
an official release or a community update. Nothing in the conversion path cares
about the difference, so this word belongs only where the provenance is the
actual point: the reverse-engineering notes in
[src/keyInference.ts](src/keyInference.ts) and [src/tableMetadata.ts](src/tableMetadata.ts)
("observed in real saves"). Do not use it as a generic name for the input file.

## Commands

- Install: `npm install`
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2025 Mathieu Picciolli
Copyright (c) 2025 PCMStack

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
42 changes: 21 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# cdb-converter

[![npm version](https://img.shields.io/npm/v/cdb-converter.svg)](https://www.npmjs.com/package/cdb-converter)
[![CI](https://github.com/mpicciolli/cdb-converter/actions/workflows/ci.yml/badge.svg)](https://github.com/mpicciolli/cdb-converter/actions/workflows/ci.yml)
[![CI](https://github.com/PCMStack/converter/actions/workflows/ci.yml/badge.svg)](https://github.com/PCMStack/converter/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/npm/l/cdb-converter.svg)](./LICENSE)
[![Node.js](https://img.shields.io/node/v/cdb-converter.svg)](https://nodejs.org)

Convert **Pro Cycling Manager CDB** database files to and from SQLite, straight from the command line or your own code. Lightweight, isomorphic (Node.js **and** the browser), and zero-configuration.

The conversion is **lossless**: a full `cdb → sqlite → cdb` round-trip preserves every table, column, data type, and flag — so you can edit a save in any SQLite tool and load it back into the game. Optionally, it can reconstruct the save's relationships as real `PRIMARY KEY` / `FOREIGN KEY` constraints, turning the export into a normalized database you can explore with JOINs and ER-diagram tools.
The conversion is **lossless**: a full `cdb → sqlite → cdb` round-trip preserves every table, column, data type, and flag — so you can edit a database in any SQLite tool and load it back into the game. Optionally, it can reconstruct the database relationships as real `PRIMARY KEY` / `FOREIGN KEY` constraints, turning the export into a normalized database you can explore with JOINs and ER-diagram tools.

> [!NOTE]
> Based on [agfor/pcmdbedit](https://github.com/agfor/pcmdbedit/) — many thanks to agfor for the foundational work.
Expand Down Expand Up @@ -53,25 +53,25 @@ npm install cdb-converter
The fastest way to try it is the CLI:

```bash
npx cdb-converter save.cdb
npx cdb-converter database.cdb
```

## Command line

The package ships a `cdb-converter` command. The conversion direction is auto-detected from the input file extension.

```bash
# CDB → SQLite (default output: save.sqlite)
npx cdb-converter save.cdb
# CDB → SQLite (default output: database.sqlite)
npx cdb-converter database.cdb

# SQLite → CDB (default output: save.cdb)
npx cdb-converter save.sqlite
# SQLite → CDB (default output: database.cdb)
npx cdb-converter database.sqlite

# Provide an explicit output path (directories are created as needed)
npx cdb-converter save.cdb data/save.sqlite
npx cdb-converter database.cdb data/database.sqlite

# Reconstruct PRIMARY KEY / FOREIGN KEY constraints (CDB → SQLite only)
npx cdb-converter save.cdb save.sqlite --normalize
npx cdb-converter database.cdb database.sqlite --normalize

# Help / version
npx cdb-converter --help
Expand All @@ -83,10 +83,10 @@ npx cdb-converter --version
| `.cdb` | CDB → SQLite | `<input>.sqlite` |
| `.sqlite` / `.db` | SQLite → CDB | `<input>.cdb` |

| Option | Effect |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| Option | Effect |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `-n`, `--normalize` | (CDB → SQLite only) reconstruct PK/FK constraints from PCM naming conventions. See [Normalized schema](#normalized-schema). |
| `--index-fk` | Implies `--normalize`; also indexes every FK column for faster JOINs (roughly doubles output size). |
| `--index-fk` | Implies `--normalize`; also indexes every FK column for faster JOINs (roughly doubles output size). |

## Library usage

Expand All @@ -100,15 +100,15 @@ import { cdbToSql } from "cdb-converter";
const SQL = await initSqlJs();

// Read and convert a CDB file
const cdbBuffer = fs.readFileSync("save.cdb");
const cdbBuffer = fs.readFileSync("database.cdb");
const db = cdbToSql(cdbBuffer, SQL);

// Query it like any SQLite database
const result = db.exec("SELECT * FROM Teams LIMIT 5");
console.log(result[0].values);

// Export to a .sqlite file
fs.writeFileSync("save.sqlite", db.export());
fs.writeFileSync("database.sqlite", db.export());
```

> [!IMPORTANT]
Expand Down Expand Up @@ -154,11 +154,11 @@ import { sqlToCdb } from "cdb-converter";
const SQL = await initSqlJs();

// Load a SQLite database and convert back to CDB
const sqliteBuffer = fs.readFileSync("save.sqlite");
const sqliteBuffer = fs.readFileSync("database.sqlite");
const db = new SQL.Database(sqliteBuffer);

const cdbBuffer = sqlToCdb(db); // automatically compressed
fs.writeFileSync("save.cdb", Buffer.from(cdbBuffer));
fs.writeFileSync("database.cdb", Buffer.from(cdbBuffer));
```

### Compression
Expand Down Expand Up @@ -269,11 +269,11 @@ A full `cdb → sqlite → cdb` round-trip on a real ~60k-row database stays wel

Normalization is opt-in and costs only what you ask for (measured against the default conversion, ~60k rows):

| Mode | Conversion time | Output size |
| --------------------------------------------- | --------------- | ----------- |
| Default (flat) | baseline | baseline |
| `normalize` | +~10% | +~40% |
| `normalize` + `indexForeignKeys` | +~40% | +~130% |
| Mode | Conversion time | Output size |
| -------------------------------- | --------------- | ----------- |
| Default (flat) | baseline | baseline |
| `normalize` | +~10% | +~40% |
| `normalize` + `indexForeignKeys` | +~40% | +~130% |

See **[bench/README.md](bench/README.md)** for the full per-fixture numbers, the bundle breakdown, and how to reproduce them (`npm run bench`).

Expand Down
8 changes: 7 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.4/schema.json",

Check notice on line 2 in biome.json

View workflow job for this annotation

GitHub Actions / test (22.x)

deserialize

The configuration schema version does not match the CLI version 2.5.6

Check notice on line 2 in biome.json

View workflow job for this annotation

GitHub Actions / test (24.x)

deserialize

The configuration schema version does not match the CLI version 2.5.6

Check notice on line 2 in biome.json

View workflow job for this annotation

GitHub Actions / test (26.x)

deserialize

The configuration schema version does not match the CLI version 2.5.6
"formatter": {
"enabled": true
},
Expand All @@ -10,6 +10,12 @@
"enabled": false
},
"files": {
"includes": ["**", "!dist", "!node_modules", "!coverage"]
"includes": [
"**",
"!dist",
"!node_modules",
"!coverage",
"!samples/browser/vendor"
]
}
}
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
"version": "0.3.0",
"description": "Convert Pro Cycling Manager CDB files to/from SQLite and other formats. TypeScript library with zero configuration.",
"license": "MIT",
"author": "mpicciolli",
"author": "PCMStack",
"repository": {
"type": "git",
"url": "https://github.com/mpicciolli/cdb-converter"
"url": "https://github.com/PCMStack/converter"
},
"bugs": {
"url": "https://github.com/PCMStack/converter/issues"
},
"homepage": "https://github.com/PCMStack/converter#readme",
"keywords": [
"cdb",
"database",
Expand Down
51 changes: 46 additions & 5 deletions samples/browser/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
# Browser sample

This sample shows how to use `cdbToSql` in the browser with `sql.js`.
This sample shows how to use `cdbToSql` in the browser with `sql.js`. It is also
the public demo, deployed to GitHub Pages at <https://pcmstack.github.io/converter/>
by [`.github/workflows/static.yml`](../../.github/workflows/static.yml), which
copies this folder verbatim.

## What it does

- Load a `.cdb` file from an `<input type="file">`
- Convert it to a SQLite database in the browser
- Display the detected tables from `DB_STRUCTURE`
- Load a `.cdb` database from an `<input type="file">` or by drag and drop
- Convert it to a SQLite database, entirely in the browser
- Display the detected tables from `DB_STRUCTURE` and preview their first rows
- Download the generated `.sqlite` file

Nothing is ever uploaded: the conversion runs client-side, in WebAssembly.

## Run the sample

Serve this folder with any static HTTP server, then open `/samples/browser/` in your browser.
Expand All @@ -17,9 +22,45 @@ Example with VS Code Live Server or any equivalent local server:

1. Start a local server from the repository root
2. Open `http://127.0.0.1:4173/samples/browser/`
3. Select a `.cdb` file and click **Convert**
3. Select a `.cdb` database and click **Convert**

Opening `index.html` straight from the filesystem does not work: `app.js` is an
ES module and `sql-wasm.wasm` is fetched over HTTP, both of which browsers block
on `file://`.

## Files

- `index.html` defines the UI
- `app.js` initializes `sql.js`, runs `cdbToSql`, and prepares the SQLite download
- `style.css` maps the PCMStack design tokens onto Pico's CSS variables
- `assets/` holds the PCMStack favicon, wordmark and Open Graph card

The header inlines `assets/logo.svg` rather than pointing an `<img>` at it: the
wordmark is built from live `<text>`, and an externally referenced SVG cannot
reach the page's webfonts, so it would fall back to a system sans. The file is
kept as the source of truth for that markup.
- `vendor/` holds the third-party runtime dependencies, served from this origin

## Vendored dependencies

`vendor/` is checked in on purpose. This page hands the user's own database to
the code it loads, so it must not depend on a third-party CDN staying up or
serving what it served yesterday.

| File | Source |
| -------------- | ----------------------------------------- |
| `pico.min.css` | `@picocss/pico` v2.1.1 |
| `sql-wasm.js` | `sql.js` v1.14.1, from `node_modules` |
| `sql-wasm.wasm`| `sql.js` v1.14.1, from `node_modules` |

To refresh the `sql.js` pair after a dependency bump:

```bash
cp node_modules/sql.js/dist/sql-wasm.{js,wasm} samples/browser/vendor/
```

`app.js` points `initSqlJs`'s `locateFile` at `./vendor/`, so the `.wasm` is
resolved next to the page rather than next to the script that loaded it.

The folder is excluded from Biome in [`biome.json`](../../biome.json): these are
minified upstream artifacts and must not be reformatted.
6 changes: 4 additions & 2 deletions samples/browser/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ async function convertFile() {

const startedAt = performance.now();
try {
// sql.js resolves its .wasm relative to this callback, not to the script
// URL, so it has to be pointed at the vendored copy explicitly.
const SQL = await initSqlJs({
locateFile: (filename) =>
`https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.2/${filename}`,
new URL(`./vendor/${filename}`, import.meta.url).href,
});
const cdbBuffer = await file.arrayBuffer();
const db = cdbToSql(new Uint8Array(cdbBuffer), SQL);
Expand Down Expand Up @@ -299,7 +301,7 @@ function copyInstallCommand() {
const command = "npm install cdb-converter";
void navigator.clipboard?.writeText(command);
copyInstallButton.innerHTML =
'<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="#1D9E75" stroke-width="2" aria-hidden="true"><path d="M3 8l4 4 6-6"/></svg>';
'<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="#0f9d6b" stroke-width="2" aria-hidden="true"><path d="M3 8l4 4 6-6"/></svg>';
window.setTimeout(() => {
copyInstallButton.innerHTML =
'<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="5" y="5" width="9" height="9" rx="2"/><path d="M3 11V3a2 2 0 0 1 2-2h8"/></svg>';
Expand Down
5 changes: 5 additions & 0 deletions samples/browser/assets/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions samples/browser/assets/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added samples/browser/assets/og-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 56 additions & 0 deletions samples/browser/assets/og-image.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading