Skip to main content

oracledb_protocol/thin/
sessionless.rs

1#![forbid(unsafe_code)]
2
3use super::*;
4
5/// Body of the transaction-switch message (reference impl/thin/messages/
6/// tpc_switch.pyx `_write_message`), shared by the direct function call and the
7/// piggyback forms. `xid` is the (format_id, global_txn_id) of a sessionless
8/// transaction being started; `None` for a suspend/detach which carries no XID.
9pub(crate) fn write_tpc_txn_switch_body(
10    writer: &mut TtcWriter,
11    operation: u32,
12    flags: u32,
13    timeout: u32,
14    xid: Option<&[u8]>,
15) {
16    writer.write_ub4(operation);
17    writer.write_u8(0); // pointer (transaction context)
18    writer.write_ub4(0); // transaction context length
19    if let Some(global_txn_id) = xid {
20        // sessionless transactions send only a global transaction id; the
21        // branch qualifier is empty and the combined value is right-padded
22        // with zero bytes to 128 bytes (tpc_switch.pyx:80-81).
23        let mut xid_bytes = global_txn_id.to_vec();
24        xid_bytes.resize(128, 0);
25        writer.write_ub4(SESSIONLESS_FORMAT_ID);
26        writer.write_ub4(u32::try_from(global_txn_id.len()).unwrap_or(0)); // global txn id len
27        writer.write_ub4(0); // branch qualifier length
28        writer.write_u8(1); // pointer (XID)
29        writer.write_ub4(u32::try_from(xid_bytes.len()).unwrap_or(0));
30        writer.write_ub4(flags);
31        writer.write_ub4(timeout);
32        writer.write_u8(1); // pointer (application value)
33        writer.write_u8(1); // pointer (return context)
34        writer.write_u8(1); // pointer (return context length)
35        writer.write_u8(0); // pointer (internal name)
36        writer.write_ub4(0); // length of internal name
37        writer.write_u8(0); // pointer (external name)
38        writer.write_ub4(0); // length of external name
39        writer.write_raw(&xid_bytes);
40        writer.write_ub4(0); // application value
41    } else {
42        writer.write_ub4(0); // format id
43        writer.write_ub4(0); // global transaction id length
44        writer.write_ub4(0); // branch qualifier length
45        writer.write_u8(0); // pointer (XID)
46        writer.write_ub4(0); // XID length
47        writer.write_ub4(flags);
48        writer.write_ub4(timeout);
49        writer.write_u8(1); // pointer (application value)
50        writer.write_u8(1); // pointer (return context)
51        writer.write_u8(1); // pointer (return context length)
52        writer.write_u8(0); // pointer (internal name)
53        writer.write_ub4(0); // length of internal name
54        writer.write_u8(0); // pointer (external name)
55        writer.write_ub4(0); // length of external name
56        writer.write_ub4(0); // application value
57    }
58}
59
60/// Direct (non-deferred) transaction-switch function call used to begin/resume
61/// (`TNS_TPC_TXN_START` + new/resume flag, with `xid`) or suspend
62/// (`TNS_TPC_TXN_DETACH`, no `xid`) a sessionless transaction. Reference
63/// impl/thin/connection.pyx `begin/resume/suspend_sessionless_transaction`.
64pub fn build_tpc_txn_switch_payload_with_seq(
65    seq_num: u8,
66    token_num: u64,
67    operation: u32,
68    flags: u32,
69    timeout: u32,
70    xid: Option<&[u8]>,
71) -> Vec<u8> {
72    let mut writer = TtcWriter::new();
73    writer.write_function_code_with_seq(TNS_FUNC_TPC_TXN_SWITCH, seq_num);
74    writer.write_ub8(token_num);
75    write_tpc_txn_switch_body(&mut writer, operation, flags, timeout, xid);
76    writer.into_bytes()
77}
78
79/// Sessionless transaction-switch piggyback, prepended to the next execute
80/// message's payload (reference messages/base.pyx `_write_sessionless_piggyback`
81/// — the same message body written with a `TNS_MSG_TYPE_PIGGYBACK` header). Used
82/// for a deferred begin/resume (`defer_round_trip=True`) and for the
83/// `suspend_on_success` post-detach. `operation` already encodes whether a
84/// post-detach is folded in (`TNS_TPC_TXN_START | TNS_TPC_TXN_POST_DETACH`).
85pub fn build_sessionless_piggyback(
86    seq_num: u8,
87    token_num: u64,
88    operation: u32,
89    flags: u32,
90    timeout: u32,
91    xid: Option<&[u8]>,
92) -> Vec<u8> {
93    let mut writer = TtcWriter::new();
94    writer.write_u8(TNS_MSG_TYPE_PIGGYBACK);
95    writer.write_u8(TNS_FUNC_TPC_TXN_SWITCH);
96    writer.write_u8(seq_num);
97    writer.write_ub8(token_num);
98    write_tpc_txn_switch_body(&mut writer, operation, flags, timeout, xid);
99    writer.into_bytes()
100}
101
102/// Decode the sessionless state bits packed in the transaction-id key/value
103/// binary payload (reference `_update_sessionless_txn_state`). The last two
104/// bytes are the state mask and the sync version; the leading bytes are the
105/// transaction id itself.
106pub fn decode_sessionless_txn_state(binary: &[u8]) -> Result<Option<SessionlessTxnState>> {
107    if binary.len() < 2 {
108        return Err(ProtocolError::TtcDecode("short sessionless txn state"));
109    }
110    let state = binary[binary.len() - 2];
111    let sync_version = binary[binary.len() - 1];
112    if sync_version != 1 {
113        return Err(ProtocolError::TtcDecode("unknown transaction sync version"));
114    }
115    if state & TNS_TPC_TXNID_SYNC_UNSET != 0 {
116        Ok(Some(SessionlessTxnState::Unset))
117    } else if state & TNS_TPC_TXNID_SYNC_SET != 0 {
118        Ok(Some(SessionlessTxnState::Set {
119            started_on_server: state & TNS_TPC_TXNID_SYNC_SERVER != 0,
120        }))
121    } else {
122        Ok(None)
123    }
124}
125
126/// Parse a transaction-switch response (reference tpc_switch.pyx
127/// `_process_return_parameters` plus base.pyx message loop). Returns any
128/// sessionless state update carried by a transaction-id key/value pair; server
129/// errors (e.g. ORA-25351 / ORA-26217) are surfaced as `ProtocolError`.
130pub fn parse_tpc_txn_switch_response(
131    payload: &[u8],
132    capabilities: ClientCapabilities,
133) -> Result<Option<SessionlessTxnState>> {
134    let mut reader = TtcReader::new(payload);
135    let mut state = None;
136    while reader.remaining() > 0 {
137        let message_type = reader.read_u8()?;
138        match message_type {
139            0 => {}
140            TNS_MSG_TYPE_STATUS => {
141                let _call_status = reader.read_ub4()?;
142                let _seq = reader.read_ub2()?;
143            }
144            TNS_MSG_TYPE_PARAMETER => {
145                // tpc_switch.pyx `_process_return_parameters`: application value
146                // (ub4) then the return transaction context (ub2 length + bytes).
147                let _application_value = reader.read_ub4()?;
148                let context_len = reader.read_ub2()?;
149                if context_len > 0 {
150                    reader.skip(usize::from(context_len))?;
151                }
152            }
153            TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => {
154                if let Some(update) = skip_server_side_piggyback(&mut reader)? {
155                    state = Some(update);
156                }
157            }
158            TNS_MSG_TYPE_END_OF_RESPONSE => break,
159            TNS_MSG_TYPE_ERROR => {
160                let info = parse_server_error_info(&mut reader, capabilities.ttc_field_version)?;
161                if info.number != 0 {
162                    return Err(ProtocolError::ServerErrorInfo(Box::new(
163                        info.into_details(),
164                    )));
165                }
166            }
167            _ => break,
168        }
169    }
170    Ok(state)
171}
172
173/// Begin-pipeline piggyback (messages/base.pyx `_write_begin_pipeline_piggyback`
174/// and `_write_piggyback_code`): prepended to the first pipelined message's
175/// payload. The packet carrying it must set [`TNS_DATA_FLAGS_BEGIN_PIPELINE`].
176///
177/// `token_num` is the token of the message the piggyback rides on (1 for the
178/// first pipeline operation); `pipeline_mode` is one of
179/// [`TNS_PIPELINE_MODE_CONTINUE_ON_ERROR`] / [`TNS_PIPELINE_MODE_ABORT_ON_ERROR`].
180pub fn build_begin_pipeline_piggyback(seq_num: u8, token_num: u64, pipeline_mode: u8) -> Vec<u8> {
181    let mut writer = TtcWriter::new();
182    writer.write_u8(TNS_MSG_TYPE_PIGGYBACK);
183    writer.write_u8(TNS_FUNC_PIPELINE_BEGIN);
184    writer.write_u8(seq_num);
185    writer.write_ub8(token_num);
186    writer.write_ub2(0); // error set ID
187    writer.write_u8(0); // error set mode
188    writer.write_u8(pipeline_mode);
189    writer.into_bytes()
190}
191
192/// End-pipeline message (messages/end_pipeline.pyx): function 200 plus an
193/// unused ub4 identifier. Sent after every pipelined operation message; its
194/// packet carries no END_OF_REQUEST flag and its response is the final
195/// (N+1th) boundary-delimited response of the pipeline.
196pub fn build_end_pipeline_payload_with_seq(seq_num: u8) -> Vec<u8> {
197    let mut writer = TtcWriter::new();
198    writer.write_function_code_with_seq(TNS_FUNC_PIPELINE_END, seq_num);
199    writer.write_ub8(0); // token (the end-pipeline message itself has none)
200    writer.write_ub4(0); // error set ID (unused)
201    writer.into_bytes()
202}
203
204/// A two-phase-commit transaction id (reference `Xid` namedtuple). The
205/// `global_transaction_id` and `branch_qualifier` are the raw (already
206/// UTF-8 encoded) byte values; the shim coerces `str` members before calling.
207#[derive(Clone, Debug)]
208pub struct TpcXid<'a> {
209    pub format_id: u32,
210    pub global_transaction_id: &'a [u8],
211    pub branch_qualifier: &'a [u8],
212}
213
214/// Writes the XID descriptor + the 128-byte zero-padded XID block, shared by
215/// the full-XA switch (func 103) and change-state (func 104) messages. The
216/// descriptor (`format_id`, gtid length, bqual length, pointer, block length)
217/// is written at the caller-specified position; the 128-byte block itself is
218/// written by [`write_xid_block_bytes`] later in the message body, after the
219/// context bytes (reference tpc_switch.pyx / tpc_change_state.pyx).
220fn write_xid_descriptor(writer: &mut TtcWriter, xid: Option<&TpcXid<'_>>) {
221    match xid {
222        Some(xid) => {
223            writer.write_ub4(xid.format_id);
224            writer.write_ub4(u32::try_from(xid.global_transaction_id.len()).unwrap_or(0));
225            writer.write_ub4(u32::try_from(xid.branch_qualifier.len()).unwrap_or(0));
226            writer.write_u8(1); // pointer (XID)
227            writer.write_ub4(128); // length of the XID block
228        }
229        None => {
230            writer.write_ub4(0); // format id
231            writer.write_ub4(0); // global transaction id length
232            writer.write_ub4(0); // branch qualifier length
233            writer.write_u8(0); // pointer (XID)
234            writer.write_ub4(0); // XID length
235        }
236    }
237}
238
239/// The 128-byte XID block: `global_transaction_id + branch_qualifier`,
240/// right-zero-padded to exactly 128 bytes (reference tpc_switch.pyx:80-81).
241fn write_xid_block_bytes(writer: &mut TtcWriter, xid: &TpcXid<'_>) {
242    let mut xid_bytes = Vec::with_capacity(128);
243    xid_bytes.extend_from_slice(xid.global_transaction_id);
244    xid_bytes.extend_from_slice(xid.branch_qualifier);
245    xid_bytes.resize(128, 0);
246    writer.write_raw(&xid_bytes);
247}
248
249/// Full-XA transaction-switch payload (func 103), used by `tpc_begin`
250/// (`operation = TNS_TPC_TXN_START`) and `tpc_end` (`operation =
251/// TNS_TPC_TXN_DETACH`). Unlike [`build_tpc_txn_switch_payload_with_seq`] (the
252/// sessionless special case) this carries a real `format_id`, a non-empty
253/// branch qualifier, and the captured transaction `context` to echo back.
254/// Reference messages/tpc_switch.pyx `_write_message`.
255pub fn build_tpc_switch_payload_with_seq(
256    seq_num: u8,
257    operation: u32,
258    flags: u32,
259    timeout: u32,
260    xid: Option<&TpcXid<'_>>,
261    context: Option<&[u8]>,
262) -> Vec<u8> {
263    let mut writer = TtcWriter::new();
264    writer.write_function_code_with_seq(TNS_FUNC_TPC_TXN_SWITCH, seq_num);
265    writer.write_ub8(0); // token
266    writer.write_ub4(operation);
267    match context {
268        Some(context) => {
269            writer.write_u8(1); // pointer (transaction context)
270            writer.write_ub4(u32::try_from(context.len()).unwrap_or(0));
271        }
272        None => {
273            writer.write_u8(0); // pointer (transaction context)
274            writer.write_ub4(0); // transaction context length
275        }
276    }
277    write_xid_descriptor(&mut writer, xid);
278    writer.write_ub4(flags);
279    writer.write_ub4(timeout);
280    writer.write_u8(1); // pointer (application value)
281    writer.write_u8(1); // pointer (return context)
282    writer.write_u8(1); // pointer (return context length)
283    writer.write_u8(0); // pointer (internal name)
284    writer.write_ub4(0); // length of internal name
285    writer.write_u8(0); // pointer (external name)
286    writer.write_ub4(0); // length of external name
287    if let Some(context) = context {
288        writer.write_raw(context);
289    }
290    if let Some(xid) = xid {
291        write_xid_block_bytes(&mut writer, xid);
292    }
293    writer.write_ub4(0); // application value
294    writer.into_bytes()
295}
296
297/// TPC transaction change-state payload (func 104), used by `tpc_prepare`
298/// (`operation = TNS_TPC_TXN_PREPARE`), `tpc_commit` (`TNS_TPC_TXN_COMMIT`) and
299/// `tpc_rollback` (`TNS_TPC_TXN_ABORT`). `requested_state` is the desired state
300/// (0 for prepare; READ_ONLY/COMMITTED for commit; ABORTED for rollback).
301/// Reference messages/tpc_change_state.pyx `_write_message`.
302pub fn build_tpc_change_state_payload_with_seq(
303    seq_num: u8,
304    operation: u32,
305    requested_state: u32,
306    flags: u32,
307    xid: Option<&TpcXid<'_>>,
308    context: Option<&[u8]>,
309) -> Vec<u8> {
310    let mut writer = TtcWriter::new();
311    writer.write_function_code_with_seq(TNS_FUNC_TPC_TXN_CHANGE_STATE, seq_num);
312    writer.write_ub8(0); // token
313    writer.write_ub4(operation);
314    match context {
315        Some(context) => {
316            writer.write_u8(1); // pointer (context)
317            writer.write_ub4(u32::try_from(context.len()).unwrap_or(0));
318        }
319        None => {
320            writer.write_u8(0); // pointer (context)
321            writer.write_ub4(0); // context length
322        }
323    }
324    write_xid_descriptor(&mut writer, xid);
325    writer.write_ub4(0); // timeout (always 0)
326    writer.write_ub4(requested_state);
327    writer.write_u8(1); // pointer (out state)
328    writer.write_ub4(flags);
329    if let Some(context) = context {
330        writer.write_raw(context);
331    }
332    if let Some(xid) = xid {
333        write_xid_block_bytes(&mut writer, xid);
334    }
335    writer.into_bytes()
336}
337
338/// Parse a full-XA transaction-switch response (reference tpc_switch.pyx
339/// `_process_return_parameters` plus the base.pyx message loop). Captures the
340/// returned transaction context (PARAMETER message) and the txn-in-progress bit
341/// (last call status). Server errors are surfaced as `ProtocolError`.
342pub fn parse_tpc_switch_response(
343    payload: &[u8],
344    capabilities: ClientCapabilities,
345) -> Result<TpcSwitchResponse> {
346    let mut reader = TtcReader::new(payload);
347    let mut response = TpcSwitchResponse::default();
348    while reader.remaining() > 0 {
349        let message_type = reader.read_u8()?;
350        match message_type {
351            0 => {}
352            TNS_MSG_TYPE_STATUS => {
353                let call_status = reader.read_ub4()?;
354                let _seq = reader.read_ub2()?;
355                response.txn_in_progress = call_status & TNS_EOCS_FLAGS_TXN_IN_PROGRESS != 0;
356            }
357            TNS_MSG_TYPE_PARAMETER => {
358                // tpc_switch.pyx `_process_return_parameters`: application value
359                // (ub4) then the return transaction context (ub2 length + bytes).
360                let _application_value = reader.read_ub4()?;
361                let context_len = reader.read_ub2()?;
362                let context = reader.read_raw(usize::from(context_len))?;
363                response.context = context.to_vec();
364            }
365            TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => {
366                if let Some(update) = skip_server_side_piggyback(&mut reader)? {
367                    response.sessionless_state = Some(update);
368                }
369            }
370            TNS_MSG_TYPE_END_OF_RESPONSE => break,
371            TNS_MSG_TYPE_ERROR => {
372                let info = parse_server_error_info(&mut reader, capabilities.ttc_field_version)?;
373                if info.number != 0 {
374                    // On a server error the reference raises before
375                    // `_process_call_status` runs, so `_txn_in_progress` keeps
376                    // its prior value; we surface the error without touching the
377                    // flag.
378                    return Err(ProtocolError::ServerErrorInfo(Box::new(
379                        info.into_details(),
380                    )));
381                }
382                // The end-of-call ERROR (number 0 on success) carries the
383                // end-of-call status; sample the transaction-in-progress bit.
384                response.txn_in_progress = info.call_status & TNS_EOCS_FLAGS_TXN_IN_PROGRESS != 0;
385            }
386            _ => break,
387        }
388    }
389    Ok(response)
390}
391
392/// Parse a TPC change-state response (reference tpc_change_state.pyx
393/// `_process_return_parameters` plus the base.pyx message loop). Reads the out
394/// state from the PARAMETER message and the txn-in-progress bit from the last
395/// call status. Server errors are surfaced as `ProtocolError`.
396pub fn parse_tpc_change_state_response(
397    payload: &[u8],
398    capabilities: ClientCapabilities,
399) -> Result<TpcChangeStateResponse> {
400    let mut reader = TtcReader::new(payload);
401    let mut response = TpcChangeStateResponse::default();
402    while reader.remaining() > 0 {
403        let message_type = reader.read_u8()?;
404        match message_type {
405            0 => {}
406            TNS_MSG_TYPE_STATUS => {
407                let call_status = reader.read_ub4()?;
408                let _seq = reader.read_ub2()?;
409                response.txn_in_progress = call_status & TNS_EOCS_FLAGS_TXN_IN_PROGRESS != 0;
410            }
411            TNS_MSG_TYPE_PARAMETER => {
412                // tpc_change_state.pyx `_process_return_parameters` reads the
413                // out state (ub4).
414                response.state = reader.read_ub4()?;
415            }
416            TNS_MSG_TYPE_SERVER_SIDE_PIGGYBACK => {
417                skip_server_side_piggyback(&mut reader)?;
418            }
419            TNS_MSG_TYPE_END_OF_RESPONSE => break,
420            TNS_MSG_TYPE_ERROR => {
421                let info = parse_server_error_info(&mut reader, capabilities.ttc_field_version)?;
422                if info.number != 0 {
423                    // On a server error the reference raises before
424                    // `_process_call_status` runs, so `_txn_in_progress` keeps
425                    // its prior value; we surface the error without touching the
426                    // flag.
427                    return Err(ProtocolError::ServerErrorInfo(Box::new(
428                        info.into_details(),
429                    )));
430                }
431                // The end-of-call ERROR (number 0 on success) carries the
432                // end-of-call status; sample the transaction-in-progress bit.
433                response.txn_in_progress = info.call_status & TNS_EOCS_FLAGS_TXN_IN_PROGRESS != 0;
434            }
435            _ => break,
436        }
437    }
438    Ok(response)
439}
440
441pub(crate) fn skip_keyword_value_pairs(reader: &mut TtcReader<'_>, num_pairs: u16) -> Result<()> {
442    read_keyword_value_pairs_for_txn_state(reader, num_pairs).map(|_| ())
443}
444
445/// Like [`skip_keyword_value_pairs`] but extracts the sessionless transaction
446/// state carried by the `TRANSACTION_ID` keyword (201). Reference
447/// `_process_keyword_value_pairs` calls `_update_sessionless_txn_state` on the
448/// binary value of that keyword.
449pub(crate) fn read_keyword_value_pairs_for_txn_state(
450    reader: &mut TtcReader<'_>,
451    num_pairs: u16,
452) -> Result<Option<SessionlessTxnState>> {
453    let mut state = None;
454    for _ in 0..num_pairs {
455        if reader.read_ub2()? > 0 {
456            let _text_value = reader.read_bytes()?;
457        }
458        let mut binary_value = None;
459        if reader.read_ub2()? > 0 {
460            binary_value = reader.read_bytes()?;
461        }
462        let keyword_num = reader.read_ub2()?;
463        if keyword_num == TNS_KEYWORD_NUM_TRANSACTION_ID {
464            if let Some(binary) = binary_value.as_deref() {
465                if let Some(update) = decode_sessionless_txn_state(binary)? {
466                    state = Some(update);
467                }
468            }
469        }
470    }
471    Ok(state)
472}
473
474#[cfg(test)]
475mod tpc_tests {
476    use super::*;
477
478    fn xid() -> ([u8; 7], [u8; 8]) {
479        (*b"txn4400", *b"branchId")
480    }
481
482    #[test]
483    fn tpc_begin_payload_encodes_format_branch_and_128_byte_xid() {
484        let (gtid, bqual) = xid();
485        let tpc_xid = TpcXid {
486            format_id: 4400,
487            global_transaction_id: &gtid,
488            branch_qualifier: &bqual,
489        };
490        let payload = build_tpc_switch_payload_with_seq(
491            4,
492            TNS_TPC_TXN_START,
493            TPC_TXN_FLAGS_NEW,
494            0,
495            Some(&tpc_xid),
496            None,
497        );
498        // [msg_type=3][func=0x67=103][seq=4] + token ub8(0) = 1 byte
499        assert_eq!(&payload[..3], &[3, TNS_FUNC_TPC_TXN_SWITCH, 4]);
500        let body = &payload[4..];
501        // operation ub4(START=1) = [1,1]; context ptr u8(0) = [0]; len ub4(0) = [0]
502        assert_eq!(&body[..4], &[1, 1, 0, 0]);
503        // format id ub4(4400=0x1130) = len2 + value (golden: 02 11 30)
504        assert_eq!(&body[4..7], &[2, 0x11, 0x30]);
505        // gtid len ub4(7)=[1,7], bqual len ub4(8)=[1,8], xid ptr u8(1)=[1],
506        // block len ub4(128)=[1,0x80]
507        assert_eq!(&body[7..14], &[1, 7, 1, 8, 1, 1, 0x80]);
508        // the 128-byte xid block must contain gtid+bqual zero-padded; it is the
509        // last 128 bytes before the trailing application value ub4(0) = [0].
510        let block_start = payload.len() - 128 - 1;
511        let block = &payload[block_start..block_start + 128];
512        assert_eq!(&block[..7], b"txn4400");
513        assert_eq!(&block[7..15], b"branchId");
514        assert!(block[15..].iter().all(|&byte| byte == 0));
515    }
516
517    #[test]
518    fn tpc_end_payload_echoes_context() {
519        let context = vec![0xAAu8; 168];
520        let payload =
521            build_tpc_switch_payload_with_seq(7, TNS_TPC_TXN_DETACH, 0, 0, None, Some(&context));
522        let body = &payload[4..];
523        // operation ub4(DETACH=2)=[1,2]; context ptr u8(1)=[1]; len ub4(168)=[1,0xA8]
524        assert_eq!(&body[..5], &[1, 2, 1, 1, 0xA8]);
525        // context bytes are echoed verbatim somewhere in the payload tail
526        assert!(payload
527            .windows(context.len())
528            .any(|window| window == context.as_slice()));
529    }
530
531    #[test]
532    fn change_state_prepare_payload_shape() {
533        let (gtid, bqual) = xid();
534        let tpc_xid = TpcXid {
535            format_id: 4400,
536            global_transaction_id: &gtid,
537            branch_qualifier: &bqual,
538        };
539        let payload = build_tpc_change_state_payload_with_seq(
540            8,
541            TNS_TPC_TXN_PREPARE,
542            TNS_TPC_TXN_STATE_PREPARE,
543            0,
544            Some(&tpc_xid),
545            None,
546        );
547        assert_eq!(&payload[..3], &[3, TNS_FUNC_TPC_TXN_CHANGE_STATE, 8]);
548        let body = &payload[4..];
549        // operation ub4(PREPARE=3)=[1,3]; context ptr u8(0)=[0]; len ub4(0)=[0]
550        assert_eq!(&body[..4], &[1, 3, 0, 0]);
551    }
552
553    #[test]
554    fn switch_response_captures_context_and_txn_bit() {
555        // PARAMETER(8): app_value ub4(0) + context_len ub2(4) + 4 context bytes;
556        // STATUS(9): call_status ub4 = 3 (TXN bit set) + seq ub2(0); EOR(29).
557        let mut payload = Vec::new();
558        payload.push(TNS_MSG_TYPE_PARAMETER);
559        payload.push(0); // app value ub4(0)
560        payload.extend_from_slice(&[2, 0, 4]); // context_len ub2 = 4
561        payload.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
562        payload.push(TNS_MSG_TYPE_STATUS);
563        payload.extend_from_slice(&[1, 3]); // call_status ub4 = 3
564        payload.extend_from_slice(&[0]); // seq ub2 = 0
565        payload.push(TNS_MSG_TYPE_END_OF_RESPONSE);
566
567        let response =
568            parse_tpc_switch_response(&payload, ClientCapabilities::default()).expect("decode");
569        assert_eq!(response.context, vec![0xDE, 0xAD, 0xBE, 0xEF]);
570        assert!(response.txn_in_progress);
571    }
572
573    #[test]
574    fn switch_response_end_status_clears_txn_bit() {
575        // STATUS call_status = 1 (TXN bit clear) -> txn_in_progress == false.
576        let mut payload = Vec::new();
577        payload.push(TNS_MSG_TYPE_STATUS);
578        payload.extend_from_slice(&[1, 1]); // call_status ub4 = 1
579        payload.extend_from_slice(&[0]); // seq ub2 = 0
580        payload.push(TNS_MSG_TYPE_END_OF_RESPONSE);
581
582        let response =
583            parse_tpc_switch_response(&payload, ClientCapabilities::default()).expect("decode");
584        assert!(!response.txn_in_progress);
585    }
586
587    #[test]
588    fn change_state_response_reads_out_state() {
589        // PARAMETER out state ub4 = 1 (REQUIRES_COMMIT); STATUS txn bit clear.
590        let mut payload = Vec::new();
591        payload.push(TNS_MSG_TYPE_PARAMETER);
592        payload.extend_from_slice(&[1, 1]); // state ub4 = 1
593        payload.push(TNS_MSG_TYPE_STATUS);
594        payload.extend_from_slice(&[1, 1]); // call_status ub4 = 1
595        payload.extend_from_slice(&[0]); // seq ub2 = 0
596        payload.push(TNS_MSG_TYPE_END_OF_RESPONSE);
597
598        let response = parse_tpc_change_state_response(&payload, ClientCapabilities::default())
599            .expect("decode");
600        assert_eq!(response.state, TNS_TPC_TXN_STATE_REQUIRES_COMMIT);
601        assert!(!response.txn_in_progress);
602    }
603}