Skip to content

Commit 2d0c0a4

Browse files
committed
feat: enforce non-zero completed count in torrents table
Add CHECK constraint to ensure the `completed` column in the `torrents` table is always >= 1, preventing invalid zero values. This change reflects the semantic meaning that a torrent entry should only exist once it has been completed at least once. Changes: - Add migration scripts for both SQLite and MySQL to: - Delete any existing rows with completed = 0 - Add CHECK constraint (completed >= 1) - Change default value from 0 to 1 - Update hardcoded table creation SQL in database drivers to include the new constraint and default value - Document the new constraint in migrations README with complete table schema reference for all 4 tracker tables - Update module documentation to clarify permanent keys support The migration handles SQLite's limitation by recreating the table, while MySQL can add the constraint directly (requires MySQL 8.0.16+).
1 parent 93fc909 commit 2d0c0a4

6 files changed

Lines changed: 105 additions & 4 deletions

File tree

packages/tracker-core/migrations/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,66 @@
33
We don't support automatic migrations yet. The tracker creates all the needed tables when it starts. The SQL sentences are hardcoded in each database driver.
44

55
The migrations in this folder were introduced to add some new changes (permanent keys) and to allow users to migrate to the new version. In the future, we will remove the hardcoded SQL and start using a Rust crate for database migrations. For the time being, if you are using the initial schema described in the migration `20240730183000_torrust_tracker_create_all_tables.sql` you will need to run all the subsequent migrations manually.
6+
7+
## Database Tables
8+
9+
The tracker uses 4 tables:
10+
11+
### 1. `whitelist`
12+
13+
Stores whitelisted torrent infohashes for private/whitelisted mode.
14+
15+
| Column | SQLite Type | MySQL Type | Description |
16+
|--------|-------------|------------|-------------|
17+
| `id` | INTEGER PRIMARY KEY AUTOINCREMENT | integer PRIMARY KEY AUTO_INCREMENT | Auto-increment ID |
18+
| `info_hash` | TEXT NOT NULL UNIQUE | VARCHAR(40) NOT NULL UNIQUE | BitTorrent V1 infohash (40-char hex string) |
19+
20+
### 2. `torrents`
21+
22+
Stores per-torrent metrics (completed download count).
23+
24+
| Column | SQLite Type | MySQL Type | Description |
25+
|--------|-------------|------------|-------------|
26+
| `id` | INTEGER PRIMARY KEY AUTOINCREMENT | integer PRIMARY KEY AUTO_INCREMENT | Auto-increment ID |
27+
| `info_hash` | TEXT NOT NULL UNIQUE | VARCHAR(40) NOT NULL UNIQUE | BitTorrent V1 infohash (40-char hex string) |
28+
| `completed` | INTEGER DEFAULT 1 NOT NULL CHECK (completed >= 1) | INTEGER DEFAULT 1 NOT NULL CHECK (completed >= 1) | Number of times the torrent has been fully downloaded (minimum 1) |
29+
30+
### 3. `keys`
31+
32+
Stores authentication keys for private trackers.
33+
34+
| Column | SQLite Type | MySQL Type | Description |
35+
|--------|-------------|------------|-------------|
36+
| `id` | INTEGER PRIMARY KEY AUTOINCREMENT | INT NOT NULL AUTO_INCREMENT | Auto-increment ID |
37+
| `key` | TEXT NOT NULL UNIQUE | VARCHAR(32) NOT NULL UNIQUE | Authentication token (32-char alphanumeric string) |
38+
| `valid_until` | INTEGER (nullable) | INT(10) (nullable) | Unix timestamp for key expiration; NULL means permanent key |
39+
40+
### 4. `torrent_aggregate_metrics`
41+
42+
Stores global/aggregate metrics not tied to specific torrents (e.g., total downloads across all torrents).
43+
44+
| Column | SQLite Type | MySQL Type | Description |
45+
|--------|-------------|------------|-------------|
46+
| `id` | INTEGER PRIMARY KEY AUTOINCREMENT | integer PRIMARY KEY AUTO_INCREMENT | Auto-increment ID |
47+
| `metric_name` | TEXT NOT NULL UNIQUE | VARCHAR(50) NOT NULL UNIQUE | Unique metric identifier (e.g., `torrents_downloads_total`) |
48+
| `value` | INTEGER DEFAULT 0 NOT NULL | INTEGER DEFAULT 0 NOT NULL | The metric value |
49+
50+
## Migration Files
51+
52+
### SQLite
53+
54+
| Migration | Description |
55+
|-----------|-------------|
56+
| `20240730183000_torrust_tracker_create_all_tables.sql` | Creates initial tables: `whitelist`, `torrents`, `keys` |
57+
| `20240730183500_torrust_tracker_keys_valid_until_nullable.sql` | Makes `valid_until` column nullable in `keys` table (for permanent keys) |
58+
| `20250527093000_torrust_tracker_new_torrent_aggregate_metrics_table.sql` | Creates `torrent_aggregate_metrics` table for global metrics |
59+
| `20260206120000_torrust_tracker_torrents_completed_non_zero.sql` | Removes rows with completed=0 and adds CHECK constraint (completed >= 1) |
60+
61+
### MySQL
62+
63+
| Migration | Description |
64+
|-----------|-------------|
65+
| `20240730183000_torrust_tracker_create_all_tables.sql` | Creates initial tables: `whitelist`, `torrents`, `keys` |
66+
| `20240730183500_torrust_tracker_keys_valid_until_nullable.sql` | Makes `valid_until` column nullable in `keys` table (for permanent keys) |
67+
| `20250527093000_torrust_tracker_new_torrent_aggregate_metrics_table.sql` | Creates `torrent_aggregate_metrics` table for global metrics |
68+
| `20260206120000_torrust_tracker_torrents_completed_non_zero.sql` | Removes rows with completed=0 and adds CHECK constraint (completed >= 1) |
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- Remove any rows with completed = 0 (should not exist in normal operation)
2+
DELETE FROM torrents WHERE completed = 0;
3+
4+
-- Add CHECK constraint to enforce completed >= 1
5+
-- Note: MySQL 8.0.16+ enforces CHECK constraints
6+
ALTER TABLE torrents ADD CONSTRAINT chk_completed_non_zero CHECK (completed >= 1);
7+
8+
-- Change the default value from 0 to 1
9+
ALTER TABLE torrents ALTER completed SET DEFAULT 1;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- Remove any rows with completed = 0 (should not exist in normal operation)
2+
DELETE FROM torrents WHERE completed = 0;
3+
4+
-- SQLite doesn't support adding CHECK constraints to existing tables directly.
5+
-- We need to recreate the table with the new constraint.
6+
CREATE TABLE IF NOT EXISTS torrents_new (
7+
id INTEGER PRIMARY KEY AUTOINCREMENT,
8+
info_hash TEXT NOT NULL UNIQUE,
9+
completed INTEGER DEFAULT 1 NOT NULL CHECK (completed >= 1)
10+
);
11+
12+
INSERT INTO torrents_new (id, info_hash, completed) SELECT id, info_hash, completed FROM torrents;
13+
14+
DROP TABLE torrents;
15+
16+
ALTER TABLE torrents_new RENAME TO torrents;

packages/tracker-core/src/databases/driver/mysql.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ impl Database for Mysql {
8383
CREATE TABLE IF NOT EXISTS torrents (
8484
id integer PRIMARY KEY AUTO_INCREMENT,
8585
info_hash VARCHAR(40) NOT NULL UNIQUE,
86-
completed INTEGER DEFAULT 0 NOT NULL
86+
completed INTEGER DEFAULT 1 NOT NULL CHECK (completed >= 1)
8787
);"
8888
.to_string();
8989

packages/tracker-core/src/databases/driver/sqlite.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ impl Database for Sqlite {
9898
CREATE TABLE IF NOT EXISTS torrents (
9999
id INTEGER PRIMARY KEY AUTOINCREMENT,
100100
info_hash TEXT NOT NULL UNIQUE,
101-
completed INTEGER DEFAULT 0 NOT NULL
101+
completed INTEGER DEFAULT 1 NOT NULL CHECK (completed >= 1)
102102
);"
103103
.to_string();
104104

packages/tracker-core/src/databases/mod.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,22 @@
4343
//! |---------------|------------------------------------|--------------------------------------|
4444
//! | `id` | 1 | Auto-increment id |
4545
//! | `key` | `IrweYtVuQPGbG9Jzx1DihcPmJGGpVy82` | Authentication token (32 chars) |
46-
//! | `valid_until` | 1672419840 | Timestamp indicating expiration time |
46+
//! | `valid_until` | 1672419840 | Unix timestamp for expiration; NULL for permanent keys |
4747
//!
48-
//! > **NOTICE**: All authentication keys must have an expiration date.
48+
//! > **NOTICE**: Authentication keys can be permanent (no expiration) by setting
49+
//! > `valid_until` to NULL.
50+
//!
51+
//! # Torrent Aggregate Metrics
52+
//!
53+
//! A table for storing global/aggregate metrics that are not tied to a specific
54+
//! torrent. Currently used for tracking the total number of downloads across
55+
//! all torrents.
56+
//!
57+
//! | Field | Sample data | Description |
58+
//! |---------------|------------------------------------------|------------------------------------------|
59+
//! | `id` | 1 | Auto-increment id |
60+
//! | `metric_name` | `torrents_downloads_total` | Unique identifier for the metric |
61+
//! | `value` | 12345 | The metric value (integer) |
4962
pub mod driver;
5063
pub mod error;
5164
pub mod setup;

0 commit comments

Comments
 (0)