Summary
open database returns a multi-connection pool, and every query/execute
acquires a connection independently. There is no transaction construct in the
language, so the natural workaround is to send BEGIN / COMMIT / ROLLBACK
through execute.
That does not work, and it does not fail loudly. The statements run on
different pooled connections, so writes escape the transaction entirely while
appearing to succeed.
Reproduction
open database at "sqlite://tx.db" as db
store made as execute db with "CREATE TABLE projects (slug TEXT)"
store t1 as execute db with "BEGIN"
store ins as execute db with "INSERT INTO projects (slug) VALUES ('should-vanish')"
store t2 as execute db with "ROLLBACK"
store rows as query db with "SELECT slug FROM projects"
display "rows surviving rollback: " with length of rows
close database db
Expected: rows surviving rollback: 0
Actual: rows surviving rollback: 1
No error is raised at any point. The ROLLBACK is silently a no-op.
Tested on WFL 26.7.57 (Windows). The same applies to PostgreSQL and MySQL,
which use MAX_POOL_CONNECTIONS in src/interpreter/database.rs. In-memory
SQLite happens to hide it because that path is special-cased to a single
connection — so a program can pass its in-memory tests and lose data in
production.
Why this matters
This is a silent data-integrity failure rather than a missing feature. A user
who writes the code above has every reason to believe they have a transaction:
nothing errors, nothing warns, and it works under the in-memory SQLite most
tests use.
Any multi-statement write becomes non-atomic. A crash mid-sequence leaves
partial state with no way to undo it, and there is currently no way to express
an atomic multi-statement write in WFL at all.
Possible directions
- A transaction construct, which would be the real fix — something like a
block that holds one connection for its duration:
in transaction on db:
execute db with "INSERT ..."
execute db with "UPDATE ..."
end transaction
- Reject transaction-control statements in
execute with a clear error
pointing at the above. Much smaller change, and it converts silent
corruption into a loud failure.
Even option 2 alone would be a large improvement, since the current behavior's
main danger is that it looks like it works.
Workaround for others hitting this
Restructure so no multi-statement atomicity is needed: a single-statement
claim, then a single-statement commit point, with a status column making
interrupted work detectable and resumable.
Summary
open databasereturns a multi-connection pool, and everyquery/executeacquires a connection independently. There is no transaction construct in the
language, so the natural workaround is to send
BEGIN/COMMIT/ROLLBACKthrough
execute.That does not work, and it does not fail loudly. The statements run on
different pooled connections, so writes escape the transaction entirely while
appearing to succeed.
Reproduction
Expected:
rows surviving rollback: 0Actual:
rows surviving rollback: 1No error is raised at any point. The
ROLLBACKis silently a no-op.Tested on WFL 26.7.57 (Windows). The same applies to PostgreSQL and MySQL,
which use
MAX_POOL_CONNECTIONSinsrc/interpreter/database.rs. In-memorySQLite happens to hide it because that path is special-cased to a single
connection — so a program can pass its in-memory tests and lose data in
production.
Why this matters
This is a silent data-integrity failure rather than a missing feature. A user
who writes the code above has every reason to believe they have a transaction:
nothing errors, nothing warns, and it works under the in-memory SQLite most
tests use.
Any multi-statement write becomes non-atomic. A crash mid-sequence leaves
partial state with no way to undo it, and there is currently no way to express
an atomic multi-statement write in WFL at all.
Possible directions
block that holds one connection for its duration:
executewith a clear errorpointing at the above. Much smaller change, and it converts silent
corruption into a loud failure.
Even option 2 alone would be a large improvement, since the current behavior's
main danger is that it looks like it works.
Workaround for others hitting this
Restructure so no multi-statement atomicity is needed: a single-statement
claim, then a single-statement commit point, with a
statuscolumn makinginterrupted work detectable and resumable.