Skip to content

Transaction control via execute is silently ignored: statements run on different pooled connections #664

Description

@logbie

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

  1. 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
    
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

IN WORKPresently working on thisenhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions