Skip to content

Commit cacd12f

Browse files
committed
fix(dameng): read result metadata from the frame header, not the first column
1 parent 2024274 commit cacd12f

7 files changed

Lines changed: 664 additions & 678 deletions

File tree

dameng-protocol/src/frame.rs

Lines changed: 91 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,20 @@
1111
//! 14 4 AffectedRows (i32 LE) - rows affected (for DML responses)
1212
//! 18 1 CompressFlag (u8)
1313
//! 19 1 Checksum (u8) - XOR of bytes 0-18
14-
//! 20 44 Reserved (zeros)
14+
//! 20 44 Reserved for the message type (zeros on client messages)
1515
//! 64 var Payload body
1616
//! ```
17+
//!
18+
//! The server reuses the reserved area, and each response type spells it differently.
19+
//! The fields below overlap on purpose, so read the one that belongs to the message:
20+
//! ```text
21+
//! Offset Size Response Field
22+
//! 20 8 FETCH fetch_total (i64 LE)
23+
//! 22 2 EXEC/OPE ACK column_count (u16 LE)
24+
//! 24 8 EXEC/OPE ACK update_count (i64 LE)
25+
//! 28 4 FETCH batch_row_count (i32 LE)
26+
//! 28 4 STARTUP server_encoding (i32 LE)
27+
//! ```
1728
1829
use bytes::{Buf, BufMut, BytesMut};
1930

@@ -22,6 +33,10 @@ use crate::error::{Error, Result};
2233
/// The size of the frame header in bytes.
2334
pub const FRAME_HEADER_SIZE: usize = 64;
2435

36+
/// What the server writes in a row total it cannot state yet, because rows are still
37+
/// queued behind the cursor. A total that is not this value is the final one.
38+
pub const ROW_TOTAL_UNKNOWN: i64 = i64::MAX;
39+
2540
/// DM protocol frame header (64 bytes).
2641
#[derive(Debug, Clone, PartialEq)]
2742
pub struct Frame {
@@ -38,9 +53,17 @@ pub struct Frame {
3853
pub affected_rows: i32,
3954
/// Compression flag (0=none, 1=snappy, 2=zlib).
4055
pub compress_flag: u8,
41-
/// Update count for DML operations, stored in the reserved area
42-
/// at header offset 24 (int64 LE). Always 0 for non-DML.
56+
/// Update count at header offset 24 (int64 LE): the affected count of a DML
57+
/// statement, and the row total of a result set. It is
58+
/// [`ROW_TOTAL_UNKNOWN`] while the server still holds rows for the cursor.
4359
pub update_count: u64,
60+
/// Number of columns in a statement response, header offset 22 (uint16 LE).
61+
pub column_count: u16,
62+
/// Row total a FETCH reply reports, header offset 20 (int64 LE).
63+
/// [`ROW_TOTAL_UNKNOWN`] until the batch that drains the cursor.
64+
pub fetch_total: i64,
65+
/// Number of rows a FETCH reply carries in its payload, header offset 28 (int32 LE).
66+
pub batch_row_count: i32,
4467
/// Server encoding from header offset 28 (int32 LE).
4568
/// Used in STARTUP_RESPONSE. 0=GB18030, 1=UTF-8, 2=EUC-KR.
4669
pub server_encoding: u8,
@@ -57,6 +80,9 @@ impl Frame {
5780
affected_rows: 0,
5881
compress_flag: 0,
5982
update_count: 0,
83+
column_count: 0,
84+
fetch_total: 0,
85+
batch_row_count: 0,
6086
server_encoding: 0,
6187
}
6288
}
@@ -91,25 +117,40 @@ impl Frame {
91117
return Err(Error::ChecksumMismatch);
92118
}
93119

94-
// Parse update_count from the reserved area at header offset 24 (int64 LE).
95-
// Need to read from the raw buffer before advancing. buf currently has
96-
// cursor at byte 20 (after consuming 20 bytes).
97-
// Bytes [4..12] of the remaining 44-byte reserved area = absolute offset 24.
98-
let update_count = if buf.remaining() >= 12 {
99-
let raw = &buf.chunk()[4..12]; // offset 24-31 in absolute header
100-
u64::from_le_bytes([
101-
raw[0], raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7],
102-
])
120+
// The cursor sits at byte 20 now, so the reserved area starts at chunk()[0]
121+
// and every field below is read before the buffer advances past it.
122+
let reserved = if buf.remaining() >= 12 {
123+
let raw = &buf.chunk()[..12];
124+
let mut bytes = [0u8; 12];
125+
bytes.copy_from_slice(raw);
126+
bytes
103127
} else {
104-
0
105-
};
106-
// Server encoding at header offset 28 (int32 LE)
107-
let server_encoding = if buf.remaining() >= 12 {
108-
let raw = &buf.chunk()[8..12]; // offset 28-31 in absolute header
109-
u8::from_le_bytes([raw[0]])
110-
} else {
111-
0
128+
[0u8; 12]
112129
};
130+
let fetch_total = i64::from_le_bytes([
131+
reserved[0],
132+
reserved[1],
133+
reserved[2],
134+
reserved[3],
135+
reserved[4],
136+
reserved[5],
137+
reserved[6],
138+
reserved[7],
139+
]);
140+
let column_count = u16::from_le_bytes([reserved[2], reserved[3]]);
141+
let update_count = u64::from_le_bytes([
142+
reserved[4],
143+
reserved[5],
144+
reserved[6],
145+
reserved[7],
146+
reserved[8],
147+
reserved[9],
148+
reserved[10],
149+
reserved[11],
150+
]);
151+
let batch_row_count =
152+
i32::from_le_bytes([reserved[8], reserved[9], reserved[10], reserved[11]]);
153+
let server_encoding = reserved[8];
113154

114155
// Skip remaining 44 bytes of reserved
115156
buf.advance(44);
@@ -122,6 +163,9 @@ impl Frame {
122163
affected_rows,
123164
compress_flag,
124165
update_count,
166+
column_count,
167+
fetch_total,
168+
batch_row_count,
125169
server_encoding,
126170
})
127171
}
@@ -224,4 +268,31 @@ mod tests {
224268
assert_eq!(frame.handle, 3);
225269
assert_eq!(frame.body_len, 100);
226270
}
271+
272+
/// The reserved area carries several overlapping counts. Only the checksum over
273+
/// bytes 0-18 is fixed, so a reply can be forged by patching the tail.
274+
#[test]
275+
fn test_frame_parse_reserved_counts() {
276+
let mut encoded = Frame::new(187, 0, 64).encode();
277+
encoded[20..28].copy_from_slice(&20_000i64.to_le_bytes());
278+
encoded[28..32].copy_from_slice(&662i32.to_le_bytes());
279+
let frame = Frame::parse(&mut encoded).unwrap();
280+
assert_eq!(frame.fetch_total, 20_000);
281+
assert_eq!(frame.batch_row_count, 662);
282+
283+
let mut encoded = Frame::new(187, 0, 64).encode();
284+
encoded[22..24].copy_from_slice(&5u16.to_le_bytes());
285+
encoded[24..32].copy_from_slice(&2i64.to_le_bytes());
286+
let frame = Frame::parse(&mut encoded).unwrap();
287+
assert_eq!(frame.column_count, 5);
288+
assert_eq!(frame.update_count, 2);
289+
}
290+
291+
#[test]
292+
fn test_frame_parse_unknown_row_total() {
293+
let mut encoded = Frame::new(187, 0, 0).encode();
294+
encoded[24..32].copy_from_slice(&ROW_TOTAL_UNKNOWN.to_le_bytes());
295+
let frame = Frame::parse(&mut encoded).unwrap();
296+
assert_eq!(frame.update_count, ROW_TOTAL_UNKNOWN as u64);
297+
}
227298
}

dameng-protocol/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ pub mod frame;
88
pub mod message;
99

1010
pub use error::{Error, Result};
11-
pub use frame::Frame;
11+
pub use frame::{Frame, ROW_TOTAL_UNKNOWN};
1212
pub use message::explain::ExplainResponse;
1313
pub use message::response::{Column, ExecResponse, Row};

dameng-protocol/src/message/fetch.rs

Lines changed: 84 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,27 @@
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
2632
use bytes::{BufMut, BytesMut};
2733

28-
use crate::error::Result;
34+
use crate::frame::Frame;
2935
use 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.
3440
pub 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)]
109115
pub 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

118123
impl 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

Comments
 (0)