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
24 changes: 24 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,30 @@ 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

Two words, deliberately not interchangeable (same meanings as in `pcm-mcp`):

- **database** — a `.cdb` file. Cyanide's binary database format, and what this
library exists to read and write. It may be a player save, an official release
or a community update; nothing in the conversion path cares which. Here 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) 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).
- **save** — a `.cdb` the *game itself wrote* as the player played, as opposed to
an official release or a community update. This repository has no notion of
save discovery, so the word belongs only where 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"). Never
use it as a generic name for the input file.

Because this repository is the one place both formats are live at once, the
SQLite side is *always* qualified: "SQLite database", the `.sqlite` file, or the
`sql.js` `Database` instance (aliased `SqlDatabase`). Bare "database" in prose
means the `.cdb`.

## 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: 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
13 changes: 9 additions & 4 deletions samples/browser/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>cdb-converter</title>
<title>cdb-converter: PCMStack</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
Expand All @@ -13,8 +13,9 @@
<body>
<main class="demo">
<header class="page-header">
<p class="page-header__eyebrow">PCMStack</p>
<h1 class="page-header__title">cdb-converter</h1>
<p class="page-header__subtitle">Convert Pro Cycling Manager save files to SQLite in one line.</p>
<p class="page-header__subtitle">Convert any Pro Cycling Manager database to SQLite in one line.</p>
<div class="badge-list">
<span class="badge-list__item">MIT</span>
<span class="badge-list__item">ESM + CJS</span>
Expand All @@ -32,6 +33,10 @@ <h1 class="page-header__title">cdb-converter</h1>

<section>
<p class="section__label">Try it</p>
<p class="privacy-note">
Everything runs in your browser. Your database is never uploaded: it is
read, converted and downloaded entirely on this machine.
</p>
<label id="dropzone" class="dropzone" for="cdb-file" data-has-file="false">
<svg class="dropzone__icon" viewBox="0 0 36 36" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<rect x="4" y="6" width="20" height="26" rx="3" />
Expand Down Expand Up @@ -101,8 +106,8 @@ <h1 class="page-header__title">cdb-converter</h1>
</section>
<footer>
<nav class="footer__nav">
<a class="footer__link" href="https://github.com/mpicciolli/cdb-converter" rel="noopener noreferrer" target="_blank">GitHub</a>
<a class="footer__link" href="https://github.com/mpicciolli/cdb-converter/blob/main/LICENSE" rel="noopener noreferrer" target="_blank">License</a>
<a class="footer__link" href="https://github.com/PCMStack/converter" rel="noopener noreferrer" target="_blank">GitHub</a>
<a class="footer__link" href="https://github.com/PCMStack/converter/blob/main/LICENSE" rel="noopener noreferrer" target="_blank">License</a>
</nav>
</footer>
</main>
Expand Down