Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `pdo/column-meta` - wrap `PDOStatement::getColumnMeta`; returns a 0-indexed column's metadata as a map, or `nil` when unavailable ([#15]).
- `pdo/statement-seq` - expose a statement's rows as a lazy seq of maps (the Phel-idiomatic take on `PDOStatement::getIterator`), so callers can `map`/`reduce`/`take` without materialising the whole result set ([#16]).
- `pdo/next-rowset` - wrap `PDOStatement::nextRowset`; advances a multi-rowset statement (e.g. stored procedures on MySQL/Postgres) ([#17]).
- `pdo/with-transaction` macro - runs a body in a transaction, committing on success (returning the last body value) and rolling back + re-throwing on error; runs inline when already in a transaction ([#20]).

### Changed

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ All functions live in the `phel.pdo` namespace.
| `last-insert-id` | `(last-insert-id conn)` | ID of the last inserted row. |
| `begin` / `commit` / `rollback` | `(begin conn)` … | Transaction control. |
| `in-transaction` | `(in-transaction conn)` | `true` if a transaction is active. |
| `with-transaction` | `(with-transaction conn & body)` | Run `body` in a transaction: commit + return last value, or rollback + re-throw. Runs inline if already in a transaction. |
| `get-attribute` / `set-attribute` | `(get-attribute handle attr)` / `(set-attribute handle attr value)` | PDO attribute access; `handle` is a connection or a statement. |
| `get-available-drivers` | `(get-available-drivers conn)` | Vector of installed PDO drivers. |
| `error-code` | `(error-code handle)` | SQLSTATE of the last operation; `handle` is a connection or a statement. |
Expand Down
14 changes: 14 additions & 0 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ Available types: `\PDO/PARAM_STR` (default), `\PDO/PARAM_INT`, `\PDO/PARAM_BOOL`

## Transactions

Use `with-transaction` to bracket a body: it commits on success (returning the
last body value) and rolls back + re-throws on any exception.

```clojure
(pdo/with-transaction conn
(pdo/insert conn :accounts {:name "a" :balance 100})
(pdo/insert conn :accounts {:name "b" :balance 0}))
```

If `conn` is already in a transaction, the body runs inline - no nested
`begin`/`commit` (v1 uses no savepoints).

The manual primitives remain available when you need finer control:

```clojure
(pdo/begin conn)
(try
Expand Down
18 changes: 18 additions & 0 deletions src/pdo.phel
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,24 @@
[conn]
(php/-> (conn :pdo) (inTransaction)))

(defmacro with-transaction
"Runs body inside a transaction on conn: commits on success and returns the last
body value, or rolls back and re-throws on error. If conn is already in a
transaction, runs body inline without a nested begin/commit."
[conn & body]
`(let [conn# ~conn]
(if (in-transaction conn#)
(do ~@body)
(do
(begin conn#)
(try
(let [result# (do ~@body)]
(commit conn#)
result#)
(catch \Throwable e#
(rollback conn#)
(throw e#)))))))

(defn get-available-drivers
"Return an array of available PDO drivers"
[conn]
Expand Down
31 changes: 31 additions & 0 deletions tests/pdo_test.phel
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,37 @@
(pdo/rollback *conn*)
(is (= 0 (pdo/fetch-column (pdo/query *conn* "select count(*) from t1")))))

(deftest with-transaction-commits-and-returns-last-value
(create-t1 *conn*)
(let [ret (pdo/with-transaction *conn*
(insert-name *conn* "phel")
:done)]
(is (= :done ret))
(is (= 1 (pdo/fetch-column (pdo/query *conn* "select count(*) from t1"))))))

(deftest with-transaction-rolls-back-and-rethrows-on-error
(create-t1 *conn*)
(let [threw (try
(pdo/with-transaction *conn*
(insert-name *conn* "phel")
(throw (php/new \Exception "boom")))
false
(catch \Exception _e true))]
(is (= true threw))
(is (= 0 (pdo/fetch-column (pdo/query *conn* "select count(*) from t1"))))))

(deftest with-transaction-nested-runs-inline
(create-t1 *conn*)
(pdo/begin *conn*)
;; already in a transaction: the inner call must not begin/commit again
(pdo/with-transaction *conn*
(insert-name *conn* "phel"))
(testing "inner call left the outer transaction open"
(is (= true (pdo/in-transaction *conn*))))
(pdo/rollback *conn*)
(testing "outer rollback still reverts the inline insert"
(is (= 0 (pdo/fetch-column (pdo/query *conn* "select count(*) from t1"))))))

;; -----
;; Query
;; -----
Expand Down
Loading