1414//! 38 4 prefetchBytes (i32 LE) — max bytes to fetch, clamped [32, 65536]
1515//! ```
1616//!
17- //! Response wire format:
17+ //! DM 8.1.3.62 reads none of the three: it streams the next batch from wherever its
18+ //! cursor stands, sized to its own budget, whatever the request asked for.
19+ //!
20+ //! The reply's payload is bare inline row data from byte 0, in the same format the
21+ //! EXEC/OPE payload uses after its column descriptors. It carries no header and no
22+ //! column metadata, so the columns of the statement that opened the cursor are what
23+ //! the rows are parsed against. The counts travel in the frame header instead:
1824//! ```text
1925//! Offset Size Field
20- //! 0 20 Reserved
21- //! 20 8 updateCount (i64 LE) — total row count in result set
22- //! 28 4 rsSizeof (i32 LE) — byte size of row data
23- //! 32 N row data (same format as EXEC_RESPONSE inline rows)
26+ //! 20 8 fetch_total (i64 LE) — rows in the whole result set,
27+ //! ROW_TOTAL_UNKNOWN until the last batch
28+ //! 28 4 batch_row_count (i32 LE) — rows in this reply
2429//! ```
30+ //! A reply with an empty body means the cursor is drained.
2531
2632use bytes:: { BufMut , BytesMut } ;
2733
28- use crate :: error :: Result ;
34+ use crate :: frame :: Frame ;
2935use dameng_types:: encoding:: ServerEncoding ;
3036
31- use super :: response:: { Column , ExecResponse , Row } ;
37+ use super :: response:: { parse_inline_rows , Column , Row } ;
3238
3339/// Default prefetch byte budget for FETCH requests.
3440pub const DEFAULT_PREFETCH_BYTES : i32 = 8192 ;
@@ -107,85 +113,27 @@ impl FetchMessage {
107113/// Response from a FETCH request (msg_type=7).
108114#[ derive( Debug , Clone ) ]
109115pub struct FetchResponse {
110- /// Total number of rows in the entire result set.
116+ /// Rows in the whole result set, or [`crate::frame::ROW_TOTAL_UNKNOWN`] while the
117+ /// server still holds rows for the cursor.
111118 pub total_row_count : i64 ,
112- /// Column metadata (may be empty if already known from initial query).
113- pub columns : Vec < Column > ,
114- /// Row data fetched in this batch.
119+ /// Rows this batch carried.
115120 pub rows : Vec < Row > ,
116121}
117122
118123impl FetchResponse {
119- /// Parse a FETCH response from raw payload bytes .
124+ /// Parse a FETCH reply: the payload is bare inline rows, the counts are in `frame` .
120125 ///
121- /// Response format:
122- /// - Offset 0-19: reserved
123- /// - Offset 20-27: updateCount (i64 LE) — total row count
124- /// - Offset 28-31: rsSizeof (i32 LE) — byte size of row data
125- /// - Offset 32+: row data (same format as EXEC_RESPONSE inline rows)
126- pub fn from_bytes ( data : & [ u8 ] , server_encoding : ServerEncoding ) -> Result < Self > {
127- if data. len ( ) < 32 {
128- return Err ( crate :: error:: Error :: Incomplete ) ;
129- }
130-
131- // updateCount at offset 20
132- let total_row_count = i64:: from_le_bytes ( [
133- data[ 20 ] , data[ 21 ] , data[ 22 ] , data[ 23 ] , data[ 24 ] , data[ 25 ] , data[ 26 ] , data[ 27 ] ,
134- ] ) ;
135-
136- // rsSizeof at offset 28
137- let rs_sizeof = if data. len ( ) >= 32 {
138- i32:: from_le_bytes ( [ data[ 28 ] , data[ 29 ] , data[ 30 ] , data[ 31 ] ] ) as usize
139- } else {
140- 0
141- } ;
142-
143- // Row data starts at offset 32
144- let row_data_start = 32 ;
145- let row_data_end = ( row_data_start + rs_sizeof) . min ( data. len ( ) ) ;
146-
147- if row_data_start >= data. len ( ) || rs_sizeof == 0 {
148- return Ok ( FetchResponse {
149- total_row_count,
150- columns : vec ! [ ] ,
151- rows : vec ! [ ] ,
152- } ) ;
153- }
154-
155- let row_data = & data[ row_data_start..row_data_end] ;
156-
157- // The row data follows the same inline format as EXEC_RESPONSE.
158- // Parse it using the ExecResponse parser.
159- // Guard against parsing garbage: if row_data is all zeros or too short,
160- // the server returned metadata only (no inline data).
161- let has_real_data =
162- row_data. len ( ) > 16 && !row_data. iter ( ) . all ( |& b| b == 0 ) && row_data[ 0 ] != 0 ;
163-
164- if !has_real_data {
165- // Server returned a cursor/total count but no inline row data.
166- // This happens when the cursor_id is invalid or the result is empty.
167- return Ok ( FetchResponse {
168- total_row_count,
169- columns : vec ! [ ] ,
170- rows : vec ! [ ] ,
171- } ) ;
172- }
173-
174- match ExecResponse :: from_bytes ( row_data, server_encoding) {
175- Ok ( resp) => Ok ( FetchResponse {
176- total_row_count,
177- columns : resp. columns ,
178- rows : resp. rows ,
179- } ) ,
180- Err ( _) => {
181- // If we can't parse the row data as EXEC_RESPONSE format,
182- // return what we have with empty rows.
183- Ok ( FetchResponse {
184- total_row_count,
185- columns : vec ! [ ] ,
186- rows : vec ! [ ] ,
187- } )
188- }
126+ /// `columns` describes the result set the cursor belongs to. The reply repeats no
127+ /// metadata, so nothing else can say how wide a row is.
128+ pub fn from_frame (
129+ frame : & Frame ,
130+ data : & [ u8 ] ,
131+ columns : & [ Column ] ,
132+ server_encoding : ServerEncoding ,
133+ ) -> Self {
134+ Self {
135+ total_row_count : frame. fetch_total ,
136+ rows : parse_inline_rows ( data, 0 , columns, server_encoding) ,
189137 }
190138 }
191139
@@ -285,19 +233,66 @@ mod tests {
285233 assert_eq ! ( fetch. prefetch_bytes, DEFAULT_PREFETCH_BYTES ) ;
286234 }
287235
288- #[ test]
289- fn test_fetch_response_incomplete ( ) {
290- let data = [ 0u8 ; 10 ] ;
291- let result = FetchResponse :: from_bytes ( & data, ServerEncoding :: Utf8 ) ;
292- assert ! ( result. is_err( ) ) ;
236+ fn varchar_column ( ) -> Column {
237+ Column {
238+ name : "NAME" . to_string ( ) ,
239+ type_code : 3 ,
240+ type_name : "VARCHAR" . to_string ( ) ,
241+ precision : 100 ,
242+ scale : 0 ,
243+ nullable : true ,
244+ display_size : 0 ,
245+ table_name : "CUSTOMER" . to_string ( ) ,
246+ schema_name : "APP" . to_string ( ) ,
247+ lob_tab_id : 0 ,
248+ lob_col_id : 0 ,
249+ }
293250 }
294251
252+ /// A drained cursor answers with an empty body. That is the end of the result set,
253+ /// not a truncated message.
295254 #[ test]
296- fn test_fetch_response_empty_data ( ) {
297- let data = vec ! [ 0u8 ; 42 ] ;
298- let resp = FetchResponse :: from_bytes ( & data, ServerEncoding :: Utf8 ) . unwrap ( ) ;
299- assert_eq ! ( resp. total_row_count, 0 ) ;
255+ fn test_fetch_response_empty_body_ends_the_cursor ( ) {
256+ let mut frame = Frame :: new ( 7 , 0 , 0 ) ;
257+ frame. fetch_total = 2 ;
258+ let resp =
259+ FetchResponse :: from_frame ( & frame, & [ ] , & [ varchar_column ( ) ] , ServerEncoding :: Utf8 ) ;
260+ assert_eq ! ( resp. total_row_count, 2 ) ;
300261 assert ! ( resp. rows. is_empty( ) ) ;
262+ assert ! ( !resp. has_more( 2 ) ) ;
263+ }
264+
265+ /// Captured from DM 8.1.3.62: the payload is rows from byte 0, no header in front.
266+ #[ test]
267+ fn test_fetch_response_parses_bare_rows ( ) {
268+ let data: Vec < u8 > = vec ! [
269+ 0x13 , 0x00 , 0x01 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x0c , 0x00 , 0x05 , 0x00 ,
270+ 0x41 , 0x6c , 0x69 , 0x63 , 0x65 , // "Alice"
271+ 0x11 , 0x00 , 0x02 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x0c , 0x00 , 0x03 , 0x00 ,
272+ 0x42 , 0x6f , 0x62 , // "Bob"
273+ ] ;
274+ let mut frame = Frame :: new ( 7 , 0 , data. len ( ) as i32 ) ;
275+ frame. fetch_total = 2 ;
276+ frame. batch_row_count = 2 ;
277+
278+ let resp =
279+ FetchResponse :: from_frame ( & frame, & data, & [ varchar_column ( ) ] , ServerEncoding :: Utf8 ) ;
280+
281+ assert_eq ! ( resp. rows. len( ) , frame. batch_row_count as usize ) ;
282+ assert_eq ! ( resp. total_row_count, 2 ) ;
283+ assert_eq ! ( resp. rows[ 0 ] . get_str( 0 ) . unwrap( ) , "Alice" ) ;
284+ assert_eq ! ( resp. rows[ 1 ] . get_str( 0 ) . unwrap( ) , "Bob" ) ;
285+ }
286+
287+ /// Every batch before the last one leaves the total unknown.
288+ #[ test]
289+ fn test_fetch_response_unknown_total ( ) {
290+ let mut frame = Frame :: new ( 7 , 0 , 0 ) ;
291+ frame. fetch_total = crate :: frame:: ROW_TOTAL_UNKNOWN ;
292+ let resp =
293+ FetchResponse :: from_frame ( & frame, & [ ] , & [ varchar_column ( ) ] , ServerEncoding :: Utf8 ) ;
294+ assert_eq ! ( resp. total_row_count, crate :: frame:: ROW_TOTAL_UNKNOWN ) ;
295+ assert ! ( resp. has_more( 662 ) ) ;
301296 }
302297
303298 #[ test]
@@ -313,7 +308,6 @@ mod tests {
313308 fn test_has_more ( ) {
314309 let resp = FetchResponse {
315310 total_row_count : 1000 ,
316- columns : vec ! [ ] ,
317311 rows : vec ! [ ] ,
318312 } ;
319313 assert ! ( resp. has_more( 0 ) ) ;
0 commit comments