rails console --sandbox protects the database and nothing else
rails console --sandbox is the flag people reach for when they have to touch production and
would rather not explain themselves afterwards. It does what it says, and the gap between what it
protects and what people assume it protects is where the trouble is.
Measured on Rails 8.1.4.
It really does roll back
before the session ["existed before"]
inside the sandbox ["existed before", "created in sandbox"]
after exiting ["existed before"]
No ambiguity: the row created inside is gone afterwards and the row that existed before survived. The first version of this test counted rows instead of naming them and gave an answer that looked wrong for a reason that had nothing to do with the sandbox, which is its own small lesson about counting things.
What's worth seeing is the log while you're in there:
Survey Create (0.3ms) INSERT INTO "surveys" ("title", "created_at", "updated_at")
VALUES ('written in sandbox', ...) RETURNING "id"
That statement executed. You get an id back, the row is real for the rest of your session, and
other connections can't see it. Rails opened a transaction when the console started and issues a
ROLLBACK when you exit. The sandbox isn't preventing writes, it's undoing them.
Which is the whole reason the next section exists.
Everything else happened
In the same sandbox session, one line that wasn't a database write:
File.write("/tmp/side_effect.txt", "written from the sandbox")
After exiting, the row was gone and the file was still there, contents intact.
That's the shape of the thing. The transaction covers the database connection. It does not cover the filesystem, the network, or anything your code calls out to. So from a sandbox console:
- A
deliver_nowsent the email. It's in someone's inbox. - An HTTP call to a payment provider charged the card.
- A job pushed to a real Redis-backed queue is sitting there waiting for a worker, and the worker will run it against a database where the record it references never existed.
That last one is the nastiest, because it fails in another process minutes later, and the person debugging it has no reason to connect it to a console session that left no trace.
The cost of sitting there
Because a sandbox is an open transaction, leaving one open has the same consequences as leaving any transaction open.
On Postgres the rows you have written to stay locked against other writers, and autovacuum can't clean up dead tuples newer than your transaction's snapshot for as long as it's alive. A console somebody left open over lunch is a long-running transaction, and long-running transactions on a busy table are a real operational problem rather than a theoretical one.
Sequences are the small version of the same thing. The writes execute, so nextval advances, and
the rollback doesn't give the numbers back. You'll see a gap in your ids. It's harmless and it's
worth knowing before you go looking for the missing records.
So what is it for
One thing, and it's genuinely valuable: checking what a statement does before you run it for real.
Write the update_all, see how many rows it reports, read a couple of them back, decide you had
the condition wrong, fix it, try again. Exit. Nothing you did survives, and you now know exactly
what the real version will do. That workflow is worth the flag on its own.
What it isn't is a safe harness for exercising application code. The moment you call the service object, the mailer or the job, you've left the part the transaction covers. If what you want is to try a workflow safely, the answer is a staging environment with its own data, not a flag on a production console.
And the honest operational position: a production console with no sandbox is a loaded gun, a production console with a sandbox is a loaded gun with the database half covered, and the thing that actually protects you is not being in one. Most of what people use a console for on production is a question that a read replica, an admin page or a well-chosen log line answers better and leaves a trace of.
What this page doesn't cover
No Postgres. The lab app runs SQLite, so the lock and vacuum behaviour above is reasoned from how transactions work rather than measured, and the sequence gap is named for the same reason. Somebody on Postgres should confirm both.
Nothing here covers reload!, the app and helper objects the console gives you, or
ActiveRecord::Base.connection.execute for the cases where you want to bypass the model entirely.
Nor the multi-database case, where the sandbox opens a transaction per connection and the
interesting question is what happens when only one of them rolls back.
Comments
No comments yet. Be the first.