Ir al contenido

Escrituras que ocurren juntas ​

Traducción automática

Esta página se tradujo automáticamente del inglés y todavía no la revisó un hablante nativo; las correcciones son bienvenidas en GitHub. Si algo no coincide, vale el original en inglés.

Tarea: mover dinero entre dos cuentas de modo que nunca se pueda debitar de una sin acreditar en la otra.

yaml
# Recipe: several writes that happen together or not at all.
paths:
  components: ./components
  migrations: ./migrations

datasources:
  db:
    driver: sqlite
    database: ./data/bank.db

La tabla de registro rechaza una transferencia de más de 1000 — lo que la última prueba usa para hacer fallar la tercera escritura:

sql
CREATE TABLE accounts (
    id INTEGER PRIMARY KEY,
    owner TEXT NOT NULL,
    balance INTEGER NOT NULL CHECK (balance >= 0)
);

-- Every transfer is logged; one transfer moves at most 1000.
CREATE TABLE transfers (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    from_id INTEGER NOT NULL REFERENCES accounts (id),
    to_id INTEGER NOT NULL REFERENCES accounts (id),
    amount INTEGER NOT NULL CHECK (amount > 0 AND amount <= 1000)
);

INSERT INTO accounts (id, owner, balance) VALUES (1, 'Ana', 1500), (2, 'Bruno', 200);

La página verifica lo que puede explicar (dinero insuficiente) y lo dice con un mensaje flash. Las tres escrituras van dentro de q:transaction: si alguna falla, las anteriores se revierten y la página se detiene con el error.

xml
<q:component name="Bank">
  <q:action name="transfer" method="POST">
    <q:param name="amount" type="integer" required="true" min="1" />

    <q:query name="source" datasource="db">
      SELECT balance FROM accounts WHERE id = 1
    </q:query>
    <q:if condition="source.balance &lt; amount">
      <q:redirect url="/" flash="Not enough money in Ana's account." flashType="error" />
    </q:if>

    <!-- The debit, the credit and the log line commit together. If one of
         them fails, the ones before it are undone and the page stops with
         the error: no money appears or disappears. -->
    <q:transaction datasource="db">
      <q:query name="debit">
        UPDATE accounts SET balance = balance - :amount WHERE id = 1
        <q:param name="amount" value="{amount}" type="integer" />
      </q:query>
      <q:query name="credit">
        UPDATE accounts SET balance = balance + :amount WHERE id = 2
        <q:param name="amount" value="{amount}" type="integer" />
      </q:query>
      <q:query name="logged">
        INSERT INTO transfers (from_id, to_id, amount) VALUES (1, 2, :amount)
        <q:param name="amount" value="{amount}" type="integer" />
      </q:query>
    </q:transaction>

    <q:redirect url="/" flash="Moved {amount} from Ana to Bruno." />
  </q:action>

  <q:query name="accounts" datasource="db">
    SELECT owner, balance FROM accounts ORDER BY id
  </q:query>

  <ui:window title="Bank">
    <q:if condition="flash">
      <ui:alert variant="info">{flash}</ui:alert>
    </q:if>
    <q:loop query="accounts">
      <ui:text>{accounts.owner}: {accounts.balance}</ui:text>
    </q:loop>
    <ui:form on-submit="transfer">
      <ui:input bind="amount" placeholder="Amount" />
      <ui:button variant="primary">Move from Ana to Bruno</ui:button>
    </ui:form>
  </ui:window>
</q:component>
xml
<q:test name="a transfer moves the money and logs it" page="/">
  <test:submit action="transfer" amount="300" />
  <test:expect redirect="/" flash="Moved 300 from Ana to Bruno." />
  <test:expect table="accounts" count="1" where="owner = 'Ana' AND balance = 1200" />
  <test:expect table="accounts" count="1" where="owner = 'Bruno' AND balance = 500" />
  <test:expect table="transfers" count="1" />
</q:test>

<q:test name="the page refuses what the account cannot pay" page="/">
  <test:submit action="transfer" amount="5000" />
  <test:expect flash="Not enough money in Ana's account." />
  <test:expect table="transfers" count="0" />
</q:test>

<q:test name="when the last write fails, the first two are undone" page="/">
  <!-- 1200 passes the balance check, but the log refuses more than 1000:
       the debit and the credit already ran, and are rolled back. -->
  <test:submit action="transfer" amount="1200" />
  <test:expect status="500" />
  <test:expect table="accounts" count="1" where="owner = 'Ana' AND balance = 1500" />
  <test:expect table="accounts" count="1" where="owner = 'Bruno' AND balance = 200" />
  <test:expect table="transfers" count="0" />
</q:test>
text
tests/transfer.test.q
  PASS  a transfer moves the money and logs it
  PASS  the page refuses what the account cannot pay
  PASS  when the last write fails, the first two are undone
3 passed, 0 failed

Ver DB-4.

Licencia MIT · Hecho con VitePress