Skip to content

MySql implementation for atuin-server#3541

Draft
oxo42 wants to merge 1 commit into
atuinsh:mainfrom
oxo42:oxo42/server-mysql
Draft

MySql implementation for atuin-server#3541
oxo42 wants to merge 1 commit into
atuinsh:mainfrom
oxo42:oxo42/server-mysql

Conversation

@oxo42

@oxo42 oxo42 commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

For $reasons, I cannot run Postgres at my company. This PR implements MySQL support for atuin-server.

The test_full_db_story test introduced in #3514 passes.

$ ATUIN_TEST_DB_URI=mysql://root:pass@localhost/atuin_test_ cargo test -p tests-database
...
     Running tests/db_story.rs (target/debug/deps/db_story-65d8b4c97ada8875)

running 1 test
test test_full_db_story ... ok
...

Running a local server and syncing my history does result in

$ echo 'select count(*) from store;' | mysql -uroot -ppass -h127.0.0.1 atuin
count(*)
328

Checks

  • I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle
  • I have checked that there are no existing pull requests for the same thing

@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds MySQL support for the Atuin server database layer. The main changes are:

  • New atuin-server-mysql crate with migrations and a Database implementation.
  • MySQL URI detection and server launch wiring.
  • Test helpers and database story coverage for MySQL.
  • Updated server database documentation and example config.

Confidence Score: 2/5

These issues should be fixed before merging.

  • MySQL credentials can be exposed in startup error logs.
  • New installs can generate a config with public registration enabled.
  • MySQL sync writes can silently skip data while returning success.
  • A bad cache rollout value can crash status requests.

Focus on crates/atuin-server-mysql/src/lib.rs, crates/atuin-server-database/src/lib.rs, and crates/atuin-server/server.toml.

Security Review

Two security-relevant issues were found: MySQL database passwords can be logged on connection failure, and the generated server config now enables open registration by default.

Important Files Changed

Filename Overview
crates/atuin-server-mysql/src/lib.rs Adds the MySQL database implementation; write handling, cache rollout parsing, and cleanup need fixes.
crates/atuin-server-database/src/lib.rs Adds MySQL URI detection but leaves MySQL credentials unredacted in debug output.
crates/atuin-server/server.toml Changes the embedded generated config to active MySQL and open-registration defaults.

Comments Outside Diff (1)

  1. crates/atuin-server-database/src/lib.rs, line 100-111 (link)

    P1 security Redact MySQL URIs
    MySQL is now a supported URI type, but Debug only redacts Postgres settings. Startup failures format settings.db_settings with {:?}, so a failed MySQL connection can print mysql://user:password@... or the read replica password into logs. Redact all URL-based database URIs here.

Reviews (1): Last reviewed commit: "MySql implementation for atuin-server" | Re-trigger Greptile

Comment on lines +8 to +13
open_registration = true

## URI for postgres (using development creds here)
# db_uri="postgres://username:password@localhost/atuin"
# db_uri="sqlite:///config/atuin-server.db"
db_uri = "mysql://root:foobar@127.0.0.1/atuin"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Unsafe generated config
This file is embedded as the generated server config for new installs. With these active values, a fresh server writes open_registration = true and a local MySQL root URI by default, so it can start with public registration enabled and a development database target unless the operator edits the file first. Keep these as commented examples and leave registration disabled by default.

Comment on lines +405 to +407
let idx_cache_rollout = std::env::var("IDX_CACHE_ROLLOUT").unwrap_or("0".to_string());
let idx_cache_rollout = idx_cache_rollout.parse::<f64>().unwrap_or(0.0);
let use_idx_cache = rand::thread_rng().gen_bool(idx_cache_rollout / 100.0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Clamp cache rollout
gen_bool panics when the probability is outside 0.0..=1.0. If IDX_CACHE_ROLLOUT is set to 101 or -1, every status call that reaches this code can crash the request task instead of falling back safely. Clamp the parsed value to 0..=100 or reject invalid values before dividing.

Suggested change
let idx_cache_rollout = std::env::var("IDX_CACHE_ROLLOUT").unwrap_or("0".to_string());
let idx_cache_rollout = idx_cache_rollout.parse::<f64>().unwrap_or(0.0);
let use_idx_cache = rand::thread_rng().gen_bool(idx_cache_rollout / 100.0);
let idx_cache_rollout = std::env::var("IDX_CACHE_ROLLOUT").unwrap_or("0".to_string());
let idx_cache_rollout = idx_cache_rollout.parse::<f64>().unwrap_or(0.0).clamp(0.0, 100.0);
let use_idx_cache = rand::thread_rng().gen_bool(idx_cache_rollout / 100.0);

Comment on lines +287 to +304
let result = sqlx::query(
"insert ignore into store
(id, client_id, host, idx, timestamp, version, tag, data, cek, user_id)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
",
)
.bind(id)
.bind(i.id)
.bind(i.host.id)
.bind(i.idx as i64)
.bind(i.timestamp as i64) // throwing away some data, but i64 is still big in terms of time
.bind(&i.version)
.bind(&i.tag)
.bind(&i.data.data)
.bind(&i.data.content_encryption_key)
.bind(user.id)
.execute(&mut *tx)
.await?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Avoid ignored writes
INSERT IGNORE suppresses more than duplicate-key conflicts in MySQL. If a record has an out-of-range value, truncated tag, or another data error, MySQL can skip or coerce the row and this method still returns Ok(()); the sync client then believes the encrypted record was stored. Use duplicate-key-only handling and let other write errors fail the upload.

Comment on lines +496 to +508
sqlx::query(
"insert ignore into history
(client_id, user_id, hostname, timestamp, data)
values (?, ?, ?, ?, ?)
",
)
.bind(client_id)
.bind(i.user_id)
.bind(hostname)
.bind(i.timestamp)
.bind(data)
.execute(&mut *tx)
.await?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Avoid ignored history
INSERT IGNORE can hide non-duplicate MySQL errors here too. A history row with invalid or truncated data can be skipped while add_history commits and returns success, so the client drops local retry state for data the server did not actually store. Limit the ignore behavior to duplicate client_id conflicts and propagate other database errors.

Comment on lines +195 to +207
.await?;

sqlx::query("delete from users where id = ?")
.bind(u.id)
.execute(&self.pool)
.await?;

Ok(())
}

#[instrument(skip_all)]
async fn delete_history(&self, user: &User, id: String) -> DbResult<()> {
sqlx::query(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Clear cache rows
Deleting a user removes store rows but leaves that user's store_idx_cache rows behind. When the cache rollout is enabled, stale cache entries can be returned for a reused or restored user id, and the table will also grow with deleted accounts. Delete store_idx_cache for the user in this cleanup path.

@oxo42 oxo42 marked this pull request as draft June 19, 2026 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant