Skip to main content

questdb/ingress/column_sender/
sender.rs

1/*******************************************************************************
2 *     ___                  _   ____  ____
3 *    / _ \ _   _  ___  ___| |_|  _ \| __ )
4 *   | | | | | | |/ _ \/ __| __| | | |  _ \
5 *   | |_| | |_| |  __/\__ \ |_| |_| | |_) |
6 *    \__\_\\__,_|\___||___/\__|____/|____/
7 *
8 *  Copyright (c) 2014-2019 Appsicle
9 *  Copyright (c) 2019-2025 QuestDB
10 *
11 *  Licensed under the Apache License, Version 2.0 (the "License");
12 *  you may not use this file except in compliance with the License.
13 *  You may obtain a copy of the License at
14 *
15 *  http://www.apache.org/licenses/LICENSE-2.0
16 *
17 *  Unless required by applicable law or agreed to in writing, software
18 *  distributed under the License is distributed on an "AS IS" BASIS,
19 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 *  See the License for the specific language governing permissions and
21 *  limitations under the License.
22 *
23 ******************************************************************************/
24
25//! Borrowed-handle types for the column-major sender.
26
27use std::fmt::{self, Debug, Formatter};
28use std::sync::atomic::Ordering;
29use std::thread;
30use std::time::{Duration, Instant};
31
32use crate::ErrorCode;
33use crate::ingress::AckLevel;
34use crate::ingress::QwpWsSenderError;
35use crate::ingress::buffer::{Buffer, QwpWsColumnarBuffer, QwpWsEncodeScratch, SymbolGlobalDict};
36use crate::ingress::sender::qwp_ws::{
37    SyncQwpWsHandlerState, publish_qwp_ws_payload_background, qwp_ws_acked_fsn_background,
38    qwp_ws_begin_close_background, qwp_ws_check_error_background,
39    qwp_ws_drain_to_deadline_background, qwp_ws_is_terminal_background, qwp_ws_ok_fsn_background,
40    qwp_ws_poll_sender_error_background, qwp_ws_poll_sender_error_notification_background,
41    qwp_ws_published_fsn_background, qwp_ws_sender_errors_dropped_background,
42};
43use crate::ingress::sender::qwp_ws_sfa_publisher::{SfaForegroundPublisher, SfaPublishOutcome};
44#[cfg(feature = "arrow-ingress")]
45use crate::ingress::{ColumnName, TableName};
46use crate::{Result, error};
47
48#[cfg(feature = "arrow-ingress")]
49use super::arrow_batch::{self, ArrowColumnOverride, ArrowTsSource};
50use super::chunk::Chunk;
51use super::conn::{ColumnConn, PublishError};
52use super::encoder;
53
54#[cfg(feature = "arrow-ingress")]
55use arrow::array::RecordBatch;
56
57fn classify_flush_error(err: crate::Error) -> crate::Error {
58    if err.code() == ErrorCode::SocketError {
59        return crate::Error::new(ErrorCode::FailoverRetry, err.msg().to_owned());
60    }
61    err
62}
63
64/// Outcome of publishing a single frame on the direct backend.
65enum FrameOutcome {
66    Published,
67    /// The encoded frame exceeded the negotiated cap before any byte reached
68    /// the wire, so the caller may split the row range and retry. Carries the
69    /// detailed size error so the split floor can surface exact byte counts.
70    TooLarge(crate::Error),
71    /// The deferred window is full before any byte reached the wire. The
72    /// top-level flush surfaces this as the explicit "call sync()" contract;
73    /// a split consumes it internally (commit the published prefix, drain,
74    /// retry the range) since its extra frames are an implementation detail
75    /// the caller cannot account for.
76    NoSlot(crate::Error),
77}
78
79/// The immutable inputs to an Arrow-batch flush, bundled so the recursive split
80/// helpers don't thread five arguments through every call. `batch.slice(...)`
81/// (zero-copy) produces the sub-range views.
82#[cfg(feature = "arrow-ingress")]
83struct ArrowFrameSpec<'a> {
84    table: TableName<'a>,
85    batch: &'a RecordBatch,
86    ts: ArrowTsSource,
87    overrides: &'a [ArrowColumnOverride<'a>],
88}
89
90/// Split point for an oversize `row_count`: the largest multiple of 8 not
91/// exceeding `row_count / 2` (at least 8). `None` once the block is at the
92/// 8-row floor, which cannot be split further because bit-packed `Bool`
93/// columns and validity bitmaps are only byte-addressable.
94fn split_mid(row_count: usize) -> Option<usize> {
95    if row_count <= 8 {
96        return None;
97    }
98    let mid = (row_count / 2) & !7;
99    Some(if mid == 0 { 8 } else { mid })
100}
101
102/// Per-frame payload caps for the store-and-forward publish path, derived once
103/// per flush by [`SfaBackend::effective_frame_caps`].
104#[derive(Clone, Copy)]
105struct SfaFrameCaps {
106    /// The binding single-frame limit: the negotiated `max_buf_size` clamped
107    /// to the store-and-forward segment payload capacity. The queue rejects
108    /// any larger frame outright, so the split valve must trip against this
109    /// bound — comparing against `max_buf_size` alone (100 MiB default) left
110    /// it dead below the segment cap, turning splittable flushes into hard
111    /// `PayloadExceedsByteCapacity` failures.
112    hard: usize,
113    /// Split target: at most the two-frames-per-segment payload size, so
114    /// split output amortizes segment rotation instead of emitting one
115    /// near-cap frame per segment. Never exceeds `hard`.
116    soft: usize,
117}
118
119impl SfaFrameCaps {
120    /// Cap for one publish attempt: the split target while the range can
121    /// still split; the hard cap at the 8-row floor, where splitting is no
122    /// longer an option and only the binding limit matters.
123    fn for_range(self, row_count: usize) -> usize {
124        if split_mid(row_count).is_some() {
125            self.soft
126        } else {
127            self.hard
128        }
129    }
130}
131
132/// A store-and-forward frame exceeded `frame_cap` with no way left to split.
133/// Names both contributing limits so the caller adjusts the knob that
134/// actually binds (`sf_max_segment_bytes` when the segment cap is the smaller one).
135fn sfa_frame_size_error(encoded_len: usize, frame_cap: usize) -> crate::Error {
136    error::fmt!(
137        BatchTooLarge,
138        "QWP frame ({} bytes) exceeds the store-and-forward per-frame cap ({} bytes, \
139         the smaller of max_buf_size and the sf_max_segment_bytes segment payload capacity)",
140        encoded_len,
141        frame_cap
142    )
143}
144
145/// Whether a flush also waits for a server completion boundary at the
146/// requested [`AckLevel`] before returning (an "ACKing flush"), or returns as
147/// soon as the frame is published ("publish-only").
148#[derive(Clone, Copy, Debug)]
149pub(crate) enum WaitForAck {
150    No,
151    Yes(AckLevel),
152}
153
154/// Delivery certainty of the **current input** when an ACKing flush fails.
155///
156/// Drives the C FFI Arrow re-export decision: `NotDelivered` may re-export the
157/// caller's batch for retry; `DeliveryUnknown` must not. The
158/// distinction is delivery certainty of the current input, *not* the error
159/// code — a direct-mode `write_all`/`flush` error or a post-publish ACK-wait
160/// failure is `DeliveryUnknown` even though it reports `FailoverRetry`.
161#[derive(Debug)]
162#[doc(hidden)]
163#[non_exhaustive]
164pub enum FlushFailure {
165    /// Provably not transmitted (ACK/durable validation, encode, size check,
166    /// or a transport error before any byte was written). Manual chunks remain
167    /// untouched; Arrow input may be re-exported for retry.
168    NotDelivered(crate::Error),
169    /// May have reached the server: the write succeeded, partially succeeded,
170    /// or the post-write ACK wait failed. The current input must not be
171    /// re-exported (Arrow) or blindly replayed (manual chunk).
172    ///
173    /// Manual-chunk state depends on the sub-case: the chunk is cleared once
174    /// publication succeeds (so an ACKing flush whose
175    /// later ACK wait fails leaves it cleared), but a *partial write*
176    /// (`PublishError::DuringWrite`) returns before the chunk is cleared and so
177    /// leaves it populated. Either way delivery is uncertain — chunk state is
178    /// not a "safe to retry" signal here. [`FlushFailure::into_error`] tags the
179    /// wrapped error [`in_doubt`](crate::Error::in_doubt) so publish-only
180    /// callers can detect this without inspecting the error code.
181    DeliveryUnknown(crate::Error),
182}
183
184impl FlushFailure {
185    /// The underlying error, collapsing the delivery classification into a
186    /// plain [`crate::Error`]. Used by the public `Result<()>`-returning API,
187    /// which never re-exports. The `DeliveryUnknown` arm tags the error
188    /// [`in_doubt`](crate::Error::in_doubt) so publish-only callers retain the
189    /// delivery-unknown signal that the enum carried.
190    #[doc(hidden)]
191    pub fn into_error(self) -> crate::Error {
192        match self {
193            FlushFailure::NotDelivered(e) => e,
194            FlushFailure::DeliveryUnknown(e) => e.with_in_doubt(true),
195        }
196    }
197
198    /// `true` when the current input was provably not transmitted.
199    #[doc(hidden)]
200    #[must_use]
201    pub fn is_not_delivered(&self) -> bool {
202        matches!(self, FlushFailure::NotDelivered(_))
203    }
204}
205
206/// Direct-backend `NotDelivered`: map a socket error to `FailoverRetry`, the
207/// same way publish-only [`classify_flush_error`] does.
208fn direct_not_delivered(e: crate::Error) -> FlushFailure {
209    FlushFailure::NotDelivered(classify_flush_error(e))
210}
211
212/// Direct-backend `DeliveryUnknown`: same `FailoverRetry` mapping, but the
213/// current input may already be on the wire.
214fn direct_delivery_unknown(e: crate::Error) -> FlushFailure {
215    FlushFailure::DeliveryUnknown(classify_flush_error(e))
216}
217
218/// Downgrade a split sub-range failure once an earlier sub-range has already
219/// committed (direct) or been enqueued (store-and-forward): the chunk is now
220/// partially on the server, so it is no longer safe to blind-retry the whole
221/// chunk. `NotDelivered` (safe to re-export) becomes `DeliveryUnknown` (in
222/// doubt, must not re-export); `DeliveryUnknown` is left unchanged so we never
223/// mask an in-doubt failure as retryable.
224fn deny_retry_after_partial(f: FlushFailure) -> FlushFailure {
225    match f {
226        FlushFailure::NotDelivered(e) => FlushFailure::DeliveryUnknown(e),
227        other => other,
228    }
229}
230
231pub struct PooledSenderCore {
232    backend: Box<SfaBackend>,
233}
234
235/// Hidden direct sender used by whole-source DataFrame ingestion. It is a
236/// distinct type from [`PooledSenderCore`] so store-and-forward-only methods
237/// (Buffer publication, FSNs, and timed waits) cannot be called on it.
238#[doc(hidden)]
239pub struct DirectSenderCore {
240    backend: Box<DirectColumnBackend>,
241}
242
243struct DirectColumnBackend {
244    conn: ColumnConn,
245    symbol_dict: SymbolGlobalDict,
246    scratch: encoder::EncodeScratch,
247    first_frame_sent: bool,
248    /// A mid-split internal `sync` committed a prefix server-side since the
249    /// last successful commit+ack boundary (a caller `sync` or a waited
250    /// flush). While set, a failed `sync` classifies as delivery-unknown
251    /// even at "provably not delivered" sites: a blind whole-operation
252    /// resend would duplicate that prefix.
253    commit_since_sync: bool,
254}
255
256struct SfaBackend {
257    /// Owns the retained payload and the one in-memory/persisted symbol namespace
258    /// shared by every pooled encoder. Declared before `state` so its side-file
259    /// handle closes before the runner releases the slot lock.
260    foreground: SfaForegroundPublisher,
261    state: SyncQwpWsHandlerState,
262    buffer_scratch: QwpWsEncodeScratch,
263    scratch: encoder::EncodeScratch,
264    max_buf_size: usize,
265    request_durable_ack: bool,
266    /// No-progress deadline for the `sync` poll loop. Mirrors the direct
267    /// backend's socket `request_timeout`: it bounds how long `sync` waits
268    /// *without the ack/durable watermark advancing*, so a silent-but-alive
269    /// peer (back-pressured WAL, stuck commit) cannot block the caller
270    /// forever. A legitimately slow-but-progressing sync keeps resetting it.
271    /// `Duration::ZERO` disables the deadline (unbounded, legacy behaviour).
272    sync_timeout: Duration,
273    last_ok_sync_boundary: Option<u64>,
274    last_durable_sync_boundary: Option<u64>,
275    drop_on_return: bool,
276}
277
278impl Debug for PooledSenderCore {
279    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
280        f.debug_struct("PooledSenderCore")
281            .field(
282                "must_close",
283                &(self.backend.drop_on_return
284                    || qwp_ws_is_terminal_background(&self.backend.state)),
285            )
286            .finish()
287    }
288}
289
290impl Debug for DirectSenderCore {
291    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
292        f.debug_struct("DirectSenderCore")
293            .field("must_close", &self.backend.conn.must_close())
294            .field("in_flight", &self.backend.conn.in_flight())
295            .finish()
296    }
297}
298
299impl PooledSenderCore {
300    pub(crate) fn new_store_and_forward(
301        mut state: SyncQwpWsHandlerState,
302        max_buf_size: usize,
303        request_durable_ack: bool,
304        sync_timeout: Duration,
305    ) -> Result<Self> {
306        // The background driver enables its catch-up mirror on exactly the same
307        // condition as this foreground (memory mode always; file mode iff the
308        // side-file opened), so the two stay in lockstep: both emit delta, or both
309        // stay full-dict. In file mode, seed the dictionary from the recovered
310        // entries (so new symbols continue above the recovered ids) and claim the
311        // side-file for write-ahead -- the standalone replay encoder in this state
312        // is dormant for the pooled core, so connect_sfa_background left the
313        // side-file for us.
314        let delta_dict_enabled = state.delta_dict_enabled;
315        let persisted_symbol_dict = state.persisted_symbol_dict.take();
316        let mut foreground = SfaForegroundPublisher::new(delta_dict_enabled, persisted_symbol_dict);
317        if delta_dict_enabled {
318            // Take the recovered entries so the (potentially large) buffer is freed
319            // after seeding rather than living dead in `state` -- which the backend
320            // holds for its whole life -- for the connection's duration.
321            //
322            // Propagate a seed failure rather than degrading to dense: this is a
323            // producer, so a partially-seeded dictionary (which is what `seed`
324            // leaves behind) would hand the next interned symbol an id the stored
325            // frames already use. The orphan drainer deliberately does the
326            // opposite on the same error; both sides are argued in
327            // `SymbolGlobalDict::seed`'s docs. `SymbolDictFull` reaches the caller
328            // intact here, so a slot recovered by an under-capped client fails
329            // loudly and stays on disk.
330            let recovered = std::mem::take(&mut state.recovered_dict_entries);
331            foreground.seed(&recovered, state.recovered_dict_count)?;
332        }
333        // The standalone replay encoder in `state` is dormant for the pooled core
334        // (the foreground above owns the live dictionary); release the recovered
335        // dictionary seeded into it at connect so it is not carried dead for the
336        // connection's life -- matching the `recovered_dict_entries` take above.
337        state.release_dormant_encoder_dict();
338        Ok(Self {
339            backend: Box::new(SfaBackend {
340                foreground,
341                state,
342                buffer_scratch: QwpWsEncodeScratch::new(),
343                scratch: encoder::EncodeScratch::new(),
344                max_buf_size,
345                request_durable_ack,
346                sync_timeout,
347                last_ok_sync_boundary: None,
348                last_durable_sync_boundary: None,
349                drop_on_return: false,
350            }),
351        })
352    }
353
354    /// Scope the ack barrier and diagnostic polling to the borrowing
355    /// lease: fast-forward the sync boundaries past everything already
356    /// published so `wait` covers only the lease's own publications (and
357    /// short-circuits when it published nothing), and discard ring entries
358    /// recorded before the borrow — the pool's error handler already
359    /// received them when they were recorded.
360    pub(crate) fn rebase_lease_observation(&mut self) {
361        let sfa = &mut self.backend;
362        if let Ok(published) = qwp_ws_published_fsn_background(&sfa.state) {
363            sfa.last_ok_sync_boundary = published;
364            sfa.last_durable_sync_boundary = published;
365        }
366        while let Ok(Some(_)) = qwp_ws_poll_sender_error_background(&sfa.state) {}
367        while let Ok(Some(_)) = qwp_ws_poll_sender_error_notification_background(&sfa.state) {}
368    }
369
370    /// Poll the next server-rejection diagnostic recorded on this
371    /// connection since the lease was borrowed. The pool's error handler
372    /// independently receives every rejection at record time.
373    pub fn poll_error(&self) -> Result<Option<QwpWsSenderError>> {
374        qwp_ws_poll_sender_error_background(&self.backend.state)
375    }
376
377    /// Diagnostics dropped from the connection's bounded ring.
378    pub fn error_events_dropped(&self) -> Result<u64> {
379        qwp_ws_sender_errors_dropped_background(&self.backend.state)
380    }
381
382    #[must_use]
383    pub fn must_close(&self) -> bool {
384        self.backend.drop_on_return || qwp_ws_is_terminal_background(&self.backend.state)
385    }
386
387    pub fn mark_must_close(&mut self) {
388        self.backend.drop_on_return = true;
389    }
390
391    /// Return the current binding per-frame cap and whether the current
392    /// connection advertised `X-QWP-Max-Batch-Size` (whether or not that value
393    /// is the binding limit). The cap also includes the configured
394    /// `max_buf_size` and store-and-forward segment capacity.
395    pub fn effective_frame_cap(&self) -> (usize, bool) {
396        self.backend.effective_hard_frame_cap()
397    }
398
399    /// Non-blocking: `true` once the store-and-forward backend has no
400    /// undelivered frames — every published frame has reached the pool's ack
401    /// watermark (durable when `durable`, otherwise the OK watermark) — so the
402    /// connection can be retired without losing data. A terminal backend
403    /// reports `true`: its queued frames are already unrecoverable.
404    ///
405    /// The pool reaper uses this to avoid evicting an idle connection whose
406    /// background runner is still flushing.
407    pub(crate) fn sfa_fully_delivered(&self, durable: bool) -> bool {
408        let sfa = &self.backend;
409        if qwp_ws_is_terminal_background(&sfa.state) {
410            return true;
411        }
412        let Ok(Some(published)) = qwp_ws_published_fsn_background(&sfa.state) else {
413            // Nothing published yet, or a terminal/poisoned read: no
414            // recoverable frames are queued.
415            return true;
416        };
417        let watermark = if durable {
418            qwp_ws_acked_fsn_background(&sfa.state)
419        } else {
420            qwp_ws_ok_fsn_background(&sfa.state)
421        };
422        matches!(watermark, Ok(Some(w)) if w >= published)
423    }
424
425    /// Non-blocking: stop accepting new store-and-forward publications and wake
426    /// the background runner to flush what is queued. Pairs with
427    /// [`Self::drain_to_deadline`].
428    pub(crate) fn begin_close(&self) {
429        qwp_ws_begin_close_background(&self.backend.state);
430    }
431
432    /// Block until the store-and-forward queue has delivered every published
433    /// frame, or `deadline` elapses. `Ok(())` means fully drained (or nothing
434    /// was queued); `Err` means the drain timed out or the transport went
435    /// terminal with frames still undelivered.
436    pub(crate) fn drain_to_deadline(&mut self, deadline: Option<Instant>) -> crate::Result<()> {
437        qwp_ws_drain_to_deadline_background(&mut self.backend.state, deadline)
438    }
439
440    /// Encode and publish `chunk` into the store-and-forward queue without
441    /// waiting for a server completion boundary.
442    pub fn flush(&mut self, chunk: &mut Chunk<'_>) -> Result<()> {
443        self.backend
444            .flush_chunk(chunk, WaitForAck::No)
445            .map_err(FlushFailure::into_error)
446    }
447
448    /// Encode and publish a QWP/WebSocket row [`Buffer`] into the local
449    /// store-and-forward queue. The buffer is cleared only after local
450    /// publication succeeds.
451    pub fn flush_buffer(&mut self, buffer: &mut Buffer) -> Result<()> {
452        self.flush_buffer_and_get_fsn(buffer).map(|_| ())
453    }
454
455    /// Encode and publish a QWP/WebSocket row [`Buffer`] without clearing it.
456    pub fn flush_buffer_and_keep(&mut self, buffer: &Buffer) -> Result<()> {
457        self.flush_buffer_and_keep_and_get_fsn(buffer).map(|_| ())
458    }
459
460    /// Publish a QWP/WebSocket row [`Buffer`], clear it after local acceptance,
461    /// and return the frame sequence number. Empty buffers publish no frame and
462    /// return `None`.
463    pub fn flush_buffer_and_get_fsn(&mut self, buffer: &mut Buffer) -> Result<Option<u64>> {
464        let fsn = self.publish_buffer(buffer, None)?;
465        buffer.clear();
466        Ok(fsn)
467    }
468
469    /// Publish a QWP/WebSocket row [`Buffer`] without clearing it and return the
470    /// frame sequence number. Empty buffers publish no frame and return `None`.
471    pub fn flush_buffer_and_keep_and_get_fsn(&mut self, buffer: &Buffer) -> Result<Option<u64>> {
472        self.publish_buffer(buffer, None)
473    }
474
475    /// Publish a QWP/WebSocket row [`Buffer`] as a completion boundary, clear
476    /// it after local acceptance, and wait for every prior publication to reach
477    /// `ack_level`. A wait failure after publication still leaves the buffer
478    /// cleared because replay is owned by the store-and-forward queue.
479    pub fn flush_buffer_and_wait(
480        &mut self,
481        buffer: &mut Buffer,
482        ack_level: AckLevel,
483    ) -> Result<()> {
484        let boundary = self.publish_buffer(buffer, Some(ack_level))?;
485        buffer.clear();
486
487        let sfa = &mut self.backend;
488        match boundary {
489            Some(fsn) => sfa
490                .wait_for_boundary(ack_level, fsn, sfa.sync_timeout)
491                .map_err(FlushFailure::DeliveryUnknown)
492                .map_err(FlushFailure::into_error),
493            None => sfa.wait(ack_level, sfa.sync_timeout),
494        }
495    }
496
497    fn publish_buffer(
498        &mut self,
499        buffer: &Buffer,
500        ack_level: Option<AckLevel>,
501    ) -> Result<Option<u64>> {
502        let qwp = buffer.as_qwp_ws().ok_or_else(|| {
503            error::fmt!(
504                InvalidApiCall,
505                "Pooled QWP ingestion requires a QWP/WebSocket buffer created by `QuestDb::new_buffer()`."
506            )
507        })?;
508        self.backend
509            .publish_buffer(qwp, ack_level)
510            .map_err(FlushFailure::into_error)
511    }
512
513    /// Store-and-forward only: encode and publish `chunk` into the local SFA
514    /// queue and return the last published frame sequence number. If a chunk is
515    /// split into multiple frames, the returned FSN is the final frame boundary;
516    /// cumulative ACK coverage of that boundary covers the whole chunk.
517    pub fn flush_and_get_fsn(&mut self, chunk: &mut Chunk<'_>) -> Result<Option<u64>> {
518        self.backend
519            .flush_chunk_and_get_fsn(chunk)
520            .map(Some)
521            .map_err(FlushFailure::into_error)
522    }
523
524    /// Publish `chunk` as a completion boundary, then wait until every frame
525    /// published before or by this call on this borrowed sender reaches
526    /// `ack_level`.
527    ///
528    /// The boundary is cumulative: a successful return means all prior no-wait
529    /// flushes plus this one are acknowledged at `ack_level`. An empty `chunk`
530    /// behaves exactly like [`Self::sync`] (it encodes a header-only frame).
531    ///
532    /// `AckLevel::Durable` requires QuestDB Enterprise and a pool opened with
533    /// `request_durable_ack=on`; otherwise the call is rejected up front
534    /// (`InvalidApiCall`) before `chunk` is touched.
535    ///
536    /// Failure contract: the ACK level is validated, then the frame is
537    /// published, then the wait runs. If publication itself fails the `chunk`
538    /// is untouched and retryable. Once publication succeeds `chunk` is cleared
539    /// even if the later ACK wait fails — at which point delivery of the just
540    /// published frame is **unknown** (it may be committed, rejected, or in
541    /// flight) and the borrow should be dropped/reborrowed per the error class.
542    /// There is no internal failover retry; replay is the caller's
543    /// responsibility.
544    pub fn flush_and_wait(&mut self, chunk: &mut Chunk<'_>, ack_level: AckLevel) -> Result<()> {
545        self.backend
546            .flush_chunk(chunk, WaitForAck::Yes(ack_level))
547            .map_err(FlushFailure::into_error)
548    }
549
550    /// Encode and publish an Arrow [`RecordBatch`] **without** a per-row
551    /// designated timestamp, explicitly delegating timestamp assignment to the
552    /// server (each row is stamped on arrival).
553    ///
554    /// This is the opt-in counterpart to [`Self::flush_arrow_batch_at_column`].
555    /// If your batch carries a real event-time column, prefer
556    /// `flush_arrow_batch_at_column` — reaching for this method instead would
557    /// discard that column's role as the designated timestamp and silently
558    /// substitute server arrival time, producing wrong partitions/order.
559    #[cfg(feature = "arrow-ingress")]
560    pub fn flush_arrow_batch_at_now<'t, T>(
561        &mut self,
562        table: T,
563        batch: &RecordBatch,
564        overrides: &[ArrowColumnOverride<'_>],
565    ) -> Result<()>
566    where
567        T: TryInto<TableName<'t>>,
568        crate::Error: From<T::Error>,
569    {
570        let table: TableName<'t> = table.try_into()?;
571        self.flush_arrow_batch_dispatch(
572            table,
573            batch,
574            ArrowTsSource::ServerNow,
575            overrides,
576            WaitForAck::No,
577        )
578        .map_err(FlushFailure::into_error)
579    }
580
581    /// Store-and-forward only: Arrow counterpart of [`Self::flush_and_get_fsn`]
582    /// for server-stamped batches.
583    #[cfg(feature = "arrow-ingress")]
584    pub fn flush_arrow_batch_at_now_and_get_fsn<'t, T>(
585        &mut self,
586        table: T,
587        batch: &RecordBatch,
588        overrides: &[ArrowColumnOverride<'_>],
589    ) -> Result<Option<u64>>
590    where
591        T: TryInto<TableName<'t>>,
592        crate::Error: From<T::Error>,
593    {
594        let table: TableName<'t> = table.try_into()?;
595        self.flush_arrow_batch_dispatch_get_fsn(table, batch, ArrowTsSource::ServerNow, overrides)
596            .map_err(FlushFailure::into_error)
597    }
598
599    /// ACKing counterpart of [`Self::flush_arrow_batch_at_now`]:
600    /// publish `batch` as a boundary, then wait for `ack_level`. The same
601    /// boundary/durable/failure contract as [`Self::flush_and_wait`] applies.
602    #[cfg(feature = "arrow-ingress")]
603    pub fn flush_arrow_batch_at_now_and_wait<'t, T>(
604        &mut self,
605        table: T,
606        batch: &RecordBatch,
607        overrides: &[ArrowColumnOverride<'_>],
608        ack_level: AckLevel,
609    ) -> Result<()>
610    where
611        T: TryInto<TableName<'t>>,
612        crate::Error: From<T::Error>,
613    {
614        let table: TableName<'t> = table.try_into()?;
615        self.flush_arrow_batch_dispatch(
616            table,
617            batch,
618            ArrowTsSource::ServerNow,
619            overrides,
620            WaitForAck::Yes(ack_level),
621        )
622        .map_err(FlushFailure::into_error)
623    }
624
625    /// Encode and publish an Arrow [`RecordBatch`], sourcing the per-row
626    /// designated timestamp from the named `Timestamp(_)` column of the batch.
627    ///
628    /// Use [`Self::flush_arrow_batch_at_now`] to instead let the server
629    /// stamp each row on arrival.
630    #[cfg(feature = "arrow-ingress")]
631    pub fn flush_arrow_batch_at_column<'t, T>(
632        &mut self,
633        table: T,
634        batch: &RecordBatch,
635        ts_column: ColumnName<'_>,
636        overrides: &[ArrowColumnOverride<'_>],
637    ) -> Result<()>
638    where
639        T: TryInto<TableName<'t>>,
640        crate::Error: From<T::Error>,
641    {
642        let table: TableName<'t> = table.try_into()?;
643        let ts_col_idx = arrow_batch::resolve_ts_column(batch, ts_column)?;
644        self.flush_arrow_batch_dispatch(
645            table,
646            batch,
647            ArrowTsSource::Column(ts_col_idx),
648            overrides,
649            WaitForAck::No,
650        )
651        .map_err(FlushFailure::into_error)
652    }
653
654    /// Encode and publish an Arrow [`RecordBatch`] with one scalar
655    /// nanosecond-precision Unix epoch timestamp as every row's designated
656    /// timestamp, encoded as a repeated constant. Unlike
657    /// [`Self::flush_arrow_batch_at_now`] the value is fixed at the caller,
658    /// so resubmission is idempotent under `DEDUP UPSERT KEYS`. Rejects
659    /// negative (pre-epoch) values.
660    #[cfg(feature = "arrow-ingress")]
661    pub fn flush_arrow_batch_at_scalar_nanos<'t, T>(
662        &mut self,
663        table: T,
664        batch: &RecordBatch,
665        nanos: i64,
666        overrides: &[ArrowColumnOverride<'_>],
667    ) -> Result<()>
668    where
669        T: TryInto<TableName<'t>>,
670        crate::Error: From<T::Error>,
671    {
672        let table: TableName<'t> = table.try_into()?;
673        self.flush_arrow_batch_dispatch(
674            table,
675            batch,
676            ArrowTsSource::ScalarNanos(nanos),
677            overrides,
678            WaitForAck::No,
679        )
680        .map_err(FlushFailure::into_error)
681    }
682
683    /// Store-and-forward only: Arrow counterpart of [`Self::flush_and_get_fsn`]
684    /// for batches whose designated timestamp is sourced from `ts_column`.
685    #[cfg(feature = "arrow-ingress")]
686    pub fn flush_arrow_batch_at_column_and_get_fsn<'t, T>(
687        &mut self,
688        table: T,
689        batch: &RecordBatch,
690        ts_column: ColumnName<'_>,
691        overrides: &[ArrowColumnOverride<'_>],
692    ) -> Result<Option<u64>>
693    where
694        T: TryInto<TableName<'t>>,
695        crate::Error: From<T::Error>,
696    {
697        let table: TableName<'t> = table.try_into()?;
698        let ts_col_idx = arrow_batch::resolve_ts_column(batch, ts_column)?;
699        self.flush_arrow_batch_dispatch_get_fsn(
700            table,
701            batch,
702            ArrowTsSource::Column(ts_col_idx),
703            overrides,
704        )
705        .map_err(FlushFailure::into_error)
706    }
707
708    /// ACKing counterpart of [`Self::flush_arrow_batch_at_column`]: publish
709    /// `batch` as a boundary, then wait for `ack_level`. The same
710    /// boundary/durable/failure contract as [`Self::flush_and_wait`] applies.
711    #[cfg(feature = "arrow-ingress")]
712    pub fn flush_arrow_batch_at_column_and_wait<'t, T>(
713        &mut self,
714        table: T,
715        batch: &RecordBatch,
716        ts_column: ColumnName<'_>,
717        overrides: &[ArrowColumnOverride<'_>],
718        ack_level: AckLevel,
719    ) -> Result<()>
720    where
721        T: TryInto<TableName<'t>>,
722        crate::Error: From<T::Error>,
723    {
724        let table: TableName<'t> = table.try_into()?;
725        let ts_col_idx = arrow_batch::resolve_ts_column(batch, ts_column)?;
726        self.flush_arrow_batch_dispatch(
727            table,
728            batch,
729            ArrowTsSource::Column(ts_col_idx),
730            overrides,
731            WaitForAck::Yes(ack_level),
732        )
733        .map_err(FlushFailure::into_error)
734    }
735
736    /// Backend dispatch shared by every Arrow flush variant.
737    #[cfg(feature = "arrow-ingress")]
738    fn flush_arrow_batch_dispatch(
739        &mut self,
740        table: TableName<'_>,
741        batch: &RecordBatch,
742        ts: ArrowTsSource,
743        overrides: &[ArrowColumnOverride<'_>],
744        wait: WaitForAck,
745    ) -> std::result::Result<(), FlushFailure> {
746        self.backend
747            .flush_arrow_batch(table, batch, ts, overrides, wait)
748    }
749
750    #[cfg(feature = "arrow-ingress")]
751    fn flush_arrow_batch_dispatch_get_fsn(
752        &mut self,
753        table: TableName<'_>,
754        batch: &RecordBatch,
755        ts: ArrowTsSource,
756        overrides: &[ArrowColumnOverride<'_>],
757    ) -> std::result::Result<Option<u64>, FlushFailure> {
758        self.backend
759            .flush_arrow_batch_and_get_fsn(table, batch, ts, overrides)
760            .map(Some)
761    }
762
763    /// Preflight ACK-level validation for the C FFI ACKing-flush entry points.
764    /// Run before the Arrow C Data Interface import consumes `array->release`
765    /// (and before chunk encode), so a rejected `AckLevel::Durable` leaves
766    /// caller-owned input untouched.
767    #[doc(hidden)]
768    pub fn validate_ack_level(&self, ack_level: AckLevel) -> Result<()> {
769        self.backend.validate_ack_level(ack_level)
770    }
771
772    /// FFI-only ACKing Arrow flush (server-stamped) that surfaces the
773    /// [`FlushFailure`] delivery classification so the C layer can decide
774    /// whether to re-export the caller's `ArrowArray`.
775    #[doc(hidden)]
776    #[cfg(feature = "arrow-ingress")]
777    pub fn flush_arrow_batch_at_now_and_wait_ffi(
778        &mut self,
779        table: TableName<'_>,
780        batch: &RecordBatch,
781        overrides: &[ArrowColumnOverride<'_>],
782        ack_level: AckLevel,
783    ) -> std::result::Result<(), FlushFailure> {
784        self.flush_arrow_batch_dispatch(
785            table,
786            batch,
787            ArrowTsSource::ServerNow,
788            overrides,
789            WaitForAck::Yes(ack_level),
790        )
791    }
792
793    /// FFI-only ACKing Arrow flush (column-stamped) that surfaces the
794    /// [`FlushFailure`] delivery classification. A failure to resolve
795    /// `ts_column` is `NotDelivered` (nothing was published).
796    #[doc(hidden)]
797    #[cfg(feature = "arrow-ingress")]
798    pub fn flush_arrow_batch_at_column_and_wait_ffi(
799        &mut self,
800        table: TableName<'_>,
801        batch: &RecordBatch,
802        ts_column: ColumnName<'_>,
803        overrides: &[ArrowColumnOverride<'_>],
804        ack_level: AckLevel,
805    ) -> std::result::Result<(), FlushFailure> {
806        let ts_col_idx =
807            arrow_batch::resolve_ts_column(batch, ts_column).map_err(FlushFailure::NotDelivered)?;
808        self.flush_arrow_batch_dispatch(
809            table,
810            batch,
811            ArrowTsSource::Column(ts_col_idx),
812            overrides,
813            WaitForAck::Yes(ack_level),
814        )
815    }
816
817    pub fn sync(&mut self, ack_level: AckLevel) -> Result<()> {
818        self.backend.sync(ack_level)
819    }
820
821    /// Store-and-forward only: wait up to `timeout` for every frame published
822    /// so far to reach `ack_level`. `timeout` is a no-progress deadline — it
823    /// fires only if the ack watermark fails to advance for that long;
824    /// `Duration::ZERO` waits indefinitely. On expiry it returns a
825    /// [`ErrorCode::FailoverRetry`](crate::ErrorCode::FailoverRetry)
826    /// error and the queued frames are retained for replay.
827    pub fn wait(&mut self, ack_level: AckLevel, timeout: Duration) -> Result<()> {
828        self.backend.wait(ack_level, timeout)
829    }
830
831    /// Store-and-forward only: return the highest frame sequence number
832    /// published locally by this sender, or `None` if no frame has been
833    /// published.
834    pub fn published_fsn(&self) -> Result<Option<u64>> {
835        self.backend.published_fsn()
836    }
837
838    /// Store-and-forward only: return the highest frame sequence number
839    /// completed by server ACK or server-side reject-and-continue, or `None`
840    /// if no frame has completed.
841    pub fn acked_fsn(&self) -> Result<Option<u64>> {
842        self.backend.acked_fsn()
843    }
844}
845
846impl DirectSenderCore {
847    pub(crate) fn new(
848        conn: ColumnConn,
849        symbol_dict: SymbolGlobalDict,
850        scratch: encoder::EncodeScratch,
851        first_frame_sent: bool,
852    ) -> Self {
853        Self {
854            backend: Box::new(DirectColumnBackend {
855                conn,
856                symbol_dict,
857                scratch,
858                first_frame_sent,
859                commit_since_sync: false,
860            }),
861        }
862    }
863
864    #[must_use]
865    pub fn must_close(&self) -> bool {
866        self.backend.conn.must_close()
867    }
868
869    pub fn mark_must_close(&mut self) {
870        self.backend.conn.mark_must_close();
871    }
872
873    pub(crate) fn in_flight(&self) -> u32 {
874        self.backend.conn.in_flight()
875    }
876
877    pub(crate) fn transport_dead(&self) -> bool {
878        self.backend.conn.transport_dead()
879    }
880
881    /// `true` when a best-effort commit of the already-published deferred frames
882    /// can still succeed (transport alive, not hard-latched). A dictionary-full
883    /// (`spent`) connection stays drainable, so the drop-time commit preserves
884    /// its tail; see [`ColumnConn::can_drain_in_flight`].
885    pub(crate) fn can_drain_in_flight(&self) -> bool {
886        self.backend.conn.can_drain_in_flight()
887    }
888
889    pub(crate) fn endpoint_idx(&self) -> usize {
890        self.backend.conn.endpoint_idx()
891    }
892
893    pub fn flush(&mut self, chunk: &mut Chunk<'_>) -> Result<()> {
894        self.backend
895            .flush_inner(chunk, WaitForAck::No)
896            .map_err(FlushFailure::into_error)
897    }
898
899    pub fn flush_and_wait(&mut self, chunk: &mut Chunk<'_>, ack_level: AckLevel) -> Result<()> {
900        self.backend
901            .flush_inner(chunk, WaitForAck::Yes(ack_level))
902            .map_err(FlushFailure::into_error)
903    }
904
905    #[cfg(feature = "arrow-ingress")]
906    pub fn flush_arrow_batch_at_now<'t, T>(
907        &mut self,
908        table: T,
909        batch: &RecordBatch,
910        overrides: &[ArrowColumnOverride<'_>],
911    ) -> Result<()>
912    where
913        T: TryInto<TableName<'t>>,
914        crate::Error: From<T::Error>,
915    {
916        let table: TableName<'t> = table.try_into()?;
917        self.backend
918            .flush_arrow_batch_inner(
919                table,
920                batch,
921                ArrowTsSource::ServerNow,
922                overrides,
923                WaitForAck::No,
924            )
925            .map_err(FlushFailure::into_error)
926    }
927
928    #[cfg(feature = "arrow-ingress")]
929    pub fn flush_arrow_batch_at_column<'t, T>(
930        &mut self,
931        table: T,
932        batch: &RecordBatch,
933        ts_column: ColumnName<'_>,
934        overrides: &[ArrowColumnOverride<'_>],
935    ) -> Result<()>
936    where
937        T: TryInto<TableName<'t>>,
938        crate::Error: From<T::Error>,
939    {
940        let table: TableName<'t> = table.try_into()?;
941        let ts_col_idx = arrow_batch::resolve_ts_column(batch, ts_column)?;
942        self.backend
943            .flush_arrow_batch_inner(
944                table,
945                batch,
946                ArrowTsSource::Column(ts_col_idx),
947                overrides,
948                WaitForAck::No,
949            )
950            .map_err(FlushFailure::into_error)
951    }
952
953    #[cfg(feature = "arrow-ingress")]
954    pub fn flush_arrow_batch_at_scalar_nanos<'t, T>(
955        &mut self,
956        table: T,
957        batch: &RecordBatch,
958        nanos: i64,
959        overrides: &[ArrowColumnOverride<'_>],
960    ) -> Result<()>
961    where
962        T: TryInto<TableName<'t>>,
963        crate::Error: From<T::Error>,
964    {
965        let table: TableName<'t> = table.try_into()?;
966        self.backend
967            .flush_arrow_batch_inner(
968                table,
969                batch,
970                ArrowTsSource::ScalarNanos(nanos),
971                overrides,
972                WaitForAck::No,
973            )
974            .map_err(FlushFailure::into_error)
975    }
976
977    #[cfg(feature = "arrow-ingress")]
978    pub fn flush_arrow_batch_at_now_and_wait<'t, T>(
979        &mut self,
980        table: T,
981        batch: &RecordBatch,
982        overrides: &[ArrowColumnOverride<'_>],
983        ack_level: AckLevel,
984    ) -> Result<()>
985    where
986        T: TryInto<TableName<'t>>,
987        crate::Error: From<T::Error>,
988    {
989        let table: TableName<'t> = table.try_into()?;
990        self.backend
991            .flush_arrow_batch_inner(
992                table,
993                batch,
994                ArrowTsSource::ServerNow,
995                overrides,
996                WaitForAck::Yes(ack_level),
997            )
998            .map_err(FlushFailure::into_error)
999    }
1000
1001    #[cfg(feature = "arrow-ingress")]
1002    pub fn flush_arrow_batch_at_column_and_wait<'t, T>(
1003        &mut self,
1004        table: T,
1005        batch: &RecordBatch,
1006        ts_column: ColumnName<'_>,
1007        overrides: &[ArrowColumnOverride<'_>],
1008        ack_level: AckLevel,
1009    ) -> Result<()>
1010    where
1011        T: TryInto<TableName<'t>>,
1012        crate::Error: From<T::Error>,
1013    {
1014        let table: TableName<'t> = table.try_into()?;
1015        let ts_col_idx = arrow_batch::resolve_ts_column(batch, ts_column)?;
1016        self.backend
1017            .flush_arrow_batch_inner(
1018                table,
1019                batch,
1020                ArrowTsSource::Column(ts_col_idx),
1021                overrides,
1022                WaitForAck::Yes(ack_level),
1023            )
1024            .map_err(FlushFailure::into_error)
1025    }
1026
1027    #[doc(hidden)]
1028    pub fn validate_ack_level(&self, ack_level: AckLevel) -> Result<()> {
1029        self.backend.conn.validate_ack_level(ack_level)
1030    }
1031
1032    #[doc(hidden)]
1033    #[cfg(feature = "arrow-ingress")]
1034    pub fn flush_arrow_batch_at_now_and_wait_ffi(
1035        &mut self,
1036        table: TableName<'_>,
1037        batch: &RecordBatch,
1038        overrides: &[ArrowColumnOverride<'_>],
1039        ack_level: AckLevel,
1040    ) -> std::result::Result<(), FlushFailure> {
1041        self.backend.flush_arrow_batch_inner(
1042            table,
1043            batch,
1044            ArrowTsSource::ServerNow,
1045            overrides,
1046            WaitForAck::Yes(ack_level),
1047        )
1048    }
1049
1050    #[doc(hidden)]
1051    #[cfg(feature = "arrow-ingress")]
1052    pub fn flush_arrow_batch_at_column_and_wait_ffi(
1053        &mut self,
1054        table: TableName<'_>,
1055        batch: &RecordBatch,
1056        ts_column: ColumnName<'_>,
1057        overrides: &[ArrowColumnOverride<'_>],
1058        ack_level: AckLevel,
1059    ) -> std::result::Result<(), FlushFailure> {
1060        let ts_col_idx =
1061            arrow_batch::resolve_ts_column(batch, ts_column).map_err(FlushFailure::NotDelivered)?;
1062        self.backend.flush_arrow_batch_inner(
1063            table,
1064            batch,
1065            ArrowTsSource::Column(ts_col_idx),
1066            overrides,
1067            WaitForAck::Yes(ack_level),
1068        )
1069    }
1070
1071    pub fn sync(&mut self, ack_level: AckLevel) -> Result<()> {
1072        self.backend.sync(ack_level)
1073    }
1074}
1075
1076impl DirectColumnBackend {
1077    fn sync(&mut self, ack_level: AckLevel) -> Result<()> {
1078        // An ACKing flush of an empty chunk *is* `sync`: it publishes a
1079        // non-deferred header-only commit frame, then drains to the ack level
1080        // (`sync_all_acks`). `sync` is not a data flush, so it must leave the
1081        // deferral state untouched — the next data flush still chooses defer
1082        // from `first_frame_sent` exactly as before.
1083        //
1084        // Pending deferred frames die uncommitted with the connection, so a
1085        // failure before this sync's commit frame could reach the wire is
1086        // genuinely not-delivered and a whole-operation resend is safe —
1087        // unless a mid-split internal sync already committed a prefix
1088        // (`commit_since_sync`): then the same sites must report
1089        // delivery-unknown or a blind resend would duplicate that prefix.
1090        let first_frame_sent = self.first_frame_sent;
1091        let mut commit_chunk = Chunk::new("");
1092        let mut result = self.flush_inner(&mut commit_chunk, WaitForAck::Yes(ack_level));
1093        self.first_frame_sent = first_frame_sent;
1094        if self.commit_since_sync {
1095            result = result.map_err(deny_retry_after_partial);
1096        }
1097        result.map_err(FlushFailure::into_error)
1098    }
1099
1100    fn flush_inner(
1101        &mut self,
1102        chunk: &mut Chunk<'_>,
1103        wait: WaitForAck,
1104    ) -> std::result::Result<(), FlushFailure> {
1105        // ACK validation is a preflight: a bad / durable-without-opt-in level
1106        // is rejected before any encode or write touches caller-owned state.
1107        // An ACKing flush publishes its data-bearing frame non-deferred and
1108        // then drains in-flight to zero, so it neither defers nor needs the
1109        // reserved commit slot.
1110        let defer_commit = match wait {
1111            WaitForAck::No => self.first_frame_sent,
1112            WaitForAck::Yes(level) => {
1113                self.conn
1114                    .validate_ack_level(level)
1115                    .map_err(direct_not_delivered)?;
1116                false
1117            }
1118        };
1119
1120        self.conn.try_drain_acks().map_err(direct_not_delivered)?;
1121
1122        // Whole-chunk fast path: no slicing, no extra allocation. Only when a
1123        // single frame would exceed the negotiated cap do we fall back to
1124        // splitting the row range into multiple frames, all but the last
1125        // deferred so the chunk still commits atomically at one boundary.
1126        match self.publish_frame(chunk, None, defer_commit)? {
1127            FrameOutcome::Published => {}
1128            FrameOutcome::NoSlot(err) => return Err(FlushFailure::NotDelivered(err)),
1129            FrameOutcome::TooLarge(err) => {
1130                let row_count = chunk.row_count();
1131                match split_mid(row_count) {
1132                    Some(mid) => {
1133                        // Splitting publishes deferred prefix frames as it goes.
1134                        // If a later sub-range hits the floor, those frames sit
1135                        // on the wire uncommitted; tear the connection down so
1136                        // the drop-time best-effort commit discards them instead
1137                        // of committing a partial chunk under a later boundary.
1138                        let mut committed = false;
1139                        let mut result = self.publish_split(chunk, 0, mid, true, &mut committed);
1140                        if result.is_ok() {
1141                            result = self.publish_split(
1142                                chunk,
1143                                mid,
1144                                row_count - mid,
1145                                defer_commit,
1146                                &mut committed,
1147                            );
1148                        }
1149                        if let Err(e) = result {
1150                            self.conn.mark_must_close();
1151                            // A mid-split sync may already have committed a
1152                            // prefix. The deferred remainder is discarded on
1153                            // drop, but a committed prefix is real, so downgrade
1154                            // a "safe to retry" failure to in-doubt to avoid
1155                            // duplicating it.
1156                            return Err(if committed {
1157                                deny_retry_after_partial(e)
1158                            } else {
1159                                e
1160                            });
1161                        }
1162                    }
1163                    None => return Err(direct_not_delivered(err)),
1164                }
1165            }
1166        }
1167
1168        // Once published, the chunk is no longer needed for completion/replay
1169        // (rule holds even if the later ACK wait fails).
1170        chunk.clear();
1171
1172        if let WaitForAck::Yes(level) = wait {
1173            self.conn
1174                .sync_all_acks(level)
1175                .map_err(direct_delivery_unknown)?;
1176            self.commit_since_sync = false;
1177        }
1178        Ok(())
1179    }
1180
1181    /// Publish one frame. `range` is `None` for the whole chunk (the hot path,
1182    /// no slice allocation) or `Some((offset, count))` for a sub-range while
1183    /// splitting. Returns [`FrameOutcome::TooLarge`] (nothing on the wire, dict
1184    /// rolled back) when the frame exceeds the cap so the caller can split;
1185    /// every other failure is a terminal [`FlushFailure`].
1186    fn publish_frame(
1187        &mut self,
1188        chunk: &Chunk<'_>,
1189        range: Option<(usize, usize)>,
1190        defer_commit: bool,
1191    ) -> std::result::Result<FrameOutcome, FlushFailure> {
1192        if defer_commit && !self.conn.has_sync_commit_slot() {
1193            return Ok(FrameOutcome::NoSlot(error::fmt!(
1194                InvalidApiCall,
1195                "column sender deferred flush capacity exhausted; call sync() \
1196                 before flushing more chunks."
1197            )));
1198        }
1199
1200        if self.conn.at_in_flight_cap() {
1201            self.conn
1202                .drain_one_ack_blocking()
1203                .map_err(direct_not_delivered)?;
1204        }
1205
1206        let dict_mark = self.symbol_dict.mark();
1207        let result = self.conn.publish_qwp(|out| match range {
1208            None => encoder::encode_chunk_into(
1209                out,
1210                chunk,
1211                &mut self.symbol_dict,
1212                &mut self.scratch,
1213                defer_commit,
1214            ),
1215            Some((offset, count)) => {
1216                let view = unsafe { chunk.slice_rows(offset, count) };
1217                encoder::encode_chunk_into(
1218                    out,
1219                    &view,
1220                    &mut self.symbol_dict,
1221                    &mut self.scratch,
1222                    defer_commit,
1223                )
1224            }
1225        });
1226
1227        match result {
1228            Ok(published) => {
1229                self.conn.push_pending(published.fsn);
1230                self.first_frame_sent = true;
1231                Ok(FrameOutcome::Published)
1232            }
1233            Err(PublishError::BeforeWrite(e)) if e.code() == ErrorCode::BatchTooLarge => {
1234                self.symbol_dict.rollback(dict_mark);
1235                Ok(FrameOutcome::TooLarge(e))
1236            }
1237            Err(PublishError::BeforeWrite(e)) => {
1238                if e.code() != ErrorCode::SocketError {
1239                    self.symbol_dict.rollback(dict_mark);
1240                }
1241                self.latch_if_connection_is_spent(&e);
1242                Err(direct_not_delivered(e))
1243            }
1244            // Bytes may be on the wire: do not roll back the dict, and report
1245            // delivery as unknown.
1246            Err(PublishError::DuringWrite(e)) => Err(direct_delivery_unknown(e)),
1247        }
1248    }
1249
1250    /// Marks the connection **spent** when `err` reports a condition that belongs
1251    /// to the CONNECTION rather than to the call that surfaced it — one no later
1252    /// call on this connection can clear.
1253    ///
1254    /// Today that is only [`SymbolDictFull`](ErrorCode::SymbolDictFull): the
1255    /// connection-scoped symbol dictionary is owned by the connection, nothing
1256    /// resets it in place (a reconnect re-registers it rather than clearing it),
1257    /// and every later flush introducing a new symbol fails identically.
1258    ///
1259    /// Without the mark, [`reborrow_from_pool`] -- the documented failover
1260    /// primitive, and the first thing a caller reaches for after a flush error --
1261    /// is a silent no-op: its guard returns early on `in_flight() == 0 &&
1262    /// !must_close() && !transport_dead()`, and a dictionary-full connection
1263    /// satisfies all three (nothing reached the wire, the buffer rolled back, the
1264    /// socket is healthy). The caller then retries into the same wall indefinitely
1265    /// with no error escalation. Marked spent, the reborrow swaps in a fresh
1266    /// connection and the pool return retires this one instead of recycling its
1267    /// dictionary to the next borrower -- the free list is LIFO, so recycling
1268    /// hands the same full connection straight back.
1269    ///
1270    /// [`mark_spent`](ColumnConn::mark_spent), NOT `mark_must_close`: the failing
1271    /// frame never reached the wire and its dictionary mark was rolled back above,
1272    /// but frames the caller flushed *earlier* (deferred, uncommitted) are still on
1273    /// the wire and must not be stranded. `spent` retires the connection like
1274    /// `must_close` yet leaves [`can_drain_in_flight`](ColumnConn::can_drain_in_flight)
1275    /// `true`, so an explicit `commit()` — and the best-effort commit on plain
1276    /// drop — still flush that tail (a symbol-less commit interns nothing, so the
1277    /// full dictionary does not block it). Hard-latching would refuse both and
1278    /// discard the tail; see the `SymbolDictFull` remedy docs.
1279    ///
1280    /// [`reborrow_from_pool`]: crate::db::BorrowedDirectColumnSender::reborrow_from_pool
1281    fn latch_if_connection_is_spent(&mut self, err: &crate::Error) {
1282        if err.code() == ErrorCode::SymbolDictFull {
1283            self.conn.mark_spent();
1284        }
1285    }
1286
1287    /// Publish rows `[row_offset, row_offset + row_count)`, recursively halving
1288    /// the range whenever a frame is still too large. The prefix half is always
1289    /// deferred; the tail half inherits `defer_commit` so the original commit
1290    /// boundary lands on the very last frame.
1291    fn publish_split(
1292        &mut self,
1293        chunk: &Chunk<'_>,
1294        row_offset: usize,
1295        row_count: usize,
1296        defer_commit: bool,
1297        committed: &mut bool,
1298    ) -> std::result::Result<(), FlushFailure> {
1299        let outcome =
1300            match self.publish_frame(chunk, Some((row_offset, row_count)), defer_commit)? {
1301                FrameOutcome::NoSlot(_) => {
1302                    // The deferred window filled mid-split. The extra frames are
1303                    // an internal detail the caller cannot budget for, so commit
1304                    // the published prefix to drain the window and retry this
1305                    // range — rows are split whole, so the early-committed prefix
1306                    // rows are complete.
1307                    self.sync(AckLevel::Ok)
1308                        .map_err(FlushFailure::DeliveryUnknown)?;
1309                    // The prefix is now committed on the server: a later failure
1310                    // anywhere in this split must not report the whole chunk as
1311                    // safe to blind-retry (it would duplicate this prefix).
1312                    *committed = true;
1313                    self.commit_since_sync = true;
1314                    self.publish_frame(chunk, Some((row_offset, row_count)), defer_commit)?
1315                }
1316                outcome => outcome,
1317            };
1318        match outcome {
1319            FrameOutcome::Published => Ok(()),
1320            FrameOutcome::NoSlot(err) => Err(FlushFailure::NotDelivered(err)),
1321            FrameOutcome::TooLarge(err) => match split_mid(row_count) {
1322                Some(mid) => {
1323                    self.publish_split(chunk, row_offset, mid, true, committed)?;
1324                    self.publish_split(
1325                        chunk,
1326                        row_offset + mid,
1327                        row_count - mid,
1328                        defer_commit,
1329                        committed,
1330                    )
1331                }
1332                None => Err(direct_not_delivered(err)),
1333            },
1334        }
1335    }
1336
1337    #[cfg(feature = "arrow-ingress")]
1338    #[allow(clippy::too_many_arguments)]
1339    fn flush_arrow_batch_inner(
1340        &mut self,
1341        table: TableName<'_>,
1342        batch: &RecordBatch,
1343        ts: ArrowTsSource,
1344        overrides: &[ArrowColumnOverride<'_>],
1345        wait: WaitForAck,
1346    ) -> std::result::Result<(), FlushFailure> {
1347        let defer_commit = match wait {
1348            WaitForAck::No => self.first_frame_sent,
1349            WaitForAck::Yes(level) => {
1350                self.conn
1351                    .validate_ack_level(level)
1352                    .map_err(direct_not_delivered)?;
1353                false
1354            }
1355        };
1356
1357        self.conn.try_drain_acks().map_err(direct_not_delivered)?;
1358
1359        let spec = ArrowFrameSpec {
1360            table,
1361            batch,
1362            ts,
1363            overrides,
1364        };
1365        // Whole-batch fast path; split the row range only when a single frame
1366        // exceeds the cap, all but the last deferred so the batch still commits
1367        // at one boundary.
1368        match self.publish_arrow_frame(&spec, None, defer_commit)? {
1369            FrameOutcome::Published => {}
1370            FrameOutcome::NoSlot(err) => return Err(FlushFailure::NotDelivered(err)),
1371            FrameOutcome::TooLarge(err) => {
1372                let row_count = batch.num_rows();
1373                match split_mid(row_count) {
1374                    Some(mid) => {
1375                        let mut committed = false;
1376                        let mut result =
1377                            self.publish_arrow_split(&spec, 0, mid, true, &mut committed);
1378                        if result.is_ok() {
1379                            result = self.publish_arrow_split(
1380                                &spec,
1381                                mid,
1382                                row_count - mid,
1383                                defer_commit,
1384                                &mut committed,
1385                            );
1386                        }
1387                        if let Err(e) = result {
1388                            self.conn.mark_must_close();
1389                            // See `flush_inner`: a mid-split sync may have
1390                            // committed a prefix, so a blind retry would
1391                            // duplicate it.
1392                            return Err(if committed {
1393                                deny_retry_after_partial(e)
1394                            } else {
1395                                e
1396                            });
1397                        }
1398                    }
1399                    None => return Err(direct_not_delivered(err)),
1400                }
1401            }
1402        }
1403
1404        if let WaitForAck::Yes(level) = wait {
1405            self.conn
1406                .sync_all_acks(level)
1407                .map_err(direct_delivery_unknown)?;
1408            self.commit_since_sync = false;
1409        }
1410        Ok(())
1411    }
1412
1413    /// Arrow counterpart of [`Self::publish_frame`]: `range` is `None` for the
1414    /// whole batch or `Some((offset, count))` for a zero-copy `batch.slice`
1415    /// sub-range while splitting.
1416    #[cfg(feature = "arrow-ingress")]
1417    fn publish_arrow_frame(
1418        &mut self,
1419        spec: &ArrowFrameSpec<'_>,
1420        range: Option<(usize, usize)>,
1421        defer_commit: bool,
1422    ) -> std::result::Result<FrameOutcome, FlushFailure> {
1423        if defer_commit && !self.conn.has_sync_commit_slot() {
1424            return Ok(FrameOutcome::NoSlot(error::fmt!(
1425                InvalidApiCall,
1426                "column sender deferred flush capacity exhausted; call sync() \
1427                 before flushing more arrow batches."
1428            )));
1429        }
1430
1431        if self.conn.at_in_flight_cap() {
1432            self.conn
1433                .drain_one_ack_blocking()
1434                .map_err(direct_not_delivered)?;
1435        }
1436
1437        let dict_mark = self.symbol_dict.mark();
1438        let sliced;
1439        let batch = match range {
1440            None => spec.batch,
1441            Some((offset, count)) => {
1442                sliced = spec.batch.slice(offset, count);
1443                &sliced
1444            }
1445        };
1446        let result = self.conn.publish_qwp(|out| {
1447            arrow_batch::encode_arrow_batch_into(
1448                out,
1449                spec.table,
1450                batch,
1451                spec.ts,
1452                spec.overrides,
1453                &mut self.symbol_dict,
1454                defer_commit,
1455            )
1456        });
1457
1458        match result {
1459            Ok(published) => {
1460                self.conn.push_pending(published.fsn);
1461                self.first_frame_sent = true;
1462                Ok(FrameOutcome::Published)
1463            }
1464            Err(PublishError::BeforeWrite(e)) if e.code() == ErrorCode::BatchTooLarge => {
1465                self.symbol_dict.rollback(dict_mark);
1466                Ok(FrameOutcome::TooLarge(e))
1467            }
1468            Err(PublishError::BeforeWrite(e)) => {
1469                if e.code() != ErrorCode::SocketError {
1470                    self.symbol_dict.rollback(dict_mark);
1471                }
1472                self.latch_if_connection_is_spent(&e);
1473                Err(direct_not_delivered(e))
1474            }
1475            Err(PublishError::DuringWrite(e)) => Err(direct_delivery_unknown(e)),
1476        }
1477    }
1478
1479    /// Arrow counterpart of [`Self::publish_split`].
1480    #[cfg(feature = "arrow-ingress")]
1481    fn publish_arrow_split(
1482        &mut self,
1483        spec: &ArrowFrameSpec<'_>,
1484        row_offset: usize,
1485        row_count: usize,
1486        defer_commit: bool,
1487        committed: &mut bool,
1488    ) -> std::result::Result<(), FlushFailure> {
1489        let outcome =
1490            match self.publish_arrow_frame(spec, Some((row_offset, row_count)), defer_commit)? {
1491                FrameOutcome::NoSlot(_) => {
1492                    // Same mid-split drain as `publish_split`: commit the
1493                    // published prefix and retry this range.
1494                    self.sync(AckLevel::Ok)
1495                        .map_err(FlushFailure::DeliveryUnknown)?;
1496                    // Prefix committed on the server: see `publish_split`.
1497                    *committed = true;
1498                    self.commit_since_sync = true;
1499                    self.publish_arrow_frame(spec, Some((row_offset, row_count)), defer_commit)?
1500                }
1501                outcome => outcome,
1502            };
1503        match outcome {
1504            FrameOutcome::Published => Ok(()),
1505            FrameOutcome::NoSlot(err) => Err(FlushFailure::NotDelivered(err)),
1506            FrameOutcome::TooLarge(err) => match split_mid(row_count) {
1507                Some(mid) => {
1508                    self.publish_arrow_split(spec, row_offset, mid, true, committed)?;
1509                    self.publish_arrow_split(
1510                        spec,
1511                        row_offset + mid,
1512                        row_count - mid,
1513                        defer_commit,
1514                        committed,
1515                    )
1516                }
1517                None => Err(direct_not_delivered(err)),
1518            },
1519        }
1520    }
1521}
1522
1523impl SfaBackend {
1524    /// Lifted out of [`Self::sync`] so an ACKing flush can reject a
1525    /// durable-without-opt-in request *before* encode mutates the symbol dict
1526    /// or the Arrow import consumes the caller's array.
1527    fn validate_ack_level(&self, ack_level: AckLevel) -> Result<()> {
1528        if ack_level == AckLevel::Durable && !self.request_durable_ack {
1529            return Err(error::fmt!(
1530                InvalidApiCall,
1531                "AckLevel::Durable requires the pool to be opened with \
1532                 `request_durable_ack=on` in the connect string."
1533            ));
1534        }
1535        Ok(())
1536    }
1537
1538    /// Marks the connection for retirement (`drop_on_return`) when `err` reports
1539    /// [`SymbolDictFull`](ErrorCode::SymbolDictFull) — a full connection-scoped
1540    /// symbol dictionary that no later flush can clear. The store-and-forward
1541    /// analogue of the direct backend's
1542    /// [`latch_if_connection_is_spent`](DirectColumnBackend::latch_if_connection_is_spent),
1543    /// and it exists for the same reason: without it a naive pool return / drop
1544    /// *recycles* the full connection, and the LIFO free list hands that same full
1545    /// connection straight back to the next borrow, which fails identically —
1546    /// an unbounded loop with no data to show for it. Latched, the pool return
1547    /// retires it instead, and `return_sfa_to_pool` DRAINS the queue
1548    /// (`drain_sfa_before_drop`, bounded by `close_flush_timeout`) before dropping,
1549    /// so no queued frame is lost.
1550    ///
1551    /// `drop_on_return` does not gate the foreground publish path (which only
1552    /// checks `qwp_ws_check_error_background`), so chunks referencing only
1553    /// already-interned symbols keep flushing until the caller retires the
1554    /// connection — matching the direct backend and the documented contract. With
1555    /// `sf_dir`, `wait()` for the slot to drain before the return: a drop that
1556    /// leaves the slot unresolved persists the dictionary too, and the next
1557    /// borrower re-seeds from the side-file at the same size (see the
1558    /// `SymbolDictFull` remedy docs).
1559    fn latch_if_connection_is_spent(&mut self, err: &crate::Error) {
1560        if err.code() == ErrorCode::SymbolDictFull {
1561            self.drop_on_return = true;
1562        }
1563    }
1564
1565    fn publish_buffer(
1566        &mut self,
1567        buffer: &QwpWsColumnarBuffer,
1568        ack_level: Option<AckLevel>,
1569    ) -> std::result::Result<Option<u64>, FlushFailure> {
1570        if let Some(level) = ack_level {
1571            self.validate_ack_level(level)
1572                .map_err(FlushFailure::NotDelivered)?;
1573        }
1574        if let Err(err) = qwp_ws_check_error_background(&self.state) {
1575            return Err(FlushFailure::NotDelivered(err));
1576        }
1577        if buffer.is_empty() {
1578            return Ok(None);
1579        }
1580
1581        // A Buffer is one indivisible publication (no row-range slicing), so
1582        // only the hard cap applies — there is no split to soft-target.
1583        let frame_cap = self.effective_frame_caps().hard;
1584        let result = {
1585            let Self {
1586                foreground,
1587                state,
1588                buffer_scratch,
1589                ..
1590            } = self;
1591            foreground.encode_persist_publish(
1592                frame_cap,
1593                |payload, symbol_dict, delta_enabled| {
1594                    buffer.encode_ws_replay_message_with_defer(
1595                        payload,
1596                        buffer_scratch,
1597                        symbol_dict,
1598                        super::wire::QWP_VERSION_1,
1599                        false,
1600                        delta_enabled,
1601                    )
1602                },
1603                |payload| publish_qwp_ws_payload_background(state, payload, frame_cap),
1604            )
1605        };
1606        if let Err(err) = &result {
1607            self.latch_if_connection_is_spent(err);
1608        }
1609        match result.map_err(FlushFailure::NotDelivered)? {
1610            SfaPublishOutcome::Published(fsn) => Ok(Some(fsn)),
1611            SfaPublishOutcome::TooLarge {
1612                encoded_len,
1613                max_buf_size,
1614            } => Err(FlushFailure::NotDelivered(sfa_frame_size_error(
1615                encoded_len,
1616                max_buf_size,
1617            ))),
1618        }
1619    }
1620
1621    fn flush_chunk(
1622        &mut self,
1623        chunk: &mut Chunk<'_>,
1624        wait: WaitForAck,
1625    ) -> std::result::Result<(), FlushFailure> {
1626        self.flush_chunk_boundary(chunk, wait).map(|_| ())
1627    }
1628
1629    fn flush_chunk_and_get_fsn(
1630        &mut self,
1631        chunk: &mut Chunk<'_>,
1632    ) -> std::result::Result<u64, FlushFailure> {
1633        self.flush_chunk_boundary(chunk, WaitForAck::No)
1634    }
1635
1636    fn flush_chunk_boundary(
1637        &mut self,
1638        chunk: &mut Chunk<'_>,
1639        wait: WaitForAck,
1640    ) -> std::result::Result<u64, FlushFailure> {
1641        // Preflight: durable opt-in is validated before encode/append, so a
1642        // rejected level leaves the chunk and queue untouched.
1643        if let WaitForAck::Yes(level) = wait {
1644            self.validate_ack_level(level)
1645                .map_err(FlushFailure::NotDelivered)?;
1646        }
1647        if let Err(e) = qwp_ws_check_error_background(&self.state) {
1648            return Err(FlushFailure::NotDelivered(e));
1649        }
1650        let caps = self.effective_frame_caps();
1651        // Whole-chunk fast path; only split when a single frame exceeds the cap.
1652        // Each split frame commits on its own (never deferred) — the
1653        // store-and-forward queue is frame-granular and at-least-once, so deferred
1654        // (uncommitted) frames could be lost on a reconnect that trims them after
1655        // their ack but before the commit. The boundary to wait for is the last
1656        // frame's FSN; its cumulative ack covers the prefix. (In delta mode the
1657        // frames are not individually self-sufficient; the driver re-registers the
1658        // dictionary via a catch-up frame on reconnect.)
1659        let boundary =
1660            match self.publish_chunk_sfa(chunk, None, caps.for_range(chunk.row_count()))? {
1661                SfaPublishOutcome::Published(fsn) => fsn,
1662                SfaPublishOutcome::TooLarge {
1663                    encoded_len,
1664                    max_buf_size,
1665                } => {
1666                    let err = sfa_frame_size_error(encoded_len, max_buf_size);
1667                    let row_count = chunk.row_count();
1668                    match split_mid(row_count) {
1669                        Some(mid) => {
1670                            self.publish_split_sfa(chunk, 0, mid, caps)?;
1671                            // The prefix is now durably queued (at-least-once); a
1672                            // failure on the remainder leaves it enqueued, so the
1673                            // chunk must not be reported as safe to blind-retry.
1674                            self.publish_split_sfa(chunk, mid, row_count - mid, caps)
1675                                .map_err(deny_retry_after_partial)?
1676                        }
1677                        None => return Err(FlushFailure::NotDelivered(err)),
1678                    }
1679                }
1680            };
1681        chunk.clear();
1682        if let WaitForAck::Yes(level) = wait {
1683            // The frame is in the local queue; a wait failure is delivery-unknown.
1684            self.wait_for_boundary(level, boundary, self.sync_timeout)
1685                .map_err(FlushFailure::DeliveryUnknown)?;
1686        }
1687        Ok(boundary)
1688    }
1689
1690    /// Encode `range` (`None` = whole chunk, no slice allocation) as a replay
1691    /// frame, check it against the cap, and append it to the queue. Returns
1692    /// [`SfaPublishOutcome::TooLarge`] (nothing queued, dict rolled back) when the
1693    /// frame exceeds the cap so the caller can split.
1694    fn publish_chunk_sfa(
1695        &mut self,
1696        chunk: &Chunk<'_>,
1697        range: Option<(usize, usize)>,
1698        frame_cap: usize,
1699    ) -> std::result::Result<SfaPublishOutcome, FlushFailure> {
1700        let view;
1701        let target = match range {
1702            None => chunk,
1703            Some((offset, count)) => {
1704                view = unsafe { chunk.slice_rows(offset, count) };
1705                &view
1706            }
1707        };
1708        let result = {
1709            let Self {
1710                state,
1711                foreground,
1712                scratch,
1713                ..
1714            } = self;
1715            foreground.encode_persist_publish(
1716                frame_cap,
1717                |payload, symbol_dict, delta_enabled| {
1718                    if delta_enabled {
1719                        encoder::encode_chunk_into(payload, target, symbol_dict, scratch, false)
1720                    } else {
1721                        encoder::encode_chunk_replay_into(payload, target, symbol_dict, scratch)
1722                    }
1723                },
1724                |encoded| publish_qwp_ws_payload_background(state, encoded, frame_cap),
1725            )
1726        };
1727        if let Err(err) = &result {
1728            self.latch_if_connection_is_spent(err);
1729        }
1730        result.map_err(FlushFailure::NotDelivered)
1731    }
1732
1733    /// Append rows `[row_offset, row_offset + row_count)`, halving the range
1734    /// whenever a frame is still too large. Returns the last frame's FSN.
1735    fn publish_split_sfa(
1736        &mut self,
1737        chunk: &Chunk<'_>,
1738        row_offset: usize,
1739        row_count: usize,
1740        caps: SfaFrameCaps,
1741    ) -> std::result::Result<u64, FlushFailure> {
1742        match self.publish_chunk_sfa(
1743            chunk,
1744            Some((row_offset, row_count)),
1745            caps.for_range(row_count),
1746        )? {
1747            SfaPublishOutcome::Published(fsn) => Ok(fsn),
1748            SfaPublishOutcome::TooLarge {
1749                encoded_len,
1750                max_buf_size,
1751            } => match split_mid(row_count) {
1752                Some(mid) => {
1753                    self.publish_split_sfa(chunk, row_offset, mid, caps)?;
1754                    self.publish_split_sfa(chunk, row_offset + mid, row_count - mid, caps)
1755                        .map_err(deny_retry_after_partial)
1756                }
1757                None => Err(FlushFailure::NotDelivered(sfa_frame_size_error(
1758                    encoded_len,
1759                    max_buf_size,
1760                ))),
1761            },
1762        }
1763    }
1764
1765    #[cfg(feature = "arrow-ingress")]
1766    fn flush_arrow_batch(
1767        &mut self,
1768        table: TableName<'_>,
1769        batch: &RecordBatch,
1770        ts: ArrowTsSource,
1771        overrides: &[ArrowColumnOverride<'_>],
1772        wait: WaitForAck,
1773    ) -> std::result::Result<(), FlushFailure> {
1774        self.flush_arrow_batch_boundary(table, batch, ts, overrides, wait)
1775            .map(|_| ())
1776    }
1777
1778    #[cfg(feature = "arrow-ingress")]
1779    fn flush_arrow_batch_and_get_fsn(
1780        &mut self,
1781        table: TableName<'_>,
1782        batch: &RecordBatch,
1783        ts: ArrowTsSource,
1784        overrides: &[ArrowColumnOverride<'_>],
1785    ) -> std::result::Result<u64, FlushFailure> {
1786        self.flush_arrow_batch_boundary(table, batch, ts, overrides, WaitForAck::No)
1787    }
1788
1789    #[cfg(feature = "arrow-ingress")]
1790    fn flush_arrow_batch_boundary(
1791        &mut self,
1792        table: TableName<'_>,
1793        batch: &RecordBatch,
1794        ts: ArrowTsSource,
1795        overrides: &[ArrowColumnOverride<'_>],
1796        wait: WaitForAck,
1797    ) -> std::result::Result<u64, FlushFailure> {
1798        if let WaitForAck::Yes(level) = wait {
1799            self.validate_ack_level(level)
1800                .map_err(FlushFailure::NotDelivered)?;
1801        }
1802        if let Err(e) = qwp_ws_check_error_background(&self.state) {
1803            return Err(FlushFailure::NotDelivered(e));
1804        }
1805        let caps = self.effective_frame_caps();
1806        let spec = ArrowFrameSpec {
1807            table,
1808            batch,
1809            ts,
1810            overrides,
1811        };
1812        // Whole-batch fast path; split into self-sufficient frames only when one
1813        // exceeds the cap (see the rationale on `flush_chunk`).
1814        let boundary =
1815            match self.publish_arrow_sfa(&spec, None, caps.for_range(batch.num_rows()))? {
1816                SfaPublishOutcome::Published(fsn) => fsn,
1817                SfaPublishOutcome::TooLarge {
1818                    encoded_len,
1819                    max_buf_size,
1820                } => {
1821                    let err = sfa_frame_size_error(encoded_len, max_buf_size);
1822                    let row_count = batch.num_rows();
1823                    match split_mid(row_count) {
1824                        Some(mid) => {
1825                            self.publish_arrow_split_sfa(&spec, 0, mid, caps)?;
1826                            // Prefix is durably queued; see `flush_chunk_boundary`.
1827                            self.publish_arrow_split_sfa(&spec, mid, row_count - mid, caps)
1828                                .map_err(deny_retry_after_partial)?
1829                        }
1830                        None => return Err(FlushFailure::NotDelivered(err)),
1831                    }
1832                }
1833            };
1834        if let WaitForAck::Yes(level) = wait {
1835            self.wait_for_boundary(level, boundary, self.sync_timeout)
1836                .map_err(FlushFailure::DeliveryUnknown)?;
1837        }
1838        Ok(boundary)
1839    }
1840
1841    /// Arrow counterpart of [`Self::publish_chunk_sfa`].
1842    #[cfg(feature = "arrow-ingress")]
1843    fn publish_arrow_sfa(
1844        &mut self,
1845        spec: &ArrowFrameSpec<'_>,
1846        range: Option<(usize, usize)>,
1847        frame_cap: usize,
1848    ) -> std::result::Result<SfaPublishOutcome, FlushFailure> {
1849        let sliced;
1850        let batch = match range {
1851            None => spec.batch,
1852            Some((offset, count)) => {
1853                sliced = spec.batch.slice(offset, count);
1854                &sliced
1855            }
1856        };
1857        let result = {
1858            let Self {
1859                state, foreground, ..
1860            } = self;
1861            foreground.encode_persist_publish(
1862                frame_cap,
1863                |payload, symbol_dict, delta_enabled| {
1864                    if delta_enabled {
1865                        arrow_batch::encode_arrow_batch_into(
1866                            payload,
1867                            spec.table,
1868                            batch,
1869                            spec.ts,
1870                            spec.overrides,
1871                            symbol_dict,
1872                            false,
1873                        )
1874                    } else {
1875                        arrow_batch::encode_arrow_batch_replay_into(
1876                            payload,
1877                            spec.table,
1878                            batch,
1879                            spec.ts,
1880                            spec.overrides,
1881                            symbol_dict,
1882                        )
1883                    }
1884                },
1885                |payload| publish_qwp_ws_payload_background(state, payload, frame_cap),
1886            )
1887        };
1888        if let Err(err) = &result {
1889            self.latch_if_connection_is_spent(err);
1890        }
1891        result.map_err(FlushFailure::NotDelivered)
1892    }
1893
1894    /// Arrow counterpart of [`Self::publish_split_sfa`]. Returns the last
1895    /// frame's FSN.
1896    #[cfg(feature = "arrow-ingress")]
1897    fn publish_arrow_split_sfa(
1898        &mut self,
1899        spec: &ArrowFrameSpec<'_>,
1900        row_offset: usize,
1901        row_count: usize,
1902        caps: SfaFrameCaps,
1903    ) -> std::result::Result<u64, FlushFailure> {
1904        match self.publish_arrow_sfa(
1905            spec,
1906            Some((row_offset, row_count)),
1907            caps.for_range(row_count),
1908        )? {
1909            SfaPublishOutcome::Published(fsn) => Ok(fsn),
1910            SfaPublishOutcome::TooLarge {
1911                encoded_len,
1912                max_buf_size,
1913            } => match split_mid(row_count) {
1914                Some(mid) => {
1915                    self.publish_arrow_split_sfa(spec, row_offset, mid, caps)?;
1916                    self.publish_arrow_split_sfa(spec, row_offset + mid, row_count - mid, caps)
1917                        .map_err(deny_retry_after_partial)
1918                }
1919                None => Err(FlushFailure::NotDelivered(sfa_frame_size_error(
1920                    encoded_len,
1921                    max_buf_size,
1922                ))),
1923            },
1924        }
1925    }
1926
1927    fn sync(&mut self, ack_level: AckLevel) -> Result<()> {
1928        self.wait(ack_level, self.sync_timeout)
1929    }
1930
1931    fn wait(&mut self, ack_level: AckLevel, timeout: Duration) -> Result<()> {
1932        self.validate_ack_level(ack_level)?;
1933        let Some(boundary) = qwp_ws_published_fsn_background(&self.state)? else {
1934            return Ok(());
1935        };
1936        self.wait_for_boundary(ack_level, boundary, timeout)
1937    }
1938
1939    fn published_fsn(&self) -> Result<Option<u64>> {
1940        qwp_ws_published_fsn_background(&self.state)
1941    }
1942
1943    fn acked_fsn(&self) -> Result<Option<u64>> {
1944        qwp_ws_acked_fsn_background(&self.state)
1945    }
1946
1947    /// Block until the OK/durable watermark reaches `boundary`, then record it
1948    /// as the satisfied watermark (so a trailing `sync(level)`/`flush_and_wait`
1949    /// on the same boundary short-circuits). Short-circuits immediately when the
1950    /// cached watermark already covers `boundary`.
1951    fn wait_for_boundary(
1952        &mut self,
1953        ack_level: AckLevel,
1954        boundary: u64,
1955        timeout: Duration,
1956    ) -> Result<()> {
1957        let last_boundary = match ack_level {
1958            AckLevel::Ok => self.last_ok_sync_boundary,
1959            AckLevel::Durable => self.last_durable_sync_boundary,
1960        };
1961        if last_boundary.is_some_and(|last| last >= boundary) {
1962            return Ok(());
1963        }
1964
1965        // No-progress deadline: reset every time the ack/durable watermark
1966        // advances, so this only fires when the peer stays alive yet silent
1967        // (never advancing toward `boundary`) for `sync_timeout`. This mirrors
1968        // the direct backend, whose blocking read re-arms `request_timeout` on
1969        // every received frame and surfaces a timeout when none arrive.
1970        let mut deadline_anchor = Instant::now();
1971        let mut last_completed: Option<u64> = None;
1972
1973        loop {
1974            let completed = match ack_level {
1975                AckLevel::Ok => qwp_ws_ok_fsn_background(&self.state)?,
1976                AckLevel::Durable => qwp_ws_acked_fsn_background(&self.state)?,
1977            };
1978            if completed.is_some_and(|fsn| fsn >= boundary) {
1979                match ack_level {
1980                    AckLevel::Ok => self.last_ok_sync_boundary = Some(boundary),
1981                    AckLevel::Durable => self.last_durable_sync_boundary = Some(boundary),
1982                }
1983                return Ok(());
1984            }
1985            if completed != last_completed {
1986                last_completed = completed;
1987                deadline_anchor = Instant::now();
1988            }
1989
1990            qwp_ws_check_error_background(&self.state)?;
1991
1992            if !timeout.is_zero() && deadline_anchor.elapsed() >= timeout {
1993                return Err(sfa_sync_timeout(timeout, ack_level, boundary, completed));
1994            }
1995            thread::sleep(Duration::from_millis(10));
1996        }
1997    }
1998
1999    fn effective_hard_frame_cap(&self) -> (usize, bool) {
2000        let server_max = self.state.server_max_batch_size.load(Ordering::Acquire);
2001        effective_hard_frame_cap(
2002            self.max_buf_size,
2003            server_max,
2004            self.state.sfa_frame_payload_cap,
2005        )
2006    }
2007
2008    fn effective_frame_caps(&self) -> SfaFrameCaps {
2009        let (hard, _) = self.effective_hard_frame_cap();
2010        SfaFrameCaps {
2011            hard,
2012            soft: hard.min(self.state.sfa_frame_split_target),
2013        }
2014    }
2015}
2016
2017fn effective_hard_frame_cap(
2018    max_buf_size: usize,
2019    server_max_batch_size: usize,
2020    sfa_frame_payload_cap: usize,
2021) -> (usize, bool) {
2022    let configured_cap = if server_max_batch_size == 0 {
2023        max_buf_size
2024    } else {
2025        max_buf_size.min(server_max_batch_size)
2026    };
2027    (
2028        configured_cap.min(sfa_frame_payload_cap),
2029        server_max_batch_size != 0,
2030    )
2031}
2032
2033/// The store-and-forward `wait` poll loop made no progress toward `boundary`
2034/// for `sync_timeout`. The connection is alive but the server is not advancing
2035/// the ack/durable watermark (e.g. back-pressured WAL or a stuck commit).
2036///
2037/// Classified `FailoverRetry`, but — unlike the direct backend's
2038/// transport-timeout path, which drops the connection and discards its
2039/// uncommitted frames — the local SFA queue still holds every unacked frame
2040/// and the background runner keeps delivering them across reborrows and
2041/// reconnects. Re-flushing the same data would therefore duplicate it. The
2042/// correct recovery is to retry `wait()` (idempotent — it only re-observes
2043/// the watermark) until the runner catches up, or to close/drain the pool.
2044/// Delivery has *not* failed; the watermark simply has not reached `boundary`
2045/// yet.
2046fn sfa_sync_timeout(
2047    sync_timeout: Duration,
2048    ack_level: AckLevel,
2049    boundary: u64,
2050    completed: Option<u64>,
2051) -> crate::Error {
2052    let level = match ack_level {
2053        AckLevel::Ok => "ok",
2054        AckLevel::Durable => "durable",
2055    };
2056    let progress = match completed {
2057        Some(fsn) => format!("reached FSN {}", fsn),
2058        None => "reached no frame".to_string(),
2059    };
2060    crate::Error::new(
2061        ErrorCode::FailoverRetry,
2062        format!(
2063            "QWP/WebSocket store-and-forward wait({}) timed out after {:?} \
2064             with no ack progress (target FSN {}, {}); the connection is alive \
2065             but the server is not advancing the watermark. The frames remain \
2066             queued and the background runner keeps delivering them: retry \
2067             wait() to keep awaiting the ack, or close the pool to drain. Do \
2068             not re-flush the same data, which is already accepted and would \
2069             be delivered twice.",
2070            level, sync_timeout, boundary, progress
2071        ),
2072    )
2073}
2074
2075#[cfg(test)]
2076mod tests {
2077    use super::{effective_hard_frame_cap, split_mid};
2078
2079    #[test]
2080    fn effective_hard_cap_reports_whether_the_server_cap_is_known() {
2081        assert_eq!(effective_hard_frame_cap(1000, 0, 800), (800, false));
2082        assert_eq!(effective_hard_frame_cap(1000, 400, 800), (400, true));
2083        assert_eq!(effective_hard_frame_cap(1000, 1200, 800), (800, true));
2084    }
2085
2086    #[test]
2087    fn split_mid_floors_at_eight_rows() {
2088        assert_eq!(split_mid(0), None);
2089        assert_eq!(split_mid(1), None);
2090        assert_eq!(split_mid(8), None);
2091    }
2092
2093    #[test]
2094    fn split_mid_returns_eight_aligned_point_below_count() {
2095        for count in [9usize, 12, 15, 16, 17, 100, 10_000, 16_384] {
2096            let mid = split_mid(count).unwrap();
2097            assert_eq!(
2098                mid % 8,
2099                0,
2100                "split point must be 8-aligned for count {count}"
2101            );
2102            assert!(mid >= 8, "split point must be at least 8 for count {count}");
2103            assert!(
2104                mid < count,
2105                "split point must make progress for count {count}"
2106            );
2107        }
2108    }
2109
2110    #[test]
2111    fn sfa_frame_caps_use_split_target_only_while_range_can_split() {
2112        use super::SfaFrameCaps;
2113
2114        let caps = SfaFrameCaps {
2115            hard: 1000,
2116            soft: 400,
2117        };
2118        // At or below the 8-row split floor there is no split left to aim
2119        // for: only the binding (hard) cap matters.
2120        assert_eq!(caps.for_range(1), 1000);
2121        assert_eq!(caps.for_range(8), 1000);
2122        // Splittable ranges aim for the two-frames-per-segment target.
2123        assert_eq!(caps.for_range(9), 400);
2124        assert_eq!(caps.for_range(10_000), 400);
2125    }
2126
2127    #[test]
2128    fn deny_retry_after_partial_downgrades_not_delivered_and_never_upgrades() {
2129        use super::{FlushFailure, deny_retry_after_partial};
2130        use crate::{Error, ErrorCode};
2131
2132        // Once a split has put a prefix on the server, a "safe to retry"
2133        // (`NotDelivered`) failure on the remainder must become in-doubt
2134        // (`DeliveryUnknown`) so the caller does not blind-retry and duplicate
2135        // the committed / enqueued prefix.
2136        let nd = FlushFailure::NotDelivered(Error::new(ErrorCode::SocketError, "boom"));
2137        assert!(nd.is_not_delivered());
2138        let downgraded = deny_retry_after_partial(nd);
2139        assert!(!downgraded.is_not_delivered());
2140        assert!(
2141            downgraded.into_error().in_doubt(),
2142            "downgraded failure must be flagged in-doubt"
2143        );
2144
2145        // The reverse must never happen: an already in-doubt failure stays in
2146        // doubt — upgrading it back to retryable could cause data loss.
2147        let du = FlushFailure::DeliveryUnknown(Error::new(ErrorCode::SocketError, "boom"));
2148        let still = deny_retry_after_partial(du);
2149        assert!(!still.is_not_delivered());
2150        assert!(still.into_error().in_doubt());
2151    }
2152}