Skip to main content

fraiseql_wire/connection/conn/
core.rs

1//! Core `Connection` type and implementation
2
3use super::config::ConnectionConfig;
4use super::helpers::extract_entity_from_query;
5use crate::auth::ScramClient;
6use crate::connection::state::ConnectionState;
7use crate::connection::transport::Transport;
8use crate::protocol::{
9    decode_message, encode_message, AuthenticationMessage, BackendMessage, FrontendMessage,
10};
11use crate::{Result, WireError};
12use bytes::{Buf, BytesMut};
13use std::sync::atomic::{AtomicU64, Ordering};
14use tracing::Instrument;
15
16// Global counter for chunk metrics sampling (1 per 10 chunks)
17// Used to reduce per-chunk metric recording overhead
18static CHUNK_COUNT: AtomicU64 = AtomicU64::new(0);
19
20/// Postgres connection
21pub struct Connection {
22    transport: Transport,
23    state: ConnectionState,
24    read_buf: BytesMut,
25    process_id: Option<i32>,
26    secret_key: Option<i32>,
27}
28
29impl Connection {
30    /// Create connection from transport
31    pub fn new(transport: Transport) -> Self {
32        Self {
33            transport,
34            state: ConnectionState::Initial,
35            read_buf: BytesMut::with_capacity(8192),
36            process_id: None,
37            secret_key: None,
38        }
39    }
40
41    /// Get current connection state
42    pub const fn state(&self) -> ConnectionState {
43        self.state
44    }
45
46    /// Perform startup and authentication
47    ///
48    /// # Errors
49    ///
50    /// Returns [`WireError::InvalidState`] if the connection is not in the `Initial` state.
51    /// Returns [`WireError::Authentication`] if authentication is rejected by the server.
52    /// Returns [`WireError`] on any I/O or protocol error during the handshake.
53    pub async fn startup(&mut self, config: &ConnectionConfig) -> Result<()> {
54        async {
55            self.state.transition(ConnectionState::AwaitingAuth)?;
56
57            // Build startup parameters
58            let mut params = vec![
59                ("user".to_string(), config.user.clone()),
60                ("database".to_string(), config.database.clone()),
61            ];
62
63            // Add configured application name if specified
64            if let Some(app_name) = &config.application_name {
65                params.push(("application_name".to_string(), app_name.clone()));
66            }
67
68            // Add statement timeout if specified (in milliseconds)
69            if let Some(timeout) = config.statement_timeout {
70                params.push((
71                    "statement_timeout".to_string(),
72                    timeout.as_millis().to_string(),
73                ));
74            }
75
76            // Add extra_float_digits if specified
77            if let Some(digits) = config.extra_float_digits {
78                params.push(("extra_float_digits".to_string(), digits.to_string()));
79            }
80
81            // Add user-provided parameters
82            for (k, v) in &config.params {
83                params.push((k.clone(), v.clone()));
84            }
85
86            // Send startup message
87            let startup = FrontendMessage::Startup {
88                version: crate::protocol::constants::PROTOCOL_VERSION,
89                params,
90            };
91            self.send_message(&startup).await?;
92
93            // Authentication loop
94            self.state.transition(ConnectionState::Authenticating)?;
95            self.authenticate(config).await?;
96
97            self.state.transition(ConnectionState::Idle)?;
98            tracing::info!("startup complete");
99            Ok(())
100        }
101        .instrument(tracing::info_span!(
102            "startup",
103            user = %config.user,
104            database = %config.database
105        ))
106        .await
107    }
108
109    /// Handle authentication
110    async fn authenticate(&mut self, config: &ConnectionConfig) -> Result<()> {
111        let auth_start = std::time::Instant::now();
112        let mut auth_mechanism = "unknown";
113
114        loop {
115            let msg = self.receive_message().await?;
116
117            match msg {
118                BackendMessage::Authentication(auth) => match auth {
119                    AuthenticationMessage::Ok => {
120                        tracing::debug!("authentication successful");
121                        crate::metrics::counters::auth_successful(auth_mechanism);
122                        crate::metrics::histograms::auth_duration(
123                            auth_mechanism,
124                            auth_start.elapsed().as_millis() as u64,
125                        );
126                        // Don't break here! Must continue reading until ReadyForQuery
127                    }
128                    AuthenticationMessage::CleartextPassword => {
129                        auth_mechanism = crate::metrics::labels::MECHANISM_CLEARTEXT;
130                        crate::metrics::counters::auth_attempted(auth_mechanism);
131
132                        let password = config
133                            .password
134                            .as_ref()
135                            .ok_or_else(|| WireError::Authentication("password required".into()))?;
136                        // SECURITY: Convert from Zeroizing wrapper while preserving password content
137                        let pwd_msg = FrontendMessage::Password(password.as_str().to_string());
138                        self.send_message(&pwd_msg).await?;
139                    }
140                    AuthenticationMessage::Md5Password { .. } => {
141                        return Err(WireError::Authentication(
142                            "MD5 authentication not supported. Use SCRAM-SHA-256 or cleartext password".into(),
143                        ));
144                    }
145                    AuthenticationMessage::Sasl { mechanisms } => {
146                        auth_mechanism = crate::metrics::labels::MECHANISM_SCRAM;
147                        crate::metrics::counters::auth_attempted(auth_mechanism);
148                        self.handle_sasl(&mechanisms, config).await?;
149                    }
150                    AuthenticationMessage::SaslContinue { .. } => {
151                        return Err(WireError::Protocol(
152                            "unexpected SaslContinue outside of SASL flow".into(),
153                        ));
154                    }
155                    AuthenticationMessage::SaslFinal { .. } => {
156                        return Err(WireError::Protocol(
157                            "unexpected SaslFinal outside of SASL flow".into(),
158                        ));
159                    }
160                },
161                BackendMessage::BackendKeyData {
162                    process_id,
163                    secret_key,
164                } => {
165                    self.process_id = Some(process_id);
166                    self.secret_key = Some(secret_key);
167                }
168                BackendMessage::ParameterStatus { name, value } => {
169                    tracing::debug!("parameter status: {} = {}", name, value);
170                }
171                BackendMessage::ReadyForQuery { status: _ } => {
172                    break;
173                }
174                BackendMessage::ErrorResponse(err) => {
175                    crate::metrics::counters::auth_failed(auth_mechanism, "server_error");
176                    return Err(WireError::Authentication(err.to_string()));
177                }
178                _ => {
179                    return Err(WireError::Protocol(format!(
180                        "unexpected message during auth: {:?}",
181                        msg
182                    )));
183                }
184            }
185        }
186
187        Ok(())
188    }
189
190    /// Handle SASL authentication (SCRAM-SHA-256)
191    async fn handle_sasl(
192        &mut self,
193        mechanisms: &[String],
194        config: &ConnectionConfig,
195    ) -> Result<()> {
196        // Check if server supports SCRAM-SHA-256
197        if !mechanisms.contains(&"SCRAM-SHA-256".to_string()) {
198            return Err(WireError::Authentication(format!(
199                "server does not support SCRAM-SHA-256. Available: {}",
200                mechanisms.join(", ")
201            )));
202        }
203
204        // Get password
205        let password = config.password.as_ref().ok_or_else(|| {
206            WireError::Authentication("password required for SCRAM authentication".into())
207        })?;
208
209        // Create SCRAM client
210        // SECURITY: Convert from Zeroizing wrapper while preserving password content
211        let mut scram = ScramClient::new(config.user.clone(), password.as_str().to_string());
212        tracing::debug!("initiating SCRAM-SHA-256 authentication");
213
214        // Send SaslInitialResponse with client first message
215        let client_first = scram.client_first();
216        let msg = FrontendMessage::SaslInitialResponse {
217            mechanism: "SCRAM-SHA-256".to_string(),
218            data: client_first.into_bytes(),
219        };
220        self.send_message(&msg).await?;
221
222        // Receive SaslContinue with server first message
223        let server_first_msg = self.receive_message().await?;
224        let server_first_data = match server_first_msg {
225            BackendMessage::Authentication(AuthenticationMessage::SaslContinue { data }) => data,
226            BackendMessage::ErrorResponse(err) => {
227                return Err(WireError::Authentication(format!(
228                    "SASL server error: {}",
229                    err
230                )));
231            }
232            _ => {
233                return Err(WireError::Protocol(
234                    "expected SaslContinue message during SASL authentication".into(),
235                ));
236            }
237        };
238
239        let server_first = String::from_utf8(server_first_data).map_err(|e| {
240            WireError::Authentication(format!("invalid UTF-8 in server first message: {}", e))
241        })?;
242
243        tracing::debug!("received SCRAM server first message");
244
245        // Generate client final message
246        let (client_final, scram_state) = scram
247            .client_final(&server_first)
248            .map_err(|e| WireError::Authentication(format!("SCRAM error: {}", e)))?;
249
250        // Send SaslResponse with client final message
251        let msg = FrontendMessage::SaslResponse {
252            data: client_final.into_bytes(),
253        };
254        self.send_message(&msg).await?;
255
256        // Receive SaslFinal with server verification
257        let server_final_msg = self.receive_message().await?;
258        let server_final_data = match server_final_msg {
259            BackendMessage::Authentication(AuthenticationMessage::SaslFinal { data }) => data,
260            BackendMessage::ErrorResponse(err) => {
261                return Err(WireError::Authentication(format!(
262                    "SASL server error: {}",
263                    err
264                )));
265            }
266            _ => {
267                return Err(WireError::Protocol(
268                    "expected SaslFinal message during SASL authentication".into(),
269                ));
270            }
271        };
272
273        let server_final = String::from_utf8(server_final_data).map_err(|e| {
274            WireError::Authentication(format!("invalid UTF-8 in server final message: {}", e))
275        })?;
276
277        // Verify server signature
278        scram
279            .verify_server_final(&server_final, &scram_state)
280            .map_err(|e| WireError::Authentication(format!("SCRAM verification failed: {}", e)))?;
281
282        tracing::debug!("SCRAM-SHA-256 authentication successful");
283        Ok(())
284    }
285
286    /// Execute a simple query (returns all backend messages)
287    ///
288    /// # Errors
289    ///
290    /// Returns [`WireError::ConnectionBusy`] if the connection is not idle.
291    /// Returns [`WireError::InvalidState`] if the state machine transition fails.
292    /// Returns [`WireError`] on any I/O or protocol error during execution.
293    pub async fn simple_query(&mut self, query: &str) -> Result<Vec<BackendMessage>> {
294        if self.state != ConnectionState::Idle {
295            return Err(WireError::ConnectionBusy(format!(
296                "connection in state: {}",
297                self.state
298            )));
299        }
300
301        self.state.transition(ConnectionState::QueryInProgress)?;
302
303        let query_msg = FrontendMessage::Query(query.to_string());
304        self.send_message(&query_msg).await?;
305
306        self.state.transition(ConnectionState::ReadingResults)?;
307
308        let mut messages = Vec::new();
309
310        loop {
311            let msg = self.receive_message().await?;
312            let is_ready = matches!(msg, BackendMessage::ReadyForQuery { .. });
313            messages.push(msg);
314
315            if is_ready {
316                break;
317            }
318        }
319
320        self.state.transition(ConnectionState::Idle)?;
321        Ok(messages)
322    }
323
324    /// Send a frontend message
325    async fn send_message(&mut self, msg: &FrontendMessage) -> Result<()> {
326        let buf = encode_message(msg)?;
327        self.transport.write_all(&buf).await?;
328        self.transport.flush().await?;
329        Ok(())
330    }
331
332    /// Receive a backend message
333    async fn receive_message(&mut self) -> Result<BackendMessage> {
334        loop {
335            // Try to decode a message from buffer (without cloning!)
336            if let Ok((msg, consumed)) = decode_message(&mut self.read_buf) {
337                self.read_buf.advance(consumed);
338                return Ok(msg);
339            }
340
341            // Bound read-buffer growth: a single backend message may not exceed
342            // MAX_MESSAGE_LEN. If we have buffered more than that without
343            // decoding one, the peer is sending an oversized (or
344            // never-terminating) message — fail instead of buffering toward
345            // ~2 GiB (audit M-wire-msg-cap). The full malformed-vs-incomplete
346            // decode-error distinction lands in the wire-protocol phase (H42).
347            if self.read_buf.len() > crate::protocol::decode::MAX_MESSAGE_LEN {
348                return Err(WireError::Protocol(format!(
349                    "backend message exceeds maximum length of {} bytes",
350                    crate::protocol::decode::MAX_MESSAGE_LEN
351                )));
352            }
353
354            // Need more data
355            let n = self.transport.read_buf(&mut self.read_buf).await?;
356            if n == 0 {
357                return Err(WireError::ConnectionClosed);
358            }
359        }
360    }
361
362    /// Close the connection
363    ///
364    /// # Errors
365    ///
366    /// Returns [`WireError::InvalidState`] if the state machine transition to `Closed` fails.
367    /// Returns [`WireError`] if the transport shutdown fails.
368    pub async fn close(mut self) -> Result<()> {
369        self.state.transition(ConnectionState::Closed)?;
370        let _ = self.send_message(&FrontendMessage::Terminate).await;
371        self.transport.shutdown().await?;
372        Ok(())
373    }
374
375    /// Execute a streaming query
376    ///
377    /// Note: This method consumes the connection. The stream maintains the connection
378    /// internally. Once the stream is exhausted or dropped, the connection is closed.
379    ///
380    /// # Errors
381    ///
382    /// Returns `WireError::Io` if sending the query or reading the response fails.
383    /// Returns `WireError::Database` if the server returns an error response.
384    /// Returns `WireError::InvalidSchema` if the row description is not a single JSON column.
385    #[allow(clippy::too_many_arguments)] // Reason: streaming query requires all chunking parameters; a config struct would add allocation overhead
386    pub async fn streaming_query(
387        mut self,
388        query: &str,
389        chunk_size: usize,
390        max_memory: Option<usize>,
391        soft_limit_warn_threshold: Option<f32>,
392        soft_limit_fail_threshold: Option<f32>,
393        enable_adaptive_chunking: bool,
394        adaptive_min_chunk_size: Option<usize>,
395        adaptive_max_chunk_size: Option<usize>,
396    ) -> Result<crate::stream::JsonStream> {
397        async {
398            let startup_start = std::time::Instant::now();
399
400            use crate::json::validate_row_description;
401            use crate::stream::{extract_json_bytes, parse_json, AdaptiveChunking, ChunkingStrategy, JsonStream};
402            use serde_json::Value;
403            use tokio::sync::mpsc;
404
405            if self.state != ConnectionState::Idle {
406                return Err(WireError::ConnectionBusy(format!(
407                    "connection in state: {}",
408                    self.state
409                )));
410            }
411
412            self.state.transition(ConnectionState::QueryInProgress)?;
413
414            let query_msg = FrontendMessage::Query(query.to_string());
415            self.send_message(&query_msg).await?;
416
417            self.state.transition(ConnectionState::ReadingResults)?;
418
419            // Read RowDescription, but handle other messages that may come first
420            // (e.g., ParameterStatus, BackendKeyData, ErrorResponse, NoticeResponse)
421            let row_desc;
422            loop {
423                let msg = self.receive_message().await?;
424
425                match msg {
426                    BackendMessage::ErrorResponse(err) => {
427                        // Query failed - consume ReadyForQuery and return error
428                        tracing::debug!("PostgreSQL error response: {}", err);
429                        loop {
430                            let msg = self.receive_message().await?;
431                            if matches!(msg, BackendMessage::ReadyForQuery { .. }) {
432                                break;
433                            }
434                        }
435                        return Err(WireError::Sql(err.to_string()));
436                    }
437                    BackendMessage::BackendKeyData { process_id, secret_key: _ } => {
438                        // This provides the key needed for cancel requests - store it and continue
439                        tracing::debug!("PostgreSQL backend key data received: pid={}", process_id);
440                        // Note: We would store this if we need to support cancellation
441                        continue;
442                    }
443                    BackendMessage::ParameterStatus { .. } => {
444                        // Parameter status changes are informational - skip them
445                        tracing::debug!("PostgreSQL parameter status change received");
446                        continue;
447                    }
448                    BackendMessage::NoticeResponse(notice) => {
449                        // Notices are non-fatal warnings - skip them
450                        tracing::debug!("PostgreSQL notice: {}", notice);
451                        continue;
452                    }
453                    BackendMessage::RowDescription(_) => {
454                        row_desc = msg;
455                        break;
456                    }
457                    BackendMessage::ReadyForQuery { .. } => {
458                        // Received ReadyForQuery without RowDescription
459                        // This means the query didn't produce a result set
460                        return Err(WireError::Protocol(
461                            "no result set received from query - \
462                             check that the entity name is correct and the table/view exists"
463                                .into(),
464                        ));
465                    }
466                    _ => {
467                        return Err(WireError::Protocol(format!(
468                            "unexpected message type in query response: {:?}",
469                            msg
470                        )));
471                    }
472                }
473            }
474
475            validate_row_description(&row_desc)?;
476
477            // Record startup timing
478            let startup_duration = startup_start.elapsed().as_millis() as u64;
479            let entity = extract_entity_from_query(query).unwrap_or_else(|| "unknown".to_string());
480            crate::metrics::histograms::query_startup_duration(&entity, startup_duration);
481
482            // Create channels
483            let (result_tx, result_rx) = mpsc::channel::<Result<Value>>(chunk_size);
484            let (cancel_tx, mut cancel_rx) = mpsc::channel::<()>(1);
485
486            // Create stream instance first so we can clone its pause/resume signals
487            let entity_for_metrics = extract_entity_from_query(query).unwrap_or_else(|| "unknown".to_string());
488            let entity_for_stream = entity_for_metrics.clone();  // Clone for stream
489
490            let stream = JsonStream::new(
491                result_rx,
492                cancel_tx,
493                entity_for_stream,
494                max_memory,
495                soft_limit_warn_threshold,
496                soft_limit_fail_threshold,
497            );
498
499            // Clone pause/resume signals for background task (only if pause/resume is initialized)
500            let state_lock = stream.clone_state();
501            let pause_signal = stream.clone_pause_signal();
502            let resume_signal = stream.clone_resume_signal();
503
504            // Clone atomic state for fast state checks in background task
505            let state_atomic = stream.clone_state_atomic();
506
507            // Clone pause timeout for background task
508            let pause_timeout = stream.pause_timeout();
509
510            // Spawn background task to read rows
511            let query_start = std::time::Instant::now();
512
513            tokio::spawn(async move {
514                let strategy = ChunkingStrategy::new(chunk_size);
515                let mut chunk = strategy.new_chunk();
516                let mut total_rows = 0u64;
517
518            // Initialize adaptive chunking if enabled
519            let _adaptive = if enable_adaptive_chunking {
520                let mut adp = AdaptiveChunking::new();
521
522                // Apply custom bounds if provided
523                if let Some(min) = adaptive_min_chunk_size {
524                    if let Some(max) = adaptive_max_chunk_size {
525                        adp = adp.with_bounds(min, max);
526                    }
527                }
528
529                Some(adp)
530            } else {
531                None
532            };
533            let _current_chunk_size = chunk_size;
534
535            loop {
536                // Check lightweight atomic state first (fast path)
537                // Only check atomic if pause/resume infrastructure is actually initialized
538                if state_lock.is_some() && state_atomic.load(std::sync::atomic::Ordering::Acquire) == 1 {
539                    // Paused state detected via atomic, now handle with Mutex
540                    if let (Some(ref state_lock), Some(ref _pause_signal), Some(ref resume_signal)) =
541                        (&state_lock, &pause_signal, &resume_signal)
542                    {
543                        let current_state = state_lock.lock().await;
544                        if *current_state == crate::stream::StreamState::Paused {
545                            tracing::debug!("stream paused, waiting for resume");
546                            drop(current_state); // Release lock before waiting
547
548                            // Wait with optional timeout
549                            if let Some(timeout) = pause_timeout {
550                                if tokio::time::timeout(timeout, resume_signal.notified()).await == Ok(()) {
551                                    tracing::debug!("stream resumed");
552                                } else {
553                                    tracing::debug!("pause timeout expired, auto-resuming");
554                                    crate::metrics::counters::stream_pause_timeout_expired(&entity_for_metrics);
555                                }
556                            } else {
557                                // No timeout, wait indefinitely
558                                resume_signal.notified().await;
559                                tracing::debug!("stream resumed");
560                            }
561
562                            // Update state back to Running
563                            let mut state = state_lock.lock().await;
564                            *state = crate::stream::StreamState::Running;
565                        }
566                    }
567                }
568
569                tokio::select! {
570                    // Check for cancellation
571                    _ = cancel_rx.recv() => {
572                        tracing::debug!("query cancelled");
573                        crate::metrics::counters::query_completed("cancelled", &entity_for_metrics);
574                        break;
575                    }
576
577                    // Read next message
578                    msg_result = self.receive_message() => {
579                        match msg_result {
580                            Ok(msg) => match msg {
581                                BackendMessage::DataRow(_) => {
582                                    match extract_json_bytes(&msg) {
583                                        Ok(json_bytes) => {
584                                            chunk.push(json_bytes);
585
586                                            if strategy.is_full(&chunk) {
587                                                let chunk_start = std::time::Instant::now();
588                                                let rows = chunk.into_rows();
589                                                let chunk_size_rows = rows.len() as u64;
590
591                                                // Batch JSON parsing and sending to reduce lock contention
592                                                // Send 8 values per channel send instead of 1 (8x fewer locks)
593                                                const BATCH_SIZE: usize = 8;
594                                                let mut batch = Vec::with_capacity(BATCH_SIZE);
595                                                let mut send_error = false;
596
597                                                for row_bytes in rows {
598                                                    match parse_json(row_bytes) {
599                                                        Ok(value) => {
600                                                            total_rows += 1;
601                                                            batch.push(Ok(value));
602
603                                                            // Send batch when full
604                                                            if batch.len() == BATCH_SIZE {
605                                                                for item in batch.drain(..) {
606                                                                    if result_tx.send(item).await.is_err() {
607                                                                        crate::metrics::counters::query_completed("error", &entity_for_metrics);
608                                                                        send_error = true;
609                                                                        break;
610                                                                    }
611                                                                }
612                                                                if send_error {
613                                                                    break;
614                                                                }
615                                                            }
616                                                        }
617                                                        Err(e) => {
618                                                            crate::metrics::counters::json_parse_error(&entity_for_metrics);
619                                                            let _ = result_tx.send(Err(e)).await;
620                                                            crate::metrics::counters::query_completed("error", &entity_for_metrics);
621                                                            send_error = true;
622                                                            break;
623                                                        }
624                                                    }
625                                                }
626
627                                                // Send remaining batch items
628                                                if !send_error {
629                                                    for item in batch {
630                                                        if result_tx.send(item).await.is_err() {
631                                                            crate::metrics::counters::query_completed("error", &entity_for_metrics);
632                                                            break;
633                                                        }
634                                                    }
635                                                }
636
637                                                // Record chunk metrics (sampled, not per-chunk)
638                                                let chunk_duration = chunk_start.elapsed().as_millis() as u64;
639
640                                                // Only record metrics every 10 chunks to reduce overhead
641                                                let chunk_idx = CHUNK_COUNT.fetch_add(1, Ordering::Relaxed);
642                                                if chunk_idx.is_multiple_of(10) {
643                                                    crate::metrics::histograms::chunk_processing_duration(&entity_for_metrics, chunk_duration);
644                                                    crate::metrics::histograms::chunk_size(&entity_for_metrics, chunk_size_rows);
645                                                }
646
647                                                // Adaptive chunking: disabled by default for better performance
648                                                // Enable only if explicitly requested via enable_adaptive_chunking parameter
649                                                // Note: adaptive adjustment adds ~0.5-1% overhead per chunk
650                                                // For fixed chunk sizes (default), skip this entirely
651
652                                                chunk = strategy.new_chunk();
653                                            }
654                                        }
655                                        Err(e) => {
656                                            crate::metrics::counters::json_parse_error(&entity_for_metrics);
657                                            let _ = result_tx.send(Err(e)).await;
658                                            crate::metrics::counters::query_completed("error", &entity_for_metrics);
659                                            break;
660                                        }
661                                    }
662                                }
663                                BackendMessage::CommandComplete(_) => {
664                                    // Send remaining chunk
665                                    if !chunk.is_empty() {
666                                        let chunk_start = std::time::Instant::now();
667                                        let rows = chunk.into_rows();
668                                        let chunk_size_rows = rows.len() as u64;
669
670                                        // Batch JSON parsing and sending to reduce lock contention
671                                        const BATCH_SIZE: usize = 8;
672                                        let mut batch = Vec::with_capacity(BATCH_SIZE);
673                                        let mut send_error = false;
674
675                                        for row_bytes in rows {
676                                            match parse_json(row_bytes) {
677                                                Ok(value) => {
678                                                    total_rows += 1;
679                                                    batch.push(Ok(value));
680
681                                                    // Send batch when full
682                                                    if batch.len() == BATCH_SIZE {
683                                                        for item in batch.drain(..) {
684                                                            if result_tx.send(item).await.is_err() {
685                                                                crate::metrics::counters::query_completed("error", &entity_for_metrics);
686                                                                send_error = true;
687                                                                break;
688                                                            }
689                                                        }
690                                                        if send_error {
691                                                            break;
692                                                        }
693                                                    }
694                                                }
695                                                Err(e) => {
696                                                    crate::metrics::counters::json_parse_error(&entity_for_metrics);
697                                                    let _ = result_tx.send(Err(e)).await;
698                                                    crate::metrics::counters::query_completed("error", &entity_for_metrics);
699                                                    send_error = true;
700                                                    break;
701                                                }
702                                            }
703                                        }
704
705                                        // Send remaining batch items
706                                        if !send_error {
707                                            for item in batch {
708                                                if result_tx.send(item).await.is_err() {
709                                                    crate::metrics::counters::query_completed("error", &entity_for_metrics);
710                                                    break;
711                                                }
712                                            }
713                                        }
714
715                                        // Record final chunk metrics (sampled)
716                                        let chunk_duration = chunk_start.elapsed().as_millis() as u64;
717                                        let chunk_idx = CHUNK_COUNT.fetch_add(1, Ordering::Relaxed);
718                                        if chunk_idx.is_multiple_of(10) {
719                                            crate::metrics::histograms::chunk_processing_duration(&entity_for_metrics, chunk_duration);
720                                            crate::metrics::histograms::chunk_size(&entity_for_metrics, chunk_size_rows);
721                                        }
722                                        chunk = strategy.new_chunk();
723                                    }
724
725                                    // Record query completion metrics
726                                    let query_duration = query_start.elapsed().as_millis() as u64;
727                                    crate::metrics::counters::rows_processed(&entity_for_metrics, total_rows, "ok");
728                                    crate::metrics::histograms::query_total_duration(&entity_for_metrics, query_duration);
729                                    crate::metrics::counters::query_completed("success", &entity_for_metrics);
730                                }
731                                BackendMessage::ReadyForQuery { .. } => {
732                                    break;
733                                }
734                                BackendMessage::ErrorResponse(err) => {
735                                    crate::metrics::counters::query_error(&entity_for_metrics, "server_error");
736                                    crate::metrics::counters::query_completed("error", &entity_for_metrics);
737                                    let _ = result_tx.send(Err(WireError::Sql(err.to_string()))).await;
738                                    break;
739                                }
740                                _ => {
741                                    crate::metrics::counters::query_error(&entity_for_metrics, "protocol_error");
742                                    crate::metrics::counters::query_completed("error", &entity_for_metrics);
743                                    let _ = result_tx.send(Err(WireError::Protocol(
744                                        format!("unexpected message: {:?}", msg)
745                                    ))).await;
746                                    break;
747                                }
748                            },
749                            Err(e) => {
750                                crate::metrics::counters::query_error(&entity_for_metrics, "connection_error");
751                                crate::metrics::counters::query_completed("error", &entity_for_metrics);
752                                let _ = result_tx.send(Err(e)).await;
753                                break;
754                            }
755                        }
756                    }
757                }
758            }
759            });
760
761            Ok(stream)
762        }
763        .instrument(tracing::debug_span!(
764            "streaming_query",
765            query = %query,
766            chunk_size = %chunk_size
767        ))
768        .await
769    }
770}