POST /transactions/filter never returns parent_transaction — the column is omitted from the filter query
#292 opened on May 18, 2026
Repository metrics
- Stars
- (475 stars)
- PR merge metrics
- (PR metrics pending)
Description
Bug: POST /transactions/filter never returns parent_transaction — the column is omitted from the filter query
Type: bug
Component: database/transaction.go — GetAllTransactionsWithFilterAndOptions
Version checked: main @ e0f8676 (2026-05-18)
Summary
GET /transactions/{id} returns a transaction's parent_transaction
correctly. POST /transactions/filter returns the same transaction
with parent_transaction as an empty string — even when you filter
on parent_transaction and the row matches.
The two endpoints share the transformTransaction serializer, so the
divergence is not in serialization — it is in the SQL: the filter
query's column list omits parent_transaction entirely, so the scanned
struct's field is never populated and serializes as "".
Impact
parent_transaction is the link used to walk a transaction's lifecycle
(QUEUED → INFLIGHT → APPLIED/VOID, and refund → original). The
documented way to fetch an inflight's terminal children is to filter on
parent_transaction (see transactions/parent-transactions.mdx). That
exact query returns every child with an empty parent_transaction,
so a consumer cannot reconstruct the chain from a filter result — it is
forced into an extra GET /transactions/{id} per row to recover the
link the filter could have returned.
Root cause
database/transaction.go, GetAllTransactionsWithFilterAndOptions:
// Determine select fields based on whether count is requested
selectFields := "transaction_id, source, reference, amount, precise_amount, precision, currency, destination, description, status, hash, created_at, effective_date, meta_data"
if opts != nil && opts.IncludeCount {
selectFields += ", COUNT(*) OVER() AS total_count"
}
14 columns — no parent_transaction. The matching rows.Scan
blocks (both the IncludeCount and the plain branch) likewise never
scan it.
For contrast, GetTransaction (single fetch) does select it:
SELECT transaction_id, source, reference, amount, precise_amount, precision,
currency, destination, description, status, created_at, meta_data,
parent_transaction, hash
FROM blnk.transactions WHERE transaction_id = $1
The column exists and is indexed:
sql/1708676327.sql/sql/1714213688.sql—parent_transaction TEXTsql/1721227454.sql:17—CREATE INDEX idx_transactions_parent_transaction
So this is a plain omission in one hand-maintained column list, not a schema or design limitation.
Steps to reproduce
# 1. Create an inflight transaction, then commit it — the committed
# child carries parent_transaction = the inflight id.
INF=$(curl -s -X POST http://localhost:5001/transactions \
-H 'X-Blnk-Key: <key>' -H 'Content-Type: application/json' \
-d '{"amount":100,"precision":100,"currency":"USD","reference":"repro-'"$(date +%s)"'",
"source":"@World","destination":"<balance_id>","description":"x",
"inflight":true,"skip_queue":true,"allow_overdraft":true}')
INFID=$(echo "$INF" | jq -r .transaction_id)
CHILD=$(curl -s -X PUT "http://localhost:5001/transactions/inflight/$INFID" \
-H 'X-Blnk-Key: <key>' -H 'Content-Type: application/json' \
-d '{"status":"commit"}')
CHILDID=$(echo "$CHILD" | jq -r .transaction_id)
# 2. GET the child — parent_transaction is populated.
curl -s "http://localhost:5001/transactions/$CHILDID" -H 'X-Blnk-Key: <key>' | jq .parent_transaction
# -> "txn_<the inflight id>" ✅
# 3. Filter for the SAME child by parent_transaction — it matches the
# row, yet the returned object's parent_transaction is empty.
curl -s -X POST http://localhost:5001/transactions/filter \
-H 'X-Blnk-Key: <key>' -H 'Content-Type: application/json' \
-d '{"filters":[{"field":"parent_transaction","operator":"eq","value":"'"$INFID"'"}]}' \
| jq '.[0].parent_transaction'
# -> "" ❌ BUG
Observed
| Endpoint | parent_transaction in the response |
|---|---|
GET /transactions/{id} |
txn_… (correct) |
POST /transactions/filter |
"" (empty) |
Expected
Both endpoints describe the same transaction and should return the same
parent_transaction value. The Filter API must return the field — it
can already be filtered on it.
Suggested fix
Add parent_transaction to selectFields in
GetAllTransactionsWithFilterAndOptions, and add the matching
&transaction.ParentTransaction to both rows.Scan blocks
(the IncludeCount branch and the plain branch). One column.