Skip to content

Commit c5cdd26

Browse files
committed
refactor: migrate torrents table to completed_v1 with optimized schema
Rename the `torrents` table to `completed_v1` with improved storage efficiency and semantic clarity. This change removes the unnecessary auto-increment `id` column, uses `info_hash` directly as the PRIMARY KEY, and renames the `completed` column to `count` for better clarity. Database-specific optimizations: - **SQLite**: Use `WITHOUT ROWID` optimization since info_hash is already a suitable primary key (40-char hex string). This eliminates the hidden rowid column and reduces storage overhead. - **MySQL**: Store info_hash as `BINARY(20)` instead of `VARCHAR(40)`, reducing storage from 40 bytes to 20 bytes per entry and improving index performance. Changes: - Add migration scripts for both SQLite and MySQL to: - Create new `completed_v1` table with optimized schema - Migrate existing data (converting hex to binary for MySQL) - Drop old `torrents` table - Update hardcoded table creation SQL in database drivers to match new schema - Update all torrent download queries to use `completed_v1` table name and `count` column name - Update MySQL driver to use binary storage with `UNHEX()`/bytes() for info_hash operations - Document the new schema in migrations README with storage format details for both databases The migration preserves all existing torrent download data while improving storage efficiency and query performance. The new table name `completed_v1` better reflects its purpose of tracking download completion counts.
1 parent f935f54 commit c5cdd26

5 files changed

Lines changed: 70 additions & 46 deletions

File tree

packages/tracker-core/migrations/README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,16 @@ Stores whitelisted torrent infohashes for private/whitelisted mode.
1818

1919
> **Note**: SQLite uses `WITHOUT ROWID` optimization. MySQL stores as binary for efficiency.
2020
21-
### 2. `torrents`
21+
### 2. `completed_v1`
2222

23-
Stores per-torrent metrics (completed download count).
23+
Stores per-torrent download completion counts.
2424

2525
| Column | SQLite Type | MySQL Type | Description |
2626
|--------|-------------|------------|-------------|
27-
| `id` | INTEGER PRIMARY KEY AUTOINCREMENT | integer PRIMARY KEY AUTO_INCREMENT | Auto-increment ID |
28-
| `info_hash` | TEXT NOT NULL UNIQUE | VARCHAR(40) NOT NULL UNIQUE | BitTorrent V1 infohash (40-char hex string) |
29-
| `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) |
27+
| `info_hash` | TEXT PRIMARY KEY NOT NULL | BINARY(20) PRIMARY KEY NOT NULL | BitTorrent V1 infohash (SQLite: 40-char hex string, MySQL: 20-byte binary) |
28+
| `count` | INTEGER DEFAULT 1 NOT NULL CHECK (count >= 1) | INTEGER DEFAULT 1 NOT NULL CHECK (count >= 1) | Number of times the torrent has been fully downloaded (minimum 1) |
29+
30+
> **Note**: SQLite uses `WITHOUT ROWID` optimization. MySQL stores info_hash as binary for efficiency.
3031
3132
### 3. `keys`
3233

@@ -59,6 +60,7 @@ Stores global/aggregate metrics not tied to specific torrents (e.g., total downl
5960
| `20250527093000_torrust_tracker_new_torrent_aggregate_metrics_table.sql` | Creates `torrent_aggregate_metrics` table for global metrics |
6061
| `20260206120000_torrust_tracker_torrents_completed_non_zero.sql` | Removes rows with completed=0 and adds CHECK constraint (completed >= 1) |
6162
| `20260206130000_torrust_tracker_whitelist_without_rowid.sql` | Optimizes whitelist table: removes `id` column, uses `info_hash` as PRIMARY KEY with WITHOUT ROWID |
63+
| `20260206140000_torrust_tracker_completed_v1.sql` | Migrates `torrents` to `completed_v1`: removes `id` column, renames `completed` to `count`, uses `info_hash` as PRIMARY KEY with WITHOUT ROWID |
6264

6365
### MySQL
6466

@@ -69,3 +71,4 @@ Stores global/aggregate metrics not tied to specific torrents (e.g., total downl
6971
| `20250527093000_torrust_tracker_new_torrent_aggregate_metrics_table.sql` | Creates `torrent_aggregate_metrics` table for global metrics |
7072
| `20260206120000_torrust_tracker_torrents_completed_non_zero.sql` | Removes rows with completed=0 and adds CHECK constraint (completed >= 1) |
7173
| `20260206130000_torrust_tracker_whitelist_binary.sql` | Optimizes whitelist table: removes `id` column, uses BINARY(20) `info_hash` as PRIMARY KEY |
74+
| `20260206140000_torrust_tracker_completed_v1.sql` | Migrates `torrents` to `completed_v1`: removes `id` column, renames `completed` to `count`, uses BINARY(20) `info_hash` as PRIMARY KEY |
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- Migrate torrents table to completed_v1 with BINARY(20) info_hash as PRIMARY KEY
2+
-- Rename 'completed' column to 'count'
3+
-- BINARY(20) stores the raw 20-byte infohash instead of 40-char hex string
4+
5+
CREATE TABLE IF NOT EXISTS completed_v1 (
6+
info_hash BINARY(20) PRIMARY KEY NOT NULL,
7+
count INTEGER DEFAULT 1 NOT NULL CHECK (count >= 1)
8+
);
9+
10+
-- Convert existing hex strings to binary
11+
INSERT INTO completed_v1 (info_hash, count) SELECT UNHEX(info_hash), completed FROM torrents;
12+
13+
DROP TABLE torrents;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- Migrate torrents table to completed_v1 with info_hash as PRIMARY KEY and WITHOUT ROWID optimization
2+
-- Rename 'completed' column to 'count'
3+
CREATE TABLE IF NOT EXISTS completed_v1 (
4+
info_hash TEXT PRIMARY KEY NOT NULL,
5+
count INTEGER DEFAULT 1 NOT NULL CHECK (count >= 1)
6+
) WITHOUT ROWID;
7+
8+
INSERT INTO completed_v1 (info_hash, count) SELECT info_hash, completed FROM torrents;
9+
10+
DROP TABLE torrents;

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

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
//! based on a URL, creates the necessary tables (for torrent metrics, torrent
66
//! whitelist, and authentication keys), and implements all CRUD operations
77
//! required by the persistence layer.
8-
use std::str::FromStr;
98
use std::time::Duration;
109

1110
use bittorrent_primitives::info_hash::InfoHash;
@@ -78,11 +77,10 @@ impl Database for Mysql {
7877
);"
7978
.to_string();
8079

81-
let create_torrents_table = "
82-
CREATE TABLE IF NOT EXISTS torrents (
83-
id integer PRIMARY KEY AUTO_INCREMENT,
84-
info_hash VARCHAR(40) NOT NULL UNIQUE,
85-
completed INTEGER DEFAULT 1 NOT NULL CHECK (completed >= 1)
80+
let create_completed_table = "
81+
CREATE TABLE IF NOT EXISTS completed_v1 (
82+
info_hash BINARY(20) PRIMARY KEY NOT NULL,
83+
count INTEGER DEFAULT 1 NOT NULL CHECK (count >= 1)
8684
);"
8785
.to_string();
8886

@@ -108,8 +106,8 @@ impl Database for Mysql {
108106

109107
let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?;
110108

111-
conn.query_drop(&create_torrents_table)
112-
.expect("Could not create torrents table.");
109+
conn.query_drop(&create_completed_table)
110+
.expect("Could not create completed_v1 table.");
113111
conn.query_drop(&create_torrent_aggregate_metrics_table)
114112
.expect("Could not create create_torrent_aggregate_metrics_table table.");
115113
conn.query_drop(&create_keys_table).expect("Could not create keys table.");
@@ -125,8 +123,8 @@ impl Database for Mysql {
125123
DROP TABLE `whitelist_v1`;"
126124
.to_string();
127125

128-
let drop_torrents_table = "
129-
DROP TABLE `torrents`;"
126+
let drop_completed_table = "
127+
DROP TABLE `completed_v1`;"
130128
.to_string();
131129

132130
let drop_keys_table = "
@@ -137,8 +135,8 @@ impl Database for Mysql {
137135

138136
conn.query_drop(&drop_whitelist_table)
139137
.expect("Could not drop `whitelist_v1` table.");
140-
conn.query_drop(&drop_torrents_table)
141-
.expect("Could not drop `torrents` table.");
138+
conn.query_drop(&drop_completed_table)
139+
.expect("Could not drop `completed_v1` table.");
142140
conn.query_drop(&drop_keys_table).expect("Could not drop `keys` table.");
143141

144142
Ok(())
@@ -149,10 +147,10 @@ impl Database for Mysql {
149147
let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?;
150148

151149
let torrents = conn.query_map(
152-
"SELECT info_hash, completed FROM torrents",
153-
|(info_hash_string, completed): (String, u32)| {
154-
let info_hash = InfoHash::from_str(&info_hash_string).unwrap();
155-
(info_hash, completed)
150+
"SELECT info_hash, count FROM completed_v1",
151+
|(info_hash_bytes, count): (Vec<u8>, u32)| {
152+
let info_hash = InfoHash::from_bytes(&info_hash_bytes);
153+
(info_hash, count)
156154
},
157155
)?;
158156

@@ -164,8 +162,8 @@ impl Database for Mysql {
164162
let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?;
165163

166164
let query = conn.exec_first::<u32, _, _>(
167-
"SELECT completed FROM torrents WHERE info_hash = :info_hash",
168-
params! { "info_hash" => info_hash.to_hex_string() },
165+
"SELECT count FROM completed_v1 WHERE info_hash = :info_hash",
166+
params! { "info_hash" => info_hash.bytes() },
169167
);
170168

171169
let persistent_torrent = query?;
@@ -175,24 +173,25 @@ impl Database for Mysql {
175173

176174
/// Refer to [`databases::Database::save_persistent_torrent`](crate::core::databases::Database::save_persistent_torrent).
177175
fn save_torrent_downloads(&self, info_hash: &InfoHash, completed: u32) -> Result<(), Error> {
178-
const COMMAND : &str = "INSERT INTO torrents (info_hash, completed) VALUES (:info_hash_str, :completed) ON DUPLICATE KEY UPDATE completed = VALUES(completed)";
176+
const COMMAND: &str = "INSERT INTO completed_v1 (info_hash, count) VALUES (:info_hash_bytes, :count) ON DUPLICATE KEY UPDATE count = VALUES(count)";
179177

180178
let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?;
181179

182-
let info_hash_str = info_hash.to_string();
180+
let info_hash_bytes = info_hash.bytes();
181+
let count = completed;
183182

184-
Ok(conn.exec_drop(COMMAND, params! { info_hash_str, completed })?)
183+
Ok(conn.exec_drop(COMMAND, params! { info_hash_bytes, count })?)
185184
}
186185

187186
/// Refer to [`databases::Database::increase_number_of_downloads`](crate::core::databases::Database::increase_number_of_downloads).
188187
fn increase_downloads_for_torrent(&self, info_hash: &InfoHash) -> Result<(), Error> {
189188
let mut conn = self.pool.get().map_err(|e| (e, DRIVER))?;
190189

191-
let info_hash_str = info_hash.to_string();
190+
let info_hash_bytes = info_hash.bytes();
192191

193192
conn.exec_drop(
194-
"UPDATE torrents SET completed = completed + 1 WHERE info_hash = :info_hash_str",
195-
params! { info_hash_str },
193+
"UPDATE completed_v1 SET count = count + 1 WHERE info_hash = :info_hash_bytes",
194+
params! { info_hash_bytes },
196195
)?;
197196

198197
Ok(())

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

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,12 @@ impl Database for Sqlite {
9393
) WITHOUT ROWID;"
9494
.to_string();
9595

96-
let create_torrents_table = "
97-
CREATE TABLE IF NOT EXISTS torrents (
98-
id INTEGER PRIMARY KEY AUTOINCREMENT,
99-
info_hash TEXT NOT NULL UNIQUE,
100-
completed INTEGER DEFAULT 1 NOT NULL CHECK (completed >= 1)
101-
);"
102-
.to_string();
96+
let create_completed_table = "
97+
CREATE TABLE IF NOT EXISTS completed_v1 (
98+
info_hash TEXT PRIMARY KEY NOT NULL,
99+
count INTEGER DEFAULT 1 NOT NULL CHECK (count >= 1)
100+
) WITHOUT ROWID;"
101+
.to_string();
103102

104103
let create_torrent_aggregate_metrics_table = "
105104
CREATE TABLE IF NOT EXISTS torrent_aggregate_metrics (
@@ -121,7 +120,7 @@ impl Database for Sqlite {
121120

122121
conn.execute(&create_whitelist_table, [])?;
123122
conn.execute(&create_keys_table, [])?;
124-
conn.execute(&create_torrents_table, [])?;
123+
conn.execute(&create_completed_table, [])?;
125124
conn.execute(&create_torrent_aggregate_metrics_table, [])?;
126125

127126
Ok(())
@@ -133,8 +132,8 @@ impl Database for Sqlite {
133132
DROP TABLE whitelist_v1;"
134133
.to_string();
135134

136-
let drop_torrents_table = "
137-
DROP TABLE torrents;"
135+
let drop_completed_table = "
136+
DROP TABLE completed_v1;"
138137
.to_string();
139138

140139
let drop_keys_table = "
@@ -144,7 +143,7 @@ impl Database for Sqlite {
144143
let conn = self.pool.get().map_err(|e| (e, DRIVER))?;
145144

146145
conn.execute(&drop_whitelist_table, [])
147-
.and_then(|_| conn.execute(&drop_torrents_table, []))
146+
.and_then(|_| conn.execute(&drop_completed_table, []))
148147
.and_then(|_| conn.execute(&drop_keys_table, []))?;
149148

150149
Ok(())
@@ -154,13 +153,13 @@ impl Database for Sqlite {
154153
fn load_all_torrents_downloads(&self) -> Result<NumberOfDownloadsBTreeMap, Error> {
155154
let conn = self.pool.get().map_err(|e| (e, DRIVER))?;
156155

157-
let mut stmt = conn.prepare("SELECT info_hash, completed FROM torrents")?;
156+
let mut stmt = conn.prepare("SELECT info_hash, count FROM completed_v1")?;
158157

159158
let torrent_iter = stmt.query_map([], |row| {
160159
let info_hash_string: String = row.get(0)?;
161160
let info_hash = InfoHash::from_str(&info_hash_string).unwrap();
162-
let completed: u32 = row.get(1)?;
163-
Ok((info_hash, completed))
161+
let count: u32 = row.get(1)?;
162+
Ok((info_hash, count))
164163
})?;
165164

166165
Ok(torrent_iter.filter_map(std::result::Result::ok).collect())
@@ -170,7 +169,7 @@ impl Database for Sqlite {
170169
fn load_torrent_downloads(&self, info_hash: &InfoHash) -> Result<Option<NumberOfDownloads>, Error> {
171170
let conn = self.pool.get().map_err(|e| (e, DRIVER))?;
172171

173-
let mut stmt = conn.prepare("SELECT completed FROM torrents WHERE info_hash = ?")?;
172+
let mut stmt = conn.prepare("SELECT count FROM completed_v1 WHERE info_hash = ?")?;
174173

175174
let mut rows = stmt.query([info_hash.to_hex_string()])?;
176175

@@ -187,7 +186,7 @@ impl Database for Sqlite {
187186
let conn = self.pool.get().map_err(|e| (e, DRIVER))?;
188187

189188
let insert = conn.execute(
190-
"INSERT INTO torrents (info_hash, completed) VALUES (?1, ?2) ON CONFLICT(info_hash) DO UPDATE SET completed = ?2",
189+
"INSERT INTO completed_v1 (info_hash, count) VALUES (?1, ?2) ON CONFLICT(info_hash) DO UPDATE SET count = ?2",
191190
[info_hash.to_string(), completed.to_string()],
192191
)?;
193192

@@ -206,7 +205,7 @@ impl Database for Sqlite {
206205
let conn = self.pool.get().map_err(|e| (e, DRIVER))?;
207206

208207
let _ = conn.execute(
209-
"UPDATE torrents SET completed = completed + 1 WHERE info_hash = ?",
208+
"UPDATE completed_v1 SET count = count + 1 WHERE info_hash = ?",
210209
[info_hash.to_string()],
211210
)?;
212211

0 commit comments

Comments
 (0)