Skip to main content

powdb_server/
handler.rs

1use crate::metrics::{Metrics, QueryOutcome, SyncOperation, SyncOutcome, SyncRepairLabel};
2use crate::protocol::{Message, WireParam, WireRetainedUnit, WireSyncRepairAction, WireSyncStatus};
3use powdb_auth::{Permission, Role, UserStore};
4use powdb_query::executor::{is_read_only_statement, Engine, WalDurabilityTicket};
5use powdb_query::parser;
6use powdb_query::result::{QueryError, QueryResult};
7use powdb_query::sql;
8use powdb_storage::types::Value;
9use powdb_sync::{
10    acknowledge_replica_apply, read_identity, read_units_through, replica_sync_status,
11    retained_segments_dir, validate_retained_tail_available, validate_v1_retained_units_applyable,
12    ReplicaSyncStatus, RetainedUnit, SyncRepairAction, RETAINED_SEGMENT_FORMAT_VERSION,
13};
14use std::collections::HashMap;
15use std::net::IpAddr;
16use std::path::{Path, PathBuf};
17use std::sync::{Arc, Mutex, RwLock};
18use std::time::{Duration, Instant};
19use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter};
20use tokio::sync::{watch, OwnedSemaphorePermit, Semaphore};
21use tracing::{debug, error, info, warn};
22use zeroize::Zeroizing;
23
24/// Tracks per-IP authentication failure counts for rate limiting.
25pub type AuthRateLimiter = Arc<Mutex<HashMap<IpAddr, (u32, Instant)>>>;
26
27/// Gate that serializes wire-protocol statements while an explicit
28/// transaction is open on any connection. The connection that runs `begin`
29/// keeps an owned permit until `commit`, `rollback`, disconnect, or timeout,
30/// preventing other connections from observing or joining uncommitted state.
31pub type TxGate = Arc<Semaphore>;
32
33/// Create a transaction gate for a shared engine.
34pub fn new_tx_gate() -> TxGate {
35    Arc::new(Semaphore::new(1))
36}
37
38/// Maximum query text length accepted from the wire (1 MB).
39const MAX_QUERY_LENGTH: usize = 1024 * 1024;
40
41/// Server-side cap for private sync pull batches.
42const MAX_SYNC_PULL_UNITS: u32 = 4096;
43
44/// Server-side cap for retained-unit payload bytes in a private sync pull.
45const MAX_SYNC_PULL_BYTES: u64 = 16 * 1024 * 1024;
46
47/// Maximum encoded response payload size (64 MB). The wire format is still a
48/// single frame today, so oversized result sets must fail cleanly instead of
49/// building an unbounded `Vec<Vec<String>>` and frame in memory.
50#[cfg(not(test))]
51const MAX_RESPONSE_PAYLOAD_SIZE: usize = 64 * 1024 * 1024;
52#[cfg(test)]
53const MAX_RESPONSE_PAYLOAD_SIZE: usize = 1024;
54
55/// Timeout for writing a response to the client. Prevents slow-drain
56/// clients from blocking the handler indefinitely.
57const WRITE_TIMEOUT: Duration = Duration::from_secs(30);
58
59/// Maximum number of auth failures per IP within the rate-limit window.
60const MAX_AUTH_FAILURES: u32 = 5;
61
62/// Window during which auth failures are counted (60 seconds).
63const AUTH_FAILURE_WINDOW: Duration = Duration::from_secs(60);
64
65/// Create a new shared rate limiter.
66pub fn new_rate_limiter() -> AuthRateLimiter {
67    Arc::new(Mutex::new(HashMap::new()))
68}
69
70/// Check whether an IP is rate-limited and record a failure if requested.
71/// Returns `true` if the IP should be rejected.
72fn is_rate_limited(limiter: &AuthRateLimiter, ip: IpAddr) -> bool {
73    let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
74    // Clean up stale entries while we have the lock.
75    let now = Instant::now();
76    map.retain(|_, (_, ts)| now.duration_since(*ts) < AUTH_FAILURE_WINDOW);
77
78    if let Some((count, _)) = map.get(&ip) {
79        *count >= MAX_AUTH_FAILURES
80    } else {
81        false
82    }
83}
84
85/// Record an auth failure for the given IP.
86fn record_auth_failure(limiter: &AuthRateLimiter, ip: IpAddr) {
87    let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
88    let now = Instant::now();
89    let entry = map.entry(ip).or_insert((0, now));
90    // Reset counter if the window has elapsed.
91    if now.duration_since(entry.1) >= AUTH_FAILURE_WINDOW {
92        *entry = (1, now);
93    } else {
94        entry.0 += 1;
95    }
96}
97
98/// Clear the failure counter on successful auth.
99fn clear_auth_failures(limiter: &AuthRateLimiter, ip: IpAddr) {
100    let mut map = limiter.lock().unwrap_or_else(|e| e.into_inner());
101    map.remove(&ip);
102}
103
104/// Constant-time password comparison. Hashes both inputs to fixed-size
105/// SHA-256 digests so neither length nor content leaks through timing.
106fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
107    use sha2::{Digest, Sha256};
108    let ha = Sha256::digest(a);
109    let hb = Sha256::digest(b);
110    let mut diff = 0u8;
111    for (x, y) in ha.iter().zip(hb.iter()) {
112        diff |= x ^ y;
113    }
114    diff == 0
115}
116
117/// An authenticated connection's identity. Bound at connect time and consulted
118/// on every query by `dispatch_query` to enforce the user's role: a
119/// `readonly` principal may only execute read statements.
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct Principal {
122    pub name: String,
123    pub role: String,
124}
125
126/// Whether a parsed statement is data-definition (schema) work: creating,
127/// altering, or dropping a type or view. `explain <ddl>` is classified by its
128/// inner statement so `explain drop User` needs the same permission as
129/// `drop User`. Mutations that change *rows* (insert/update/delete/upsert/
130/// refresh) and transaction control are NOT DDL — they fall under `Write`.
131fn is_ddl_statement(stmt: &powdb_query::ast::Statement) -> bool {
132    use powdb_query::ast::Statement;
133    let inner = match stmt {
134        Statement::Explain(inner) => inner.as_ref(),
135        other => other,
136    };
137    matches!(
138        inner,
139        Statement::CreateType(_)
140            | Statement::AlterTable(_)
141            | Statement::DropTable(_)
142            | Statement::CreateView(_)
143            | Statement::DropView(_)
144    )
145}
146
147/// The capability a parsed statement requires under the RBAC lattice
148/// (`crates/auth/src/role.rs`). Reads need [`Permission::Read`]; schema
149/// definition needs [`Permission::Ddl`]; every other mutation needs
150/// [`Permission::Write`]. [`Permission::Admin`] is reserved for user/role
151/// management, which is CLI-only today and never reaches this wire path.
152fn required_permission(stmt: &powdb_query::ast::Statement) -> Permission {
153    if is_read_only_statement(stmt) {
154        Permission::Read
155    } else if is_ddl_statement(stmt) {
156        Permission::Ddl
157    } else {
158        Permission::Write
159    }
160}
161
162/// Enforce the principal's role against a parsed statement using the full
163/// permission lattice. Reads are always permitted (any authenticated role can
164/// read — unknown role names still read but fail closed on any mutation).
165/// Mutations require the specific capability the statement maps to: row
166/// mutations need `Write`, schema changes need `Ddl`. Unknown role names
167/// resolve to no builtin and therefore grant nothing beyond reads.
168///
169/// Classification uses the parsed AST via
170/// [`powdb_query::executor::is_read_only_statement`] — the exact same
171/// classifier the RwLock read/write split relies on — so the permission
172/// boundary and the concurrency boundary can never disagree.
173fn check_statement_permitted(
174    principal: Option<&Principal>,
175    stmt: &powdb_query::ast::Statement,
176) -> Result<(), QueryError> {
177    let Some(p) = principal else {
178        // No per-user identity (shared-password or open mode): full access,
179        // byte-identical to the pre-RBAC behavior.
180        return Ok(());
181    };
182    // Reads are permitted for every authenticated principal (preserves the
183    // pre-lattice contract that any connected role may run read-only queries).
184    if is_read_only_statement(stmt) {
185        return Ok(());
186    }
187    let needed = required_permission(stmt);
188    if Role::builtin(&p.role).is_some_and(|r| r.allows(needed)) {
189        return Ok(());
190    }
191    let kind = if needed == Permission::Ddl {
192        "schema-definition"
193    } else {
194        "write"
195    };
196    Err(QueryError::Execution(format!(
197        "permission denied: role '{}' cannot execute {kind} statements",
198        p.role
199    )))
200}
201
202/// Result of the connect-time authentication decision.
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub enum AuthOutcome {
205    /// Authenticated. `principal` is `Some` when a named user authenticated via
206    /// the UserStore, and `None` for the legacy shared-password / open paths
207    /// where there is no per-user identity.
208    Authenticated { principal: Option<Principal> },
209    /// Rejected. The caller sends a generic "authentication failed" error and
210    /// records a rate-limit failure — it must not reveal which check failed.
211    Rejected,
212}
213
214/// Pure, exhaustively-testable authentication decision for a CONNECT handshake.
215///
216/// Policy:
217/// - If `users` has at least one user, multi-user auth is in force: a
218///   `username` is required and `users.authenticate(username, password)` must
219///   succeed. Unknown user, wrong password, or a missing username all reject
220///   with an indistinguishable `Rejected` (no user-vs-password leak).
221/// - If `users` is empty, fall back verbatim to the legacy behavior: when
222///   `expected_password` is `Some`, the candidate must match it (constant time);
223///   when `None`, no auth is required (open). The `username` is ignored here so
224///   that a new client talking to a shared-password server still connects.
225pub fn authenticate_connect(
226    users: &UserStore,
227    expected_password: Option<&str>,
228    username: Option<&str>,
229    password: Option<&str>,
230) -> AuthOutcome {
231    if !users.is_empty() {
232        // Multi-user mode: a username is mandatory.
233        let Some(name) = username else {
234            return AuthOutcome::Rejected;
235        };
236        let Some(candidate) = password else {
237            return AuthOutcome::Rejected;
238        };
239        match users.authenticate(name, candidate) {
240            Some(user) => AuthOutcome::Authenticated {
241                principal: Some(Principal {
242                    name: user.name.clone(),
243                    role: user.role.clone(),
244                }),
245            },
246            None => AuthOutcome::Rejected,
247        }
248    } else {
249        // Legacy shared-password fallback (byte-identical to prior behavior).
250        match expected_password {
251            Some(expected) => {
252                if password.is_some_and(|p| constant_time_eq(p.as_bytes(), expected.as_bytes())) {
253                    AuthOutcome::Authenticated { principal: None }
254                } else {
255                    AuthOutcome::Rejected
256                }
257            }
258            None => AuthOutcome::Authenticated { principal: None },
259        }
260    }
261}
262
263/// The sentinel database name clients send when the user selected none. Both
264/// the CLI and the TS client default to this, so it means "no specific
265/// database" and is always accepted — even when the server is pinned to a name.
266const DEFAULT_DB_NAME: &str = "default";
267
268/// Decide whether a CONNECT's requested `db_name` is served by this process.
269///
270/// One server process serves exactly one global database. When it is pinned to
271/// a name (`configured = Some`), a request that *explicitly* names a different
272/// database is rejected so a client can never silently read/write the wrong
273/// store. An empty name or the client default sentinel (`"default"`) means "no
274/// specific database selected" and is always accepted. When unpinned (`None`)
275/// every name is accepted (0.9.x back-compat); the caller warns on a non-default
276/// name so the silent-mismatch footgun is at least visible in the logs.
277fn check_db_name(configured: Option<&str>, requested: &str) -> Result<(), String> {
278    if requested.is_empty() || requested == DEFAULT_DB_NAME {
279        return Ok(());
280    }
281    match configured {
282        None => Ok(()),
283        Some(name) if requested == name => Ok(()),
284        Some(name) => Err(format!(
285            "unknown database '{requested}'; this server serves '{name}'"
286        )),
287    }
288}
289
290/// Error messages that are safe to forward to the client verbatim.
291const SAFE_ERROR_PREFIXES: &[&str] = &[
292    "table not found",
293    // The executor's actual phrasing is `table 'X' not found`, which the
294    // bare prefix above never matches — keep both so the real message
295    // reaches clients.
296    "table '",
297    "type '",
298    "column not found",
299    // Lexer diagnostics (`at position N: unterminated quoted identifier`)
300    // are derived purely from the client's own query text.
301    "at position",
302    "parse error",
303    "type mismatch",
304    "unknown table",
305    "unknown column",
306    "unknown function",
307    "syntax error",
308    "expected",
309    "unexpected",
310    "missing",
311    "duplicate",
312    "invalid",
313    "cannot",
314    "no such",
315    "already exists",
316    "permission denied",
317    "row too large",
318    "unique constraint violation",
319    // Resource-limit errors carry actionable guidance (e.g. "add a LIMIT
320    // clause") and leak no internal state, so surface them verbatim instead
321    // of masking them to the generic message. See QueryError::{SortLimit,
322    // JoinLimit,MemoryLimit}Exceeded in crates/query/src/result.rs.
323    "sort input exceeds",
324    "join result exceeds",
325    "query exceeded memory budget",
326    "result too large",
327    // A failed covering fsync means the statement executed in memory but was
328    // never made durable — the client MUST be able to distinguish this from
329    // an ordinary failed query (the statement may still be visible until the
330    // server restarts). The io::Error detail leaks no internal state.
331    "wal durability sync failed",
332];
333
334/// Sanitize an error message before sending it to the client.
335/// Known safe errors are passed through; everything else is replaced
336/// with a generic message to avoid leaking internal details.
337fn sanitize_error(e: &str) -> String {
338    let lower = e.to_lowercase();
339    for prefix in SAFE_ERROR_PREFIXES {
340        if lower.starts_with(prefix) {
341            return e.to_string();
342        }
343    }
344    "query execution error".into()
345}
346
347/// Write a message to the client with a timeout. Returns false if the
348/// write failed or timed out (caller should close the connection).
349async fn write_msg<W: AsyncWrite + Unpin>(writer: &mut BufWriter<W>, msg: &Message) -> bool {
350    let write_fut = async {
351        if msg.write_to(writer).await.is_err() {
352            return false;
353        }
354        writer.flush().await.is_ok()
355    };
356    tokio::time::timeout(WRITE_TIMEOUT, write_fut)
357        .await
358        .unwrap_or_default()
359}
360
361/// Options for a single connection, bundled to keep `handle_connection`'s
362/// argument list short.
363pub struct ConnOpts<'a> {
364    pub engine: Arc<RwLock<Engine>>,
365    pub tx_gate: TxGate,
366    /// Expected client password. Wrapped in `Zeroizing` so the secret is wiped
367    /// from memory on drop (defends against leaking via a core dump).
368    pub expected_password: Option<Zeroizing<String>>,
369    /// Multi-user store loaded from the data dir at startup. When it has users,
370    /// the handshake authenticates `(username, password)` against it; when empty
371    /// the server falls back to `expected_password`. Shared across connections.
372    pub users: Arc<UserStore>,
373    pub shutdown_rx: &'a mut watch::Receiver<bool>,
374    pub idle_timeout: Duration,
375    pub query_timeout: Duration,
376    pub rate_limiter: Option<&'a AuthRateLimiter>,
377    pub peer_addr: Option<std::net::SocketAddr>,
378    /// Shared server metrics. Always present; tests pass `Arc::new(Metrics::new())`.
379    pub metrics: Arc<Metrics>,
380    /// How long an explicit `begin` waits to acquire the transaction gate while
381    /// another connection holds an open explicit transaction, before giving up
382    /// with a clear timeout error instead of queueing indefinitely.
383    pub tx_wait_timeout: Duration,
384    /// When `Some`, the single database name this server serves. A CONNECT that
385    /// explicitly names a *different* database is rejected at connect time.
386    /// `None` accepts any name (0.9.x behavior) and only warns.
387    pub db_name: Option<String>,
388}
389
390/// Execute a query against the engine under the RwLock. Read-only
391/// statements acquire `.read()` so concurrent SELECTs can scan in
392/// parallel; mutations acquire `.write()`.
393///
394/// When `principal` is `Some`, the user's role is enforced first: a role
395/// without the `Write` permission (i.e. `readonly`) gets a clean
396/// "permission denied" error for any non-read statement, before any lock
397/// is taken or any engine state is touched.
398/// A statement's execution result plus its (not-yet-waited) WAL durability
399/// obligation. The ticket is settled by [`finalize_durability`] AFTER the
400/// TxGate permit is dropped, so overlapping committers on other connections
401/// can share a single fsync (group commit).
402type DispatchOutcome = (Result<QueryResult, QueryError>, Option<WalDurabilityTicket>);
403
404/// Execute a mutating statement under the engine write lock with WAL group
405/// commit: the commit's fsync obligation is registered (not performed) while
406/// the lock is held, then the lock is dropped and the un-waited ticket is
407/// returned. The caller must settle the ticket (via [`finalize_durability`])
408/// before acknowledging the statement — and must do so with the TxGate
409/// permit already released, or committers can never overlap.
410fn execute_write_deferred(
411    engine: &Arc<RwLock<Engine>>,
412    f: impl FnOnce(&mut Engine) -> Result<QueryResult, QueryError>,
413) -> DispatchOutcome {
414    let mut eng = match engine.write() {
415        Ok(eng) => eng,
416        Err(e) => {
417            return (
418                Err(QueryError::Execution(format!("lock poisoned: {e}"))),
419                None,
420            )
421        }
422    };
423    eng.run_with_deferred_durability(f)
424}
425
426fn dispatch_query(
427    engine: &Arc<RwLock<Engine>>,
428    query: &str,
429    principal: Option<&Principal>,
430) -> DispatchOutcome {
431    let stmt_result = parser::parse(query).map_err(|e| e.to_string());
432
433    // Role enforcement happens on the parsed AST. Statements that fail to
434    // parse fall through — the engine returns the parse error itself and
435    // can never execute anything for them.
436    if let Ok(stmt) = &stmt_result {
437        if let Err(e) = check_statement_permitted(principal, stmt) {
438            return (Err(e), None);
439        }
440    }
441
442    let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
443    if can_try_read {
444        let res = {
445            let eng = match engine.read() {
446                Ok(eng) => eng,
447                Err(e) => {
448                    return (
449                        Err(QueryError::Execution(format!("lock poisoned: {e}"))),
450                        None,
451                    )
452                }
453            };
454            eng.execute_powql_readonly(query)
455        };
456        match res {
457            Ok(r) => return (Ok(r), None),
458            Err(QueryError::ReadonlyNeedsWrite) => {
459                // Escalate: fall through to the write path below.
460            }
461            Err(e) => return (Err(e), None),
462        }
463    }
464
465    if matches!(
466        parsed_transaction_control(&stmt_result),
467        Some(TransactionControl::Rollback)
468    ) {
469        let mut eng = match engine.write() {
470            Ok(eng) => eng,
471            Err(e) => {
472                return (
473                    Err(QueryError::Execution(format!("lock poisoned: {e}"))),
474                    None,
475                )
476            }
477        };
478        return (execute_rollback_preserving_sync_if_needed(&mut eng), None);
479    }
480    execute_write_deferred(engine, |eng| eng.execute_powql(query))
481}
482
483fn dispatch_sql_query(
484    engine: &Arc<RwLock<Engine>>,
485    query: &str,
486    principal: Option<&Principal>,
487) -> DispatchOutcome {
488    let stmt_result = sql::parse_sql(query).map_err(|e| e.to_string());
489
490    if let Ok(stmt) = &stmt_result {
491        if let Err(e) = check_statement_permitted(principal, stmt) {
492            return (Err(e), None);
493        }
494    }
495
496    let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
497    if can_try_read {
498        let res = {
499            let eng = match engine.read() {
500                Ok(eng) => eng,
501                Err(e) => {
502                    return (
503                        Err(QueryError::Execution(format!("lock poisoned: {e}"))),
504                        None,
505                    )
506                }
507            };
508            eng.execute_sql_readonly(query)
509        };
510        match res {
511            Ok(r) => return (Ok(r), None),
512            Err(QueryError::ReadonlyNeedsWrite) => {}
513            Err(e) => return (Err(e), None),
514        }
515    }
516
517    if matches!(
518        parsed_transaction_control(&stmt_result),
519        Some(TransactionControl::Rollback)
520    ) {
521        let mut eng = match engine.write() {
522            Ok(eng) => eng,
523            Err(e) => {
524                return (
525                    Err(QueryError::Execution(format!("lock poisoned: {e}"))),
526                    None,
527                )
528            }
529        };
530        return (execute_rollback_preserving_sync_if_needed(&mut eng), None);
531    }
532    execute_write_deferred(engine, |eng| eng.execute_sql(query))
533}
534
535/// Convert a wire parameter into the query-crate [`ParamValue`] used for
536/// token-level binding.
537fn wire_param_to_value(p: &WireParam) -> powdb_query::ast::ParamValue {
538    use powdb_query::ast::ParamValue;
539    match p {
540        WireParam::Null => ParamValue::Null,
541        WireParam::Int(v) => ParamValue::Int(*v),
542        WireParam::Float(v) => ParamValue::Float(*v),
543        WireParam::Bool(v) => ParamValue::Bool(*v),
544        WireParam::Str(s) => ParamValue::Str(s.clone()),
545    }
546}
547
548/// Parameterized counterpart of [`dispatch_query`]. Routes through the exact
549/// same role-enforcement and read/write escalation logic, but binds the
550/// `$N` placeholders at the token level via the query crate's
551/// `parse_with_params` path. A string parameter can never change the query's
552/// shape — it is substituted as a literal token, not interpolated text.
553#[derive(Debug, Clone, Copy, PartialEq, Eq)]
554enum TransactionControl {
555    Begin,
556    Commit,
557    Rollback,
558}
559
560fn transaction_control(stmt: &powdb_query::ast::Statement) -> Option<TransactionControl> {
561    use powdb_query::ast::Statement;
562    match stmt {
563        Statement::Begin => Some(TransactionControl::Begin),
564        Statement::Commit => Some(TransactionControl::Commit),
565        Statement::Rollback => Some(TransactionControl::Rollback),
566        _ => None,
567    }
568}
569
570fn classify_query_transaction_control(query: &str) -> Option<TransactionControl> {
571    parser::parse(query)
572        .ok()
573        .and_then(|stmt| transaction_control(&stmt))
574}
575
576fn classify_sql_transaction_control(query: &str) -> Option<TransactionControl> {
577    sql::parse_sql(query)
578        .ok()
579        .and_then(|stmt| transaction_control(&stmt))
580}
581
582fn classify_params_transaction_control(
583    query: &str,
584    params: &[WireParam],
585) -> Option<TransactionControl> {
586    let bound: Vec<powdb_query::ast::ParamValue> = params.iter().map(wire_param_to_value).collect();
587    parser::parse_with_params(query, &bound)
588        .ok()
589        .and_then(|stmt| transaction_control(&stmt))
590}
591
592fn parsed_transaction_control(
593    stmt_result: &Result<powdb_query::ast::Statement, String>,
594) -> Option<TransactionControl> {
595    stmt_result.as_ref().ok().and_then(transaction_control)
596}
597
598fn execute_rollback_preserving_sync_if_needed(
599    engine: &mut Engine,
600) -> Result<QueryResult, QueryError> {
601    engine.rollback_transaction_preserving_wal_archive()
602}
603
604fn dispatch_query_with_params(
605    engine: &Arc<RwLock<Engine>>,
606    query: &str,
607    params: &[WireParam],
608    principal: Option<&Principal>,
609) -> DispatchOutcome {
610    let bound: Vec<powdb_query::ast::ParamValue> = params.iter().map(wire_param_to_value).collect();
611
612    // Parse once (with params bound) so role enforcement and read/write
613    // classification see exactly the statement that will execute.
614    let stmt_result = parser::parse_with_params(query, &bound).map_err(|e| e.to_string());
615
616    if let Ok(stmt) = &stmt_result {
617        if let Err(e) = check_statement_permitted(principal, stmt) {
618            return (Err(e), None);
619        }
620    }
621
622    let can_try_read = matches!(&stmt_result, Ok(s) if is_read_only_statement(s));
623    if can_try_read {
624        let res = {
625            let eng = match engine.read() {
626                Ok(eng) => eng,
627                Err(e) => {
628                    return (
629                        Err(QueryError::Execution(format!("lock poisoned: {e}"))),
630                        None,
631                    )
632                }
633            };
634            eng.execute_powql_readonly_with_params(query, &bound)
635        };
636        match res {
637            Ok(r) => return (Ok(r), None),
638            Err(QueryError::ReadonlyNeedsWrite) => {
639                // Escalate to the write path below.
640            }
641            Err(e) => return (Err(e), None),
642        }
643    }
644
645    if matches!(
646        parsed_transaction_control(&stmt_result),
647        Some(TransactionControl::Rollback)
648    ) {
649        let mut eng = match engine.write() {
650            Ok(eng) => eng,
651            Err(e) => {
652                return (
653                    Err(QueryError::Execution(format!("lock poisoned: {e}"))),
654                    None,
655                )
656            }
657        };
658        return (execute_rollback_preserving_sync_if_needed(&mut eng), None);
659    }
660    execute_write_deferred(engine, |eng| eng.execute_powql_with_params(query, &bound))
661}
662
663#[derive(Debug, Clone)]
664struct SyncPullRequest {
665    replica_id: String,
666    since_lsn: u64,
667    max_units: u32,
668    max_bytes: u64,
669    database_id: [u8; 16],
670    primary_generation: u64,
671    wal_format_version: u16,
672    catalog_version: u16,
673    segment_format_version: u16,
674}
675
676#[derive(Debug, Clone, Copy, PartialEq, Eq)]
677enum SyncErrorClass {
678    AuthRequired,
679    PermissionDenied,
680    InvalidReplicaId,
681    ActiveTransaction,
682    QueryExecution,
683    InvalidMaxUnits,
684    InvalidMaxBytes,
685    SyncContext,
686    StatusRead,
687    CursorLsnMismatch,
688    IdentityRead,
689    IdentityOrFormatMismatch,
690    RetainedRead,
691    RetainedUnitEncoding,
692    RetainedChunkNotApplyable,
693    LsnAheadOfRemote,
694    AckValidation,
695    AckUpdate,
696    Internal,
697}
698
699impl SyncErrorClass {
700    const fn as_label(self) -> &'static str {
701        match self {
702            Self::AuthRequired => "auth_required",
703            Self::PermissionDenied => "permission_denied",
704            Self::InvalidReplicaId => "invalid_replica_id",
705            Self::ActiveTransaction => "active_transaction",
706            Self::QueryExecution => "query_execution",
707            Self::InvalidMaxUnits => "invalid_max_units",
708            Self::InvalidMaxBytes => "invalid_max_bytes",
709            Self::SyncContext => "sync_context",
710            Self::StatusRead => "status_read",
711            Self::CursorLsnMismatch => "cursor_lsn_mismatch",
712            Self::IdentityRead => "identity_read",
713            Self::IdentityOrFormatMismatch => "identity_or_format_mismatch",
714            Self::RetainedRead => "retained_read",
715            Self::RetainedUnitEncoding => "retained_unit_encoding",
716            Self::RetainedChunkNotApplyable => "retained_chunk_not_applyable",
717            Self::LsnAheadOfRemote => "lsn_ahead_of_remote",
718            Self::AckValidation => "ack_validation",
719            Self::AckUpdate => "ack_update",
720            Self::Internal => "internal",
721        }
722    }
723}
724
725#[derive(Debug, Clone)]
726struct SyncDecision {
727    message: Message,
728    error_class: Option<SyncErrorClass>,
729}
730
731impl SyncDecision {
732    fn ok(message: Message) -> Self {
733        Self {
734            message,
735            error_class: None,
736        }
737    }
738
739    fn error(class: SyncErrorClass, message: impl Into<String>) -> Self {
740        Self {
741            message: Message::Error {
742                message: message.into(),
743            },
744            error_class: Some(class),
745        }
746    }
747}
748
749fn check_sync_protocol_permitted(
750    credential_authenticated: bool,
751    principal: Option<&Principal>,
752) -> Result<(), (SyncErrorClass, String)> {
753    if !credential_authenticated {
754        return Err((
755            SyncErrorClass::AuthRequired,
756            "sync protocol requires authentication".to_string(),
757        ));
758    }
759    if let Some(principal) = principal {
760        let allowed =
761            Role::builtin(&principal.role).is_some_and(|role| role.allows(Permission::Write));
762        if !allowed {
763            return Err((
764                SyncErrorClass::PermissionDenied,
765                format!(
766                    "permission denied: role '{}' cannot use sync protocol",
767                    principal.role
768                ),
769            ));
770        }
771    }
772    Ok(())
773}
774
775fn validate_wire_replica_id(replica_id: &str) -> Result<(), String> {
776    if replica_id.is_empty() {
777        return Err("replica id must be non-empty".to_string());
778    }
779    if replica_id.len() > 128 {
780        return Err("replica id must be at most 128 bytes".to_string());
781    }
782    if !replica_id
783        .bytes()
784        .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b':'))
785    {
786        return Err("replica id contains unsupported characters".to_string());
787    }
788    Ok(())
789}
790
791fn sync_context(engine: &Arc<RwLock<Engine>>) -> Result<(PathBuf, u64), String> {
792    let engine = engine
793        .read()
794        .map_err(|e| format!("lock poisoned while reading sync context: {e}"))?;
795    let catalog = engine.catalog();
796    Ok((catalog.data_dir().to_path_buf(), catalog.max_lsn()))
797}
798
799fn wire_repair_action(action: SyncRepairAction) -> WireSyncRepairAction {
800    match action {
801        SyncRepairAction::None => WireSyncRepairAction::None,
802        SyncRepairAction::Pull => WireSyncRepairAction::Pull,
803        SyncRepairAction::AwaitArchive => WireSyncRepairAction::AwaitArchive,
804        SyncRepairAction::Rebootstrap => WireSyncRepairAction::Rebootstrap,
805    }
806}
807
808fn wire_sync_status(status: ReplicaSyncStatus) -> WireSyncStatus {
809    WireSyncStatus {
810        replica_id: status.replica_id,
811        active: status.active,
812        last_applied_lsn: status.last_applied_lsn,
813        remote_lsn: status.remote_lsn,
814        servable_lsn: status.servable_lsn,
815        unarchived_lsn: status.unarchived_lsn,
816        lag_lsn: status.lag_lsn,
817        lag_bytes: status.lag_bytes,
818        lag_ms: status.lag_ms,
819        stale: status.stale,
820        repair_action: wire_repair_action(status.repair_action),
821        last_sync_error: status.last_sync_error,
822    }
823}
824
825fn wire_retained_unit(unit: RetainedUnit) -> WireRetainedUnit {
826    WireRetainedUnit {
827        tx_id: unit.tx_id,
828        record_type: unit.record_type,
829        lsn: unit.lsn,
830        data: unit.data,
831    }
832}
833
834fn sync_operation_outcome(message: &Message) -> SyncOutcome {
835    match message {
836        Message::SyncStatusResult { .. }
837        | Message::SyncPullResult { .. }
838        | Message::SyncAckResult { .. } => SyncOutcome::Ok,
839        _ => SyncOutcome::Error,
840    }
841}
842
843fn sync_operation_label(operation: SyncOperation) -> &'static str {
844    match operation {
845        SyncOperation::Status => "status",
846        SyncOperation::Pull => "pull",
847        SyncOperation::Ack => "ack",
848    }
849}
850
851fn sync_pull_payload_bytes(units: &[WireRetainedUnit]) -> u64 {
852    units.iter().fold(0, |total, unit| {
853        total.saturating_add(unit.encoded_len().unwrap_or(0))
854    })
855}
856
857fn sync_repair_label(action: WireSyncRepairAction) -> SyncRepairLabel {
858    match action {
859        WireSyncRepairAction::None => SyncRepairLabel::None,
860        WireSyncRepairAction::Pull => SyncRepairLabel::Pull,
861        WireSyncRepairAction::AwaitArchive => SyncRepairLabel::AwaitArchive,
862        WireSyncRepairAction::Rebootstrap => SyncRepairLabel::Rebootstrap,
863    }
864}
865
866fn wire_repair_action_label(action: WireSyncRepairAction) -> &'static str {
867    match action {
868        WireSyncRepairAction::None => "none",
869        WireSyncRepairAction::Pull => "pull",
870        WireSyncRepairAction::AwaitArchive => "await_archive",
871        WireSyncRepairAction::Rebootstrap => "rebootstrap",
872    }
873}
874
875const FNV1A64_OFFSET: u64 = 0xcbf29ce484222325;
876const FNV1A64_PRIME: u64 = 0x100000001b3;
877const INVALID_REPLICA_FINGERPRINT: &str = "invalid";
878
879fn replica_fingerprint(replica_id: &str) -> String {
880    let mut hash = FNV1A64_OFFSET;
881    for byte in replica_id.as_bytes() {
882        hash ^= u64::from(*byte);
883        hash = hash.wrapping_mul(FNV1A64_PRIME);
884    }
885    format!("{hash:016x}")
886}
887
888fn log_replica_fingerprint(replica_id: &str) -> String {
889    if validate_wire_replica_id(replica_id).is_ok() {
890        replica_fingerprint(replica_id)
891    } else {
892        INVALID_REPLICA_FINGERPRINT.to_string()
893    }
894}
895
896#[derive(Debug, Clone)]
897struct SyncLogContext {
898    replica_fingerprint: String,
899    since_lsn: Option<u64>,
900    applied_lsn: Option<u64>,
901    observed_remote_lsn: Option<u64>,
902    max_units: Option<u32>,
903    max_bytes: Option<u64>,
904}
905
906impl SyncLogContext {
907    fn status(replica_id: &str) -> Self {
908        Self::base(replica_id)
909    }
910
911    fn pull(request: &SyncPullRequest) -> Self {
912        Self {
913            replica_fingerprint: log_replica_fingerprint(&request.replica_id),
914            since_lsn: Some(request.since_lsn),
915            applied_lsn: None,
916            observed_remote_lsn: None,
917            max_units: Some(request.max_units),
918            max_bytes: Some(request.max_bytes),
919        }
920    }
921
922    fn ack(replica_id: &str, applied_lsn: u64, observed_remote_lsn: u64) -> Self {
923        Self {
924            replica_fingerprint: log_replica_fingerprint(replica_id),
925            since_lsn: None,
926            applied_lsn: Some(applied_lsn),
927            observed_remote_lsn: Some(observed_remote_lsn),
928            max_units: None,
929            max_bytes: None,
930        }
931    }
932
933    fn base(replica_id: &str) -> Self {
934        Self {
935            replica_fingerprint: log_replica_fingerprint(replica_id),
936            since_lsn: None,
937            applied_lsn: None,
938            observed_remote_lsn: None,
939            max_units: None,
940            max_bytes: None,
941        }
942    }
943}
944
945struct SyncExecutionContext<'a> {
946    tx_gate: TxGate,
947    connection_has_transaction: bool,
948    operation: SyncOperation,
949    log_context: SyncLogContext,
950    metrics: &'a Arc<Metrics>,
951    query_timeout: Duration,
952}
953
954fn log_sync_decision(
955    operation: SyncOperation,
956    context: &SyncLogContext,
957    elapsed: Duration,
958    decision: &SyncDecision,
959) {
960    let operation = sync_operation_label(operation);
961    let elapsed_ms = elapsed.as_secs_f64() * 1000.0;
962    match &decision.message {
963        Message::SyncStatusResult { status } => {
964            let repair_action = wire_repair_action_label(status.repair_action);
965            if status.stale || status.repair_action != WireSyncRepairAction::None {
966                info!(
967                    operation = operation,
968                    replica_fingerprint = %context.replica_fingerprint,
969                    remote_lsn = status.remote_lsn,
970                    last_applied_lsn = ?status.last_applied_lsn,
971                    servable_lsn = ?status.servable_lsn,
972                    unarchived_lsn = ?status.unarchived_lsn,
973                    lag_lsn = ?status.lag_lsn,
974                    lag_bytes = ?status.lag_bytes,
975                    lag_ms = ?status.lag_ms,
976                    stale = status.stale,
977                    repair_action,
978                    elapsed_ms,
979                    "sync decision"
980                );
981            } else {
982                debug!(
983                    operation = operation,
984                    replica_fingerprint = %context.replica_fingerprint,
985                    remote_lsn = status.remote_lsn,
986                    last_applied_lsn = ?status.last_applied_lsn,
987                    repair_action,
988                    elapsed_ms,
989                    "sync decision"
990                );
991            }
992        }
993        Message::SyncPullResult {
994            status,
995            units,
996            has_more,
997        } => {
998            let repair_action = wire_repair_action_label(status.repair_action);
999            let units_len = units.len();
1000            let payload_bytes = sync_pull_payload_bytes(units);
1001            if *has_more || status.stale || status.repair_action != WireSyncRepairAction::None {
1002                info!(
1003                    operation = operation,
1004                    replica_fingerprint = %context.replica_fingerprint,
1005                    since_lsn = ?context.since_lsn,
1006                    max_units = ?context.max_units,
1007                    max_bytes = ?context.max_bytes,
1008                    units = units_len,
1009                    payload_bytes,
1010                    has_more = *has_more,
1011                    remote_lsn = status.remote_lsn,
1012                    last_applied_lsn = ?status.last_applied_lsn,
1013                    servable_lsn = ?status.servable_lsn,
1014                    unarchived_lsn = ?status.unarchived_lsn,
1015                    lag_lsn = ?status.lag_lsn,
1016                    stale = status.stale,
1017                    repair_action,
1018                    elapsed_ms,
1019                    "sync decision"
1020                );
1021            } else {
1022                debug!(
1023                    operation = operation,
1024                    replica_fingerprint = %context.replica_fingerprint,
1025                    since_lsn = ?context.since_lsn,
1026                    units = units_len,
1027                    payload_bytes,
1028                    has_more = *has_more,
1029                    remote_lsn = status.remote_lsn,
1030                    repair_action,
1031                    elapsed_ms,
1032                    "sync decision"
1033                );
1034            }
1035        }
1036        Message::SyncAckResult {
1037            previous_applied_lsn,
1038            applied_lsn,
1039            remote_lsn,
1040            advanced,
1041            status,
1042        } => {
1043            let repair_action = wire_repair_action_label(status.repair_action);
1044            if *advanced || status.stale || status.repair_action != WireSyncRepairAction::None {
1045                info!(
1046                    operation = operation,
1047                    replica_fingerprint = %context.replica_fingerprint,
1048                    requested_applied_lsn = ?context.applied_lsn,
1049                    observed_remote_lsn = ?context.observed_remote_lsn,
1050                    previous_applied_lsn = *previous_applied_lsn,
1051                    applied_lsn = *applied_lsn,
1052                    remote_lsn = *remote_lsn,
1053                    advanced = *advanced,
1054                    stale = status.stale,
1055                    repair_action,
1056                    elapsed_ms,
1057                    "sync decision"
1058                );
1059            } else {
1060                debug!(
1061                    operation = operation,
1062                    replica_fingerprint = %context.replica_fingerprint,
1063                    requested_applied_lsn = ?context.applied_lsn,
1064                    previous_applied_lsn = *previous_applied_lsn,
1065                    applied_lsn = *applied_lsn,
1066                    remote_lsn = *remote_lsn,
1067                    advanced = *advanced,
1068                    repair_action,
1069                    elapsed_ms,
1070                    "sync decision"
1071                );
1072            }
1073        }
1074        Message::Error { .. } => {
1075            warn!(
1076                operation = operation,
1077                replica_fingerprint = %context.replica_fingerprint,
1078                since_lsn = ?context.since_lsn,
1079                applied_lsn = ?context.applied_lsn,
1080                observed_remote_lsn = ?context.observed_remote_lsn,
1081                max_units = ?context.max_units,
1082                max_bytes = ?context.max_bytes,
1083                error_class = decision
1084                    .error_class
1085                    .unwrap_or(SyncErrorClass::Internal)
1086                    .as_label(),
1087                elapsed_ms,
1088                "sync decision rejected"
1089            );
1090        }
1091        _ => {
1092            debug!(
1093                operation = operation,
1094                replica_fingerprint = %context.replica_fingerprint,
1095                elapsed_ms,
1096                "unexpected sync decision response"
1097            );
1098        }
1099    }
1100}
1101
1102fn trim_to_applyable_v1_prefix(
1103    raw_units: &mut Vec<RetainedUnit>,
1104    wire_units: &mut Vec<WireRetainedUnit>,
1105) -> Result<(), String> {
1106    let mut last_error = None;
1107    while !raw_units.is_empty() {
1108        match validate_v1_retained_units_applyable(raw_units) {
1109            Ok(()) => return Ok(()),
1110            Err(err) => {
1111                last_error = Some(err.to_string());
1112                raw_units.pop();
1113                wire_units.pop();
1114            }
1115        }
1116    }
1117    if let Some(error) = last_error {
1118        return Err(format!(
1119            "sync pull cannot serve an applyable V1 retained chunk with current limits: {error}"
1120        ));
1121    }
1122    Ok(())
1123}
1124
1125fn validate_sync_ack_applyable_boundary(
1126    data_dir: &Path,
1127    replica_id: &str,
1128    applied_lsn: u64,
1129    remote_lsn: u64,
1130) -> Result<(), String> {
1131    let status =
1132        replica_sync_status(data_dir, replica_id, remote_lsn).map_err(|err| err.to_string())?;
1133    let Some(previous_lsn) = status.last_applied_lsn else {
1134        return Ok(());
1135    };
1136    if applied_lsn <= previous_lsn {
1137        return Ok(());
1138    }
1139    let range_len = applied_lsn - previous_lsn;
1140    if range_len > u64::from(MAX_SYNC_PULL_UNITS) {
1141        return Err(format!(
1142            "sync ack range contains {range_len} units; acknowledge ranges no larger than {MAX_SYNC_PULL_UNITS}"
1143        ));
1144    }
1145    let max_units =
1146        usize::try_from(range_len).map_err(|_| "sync ack range is too large to validate")?;
1147    let identity = read_identity(data_dir).map_err(|err| err.to_string())?;
1148    let segment_dir = retained_segments_dir(data_dir);
1149    let units = read_units_through(
1150        &segment_dir,
1151        identity.segment_identity(),
1152        previous_lsn,
1153        applied_lsn,
1154        max_units,
1155    )
1156    .map_err(|err| err.to_string())?;
1157    if units.len() != max_units || units.last().map(|unit| unit.lsn) != Some(applied_lsn) {
1158        return Err(
1159            "sync ack does not cover a complete retained-unit range; rebootstrap required".into(),
1160        );
1161    }
1162    validate_v1_retained_units_applyable(&units).map_err(|err| err.to_string())
1163}
1164
1165#[cfg(test)]
1166fn dispatch_sync_status(
1167    engine: &Arc<RwLock<Engine>>,
1168    replica_id: String,
1169    credential_authenticated: bool,
1170    principal: Option<&Principal>,
1171) -> Message {
1172    dispatch_sync_status_decision(engine, replica_id, credential_authenticated, principal).message
1173}
1174
1175fn dispatch_sync_status_decision(
1176    engine: &Arc<RwLock<Engine>>,
1177    replica_id: String,
1178    credential_authenticated: bool,
1179    principal: Option<&Principal>,
1180) -> SyncDecision {
1181    if let Err((class, message)) =
1182        check_sync_protocol_permitted(credential_authenticated, principal)
1183    {
1184        return SyncDecision::error(class, message);
1185    }
1186    if let Err(message) = validate_wire_replica_id(&replica_id) {
1187        return SyncDecision::error(SyncErrorClass::InvalidReplicaId, message);
1188    }
1189    let (data_dir, remote_lsn) = match sync_context(engine) {
1190        Ok(context) => context,
1191        Err(message) => return SyncDecision::error(SyncErrorClass::SyncContext, message),
1192    };
1193    match replica_sync_status(&data_dir, &replica_id, remote_lsn) {
1194        Ok(status) => SyncDecision::ok(Message::SyncStatusResult {
1195            status: wire_sync_status(status),
1196        }),
1197        Err(err) => SyncDecision::error(SyncErrorClass::StatusRead, err.to_string()),
1198    }
1199}
1200
1201#[cfg(test)]
1202fn dispatch_sync_pull(
1203    engine: &Arc<RwLock<Engine>>,
1204    request: SyncPullRequest,
1205    credential_authenticated: bool,
1206    principal: Option<&Principal>,
1207) -> Message {
1208    dispatch_sync_pull_decision(engine, request, credential_authenticated, principal).message
1209}
1210
1211fn dispatch_sync_pull_decision(
1212    engine: &Arc<RwLock<Engine>>,
1213    request: SyncPullRequest,
1214    credential_authenticated: bool,
1215    principal: Option<&Principal>,
1216) -> SyncDecision {
1217    if let Err((class, message)) =
1218        check_sync_protocol_permitted(credential_authenticated, principal)
1219    {
1220        return SyncDecision::error(class, message);
1221    }
1222    if let Err(message) = validate_wire_replica_id(&request.replica_id) {
1223        return SyncDecision::error(SyncErrorClass::InvalidReplicaId, message);
1224    }
1225    if request.max_units == 0 || request.max_units > MAX_SYNC_PULL_UNITS {
1226        return SyncDecision::error(
1227            SyncErrorClass::InvalidMaxUnits,
1228            format!("sync pull maxUnits must be between 1 and {MAX_SYNC_PULL_UNITS}"),
1229        );
1230    }
1231    if request.max_bytes == 0 || request.max_bytes > MAX_SYNC_PULL_BYTES {
1232        return SyncDecision::error(
1233            SyncErrorClass::InvalidMaxBytes,
1234            format!("sync pull maxBytes must be between 1 and {MAX_SYNC_PULL_BYTES}"),
1235        );
1236    }
1237
1238    let (data_dir, remote_lsn) = match sync_context(engine) {
1239        Ok(context) => context,
1240        Err(message) => return SyncDecision::error(SyncErrorClass::SyncContext, message),
1241    };
1242    let status = match replica_sync_status(&data_dir, &request.replica_id, remote_lsn) {
1243        Ok(status) => status,
1244        Err(err) => {
1245            return SyncDecision::error(SyncErrorClass::StatusRead, err.to_string());
1246        }
1247    };
1248    let Some(cursor_lsn) = status.last_applied_lsn else {
1249        return SyncDecision::ok(Message::SyncPullResult {
1250            status: wire_sync_status(status),
1251            units: Vec::new(),
1252            has_more: false,
1253        });
1254    };
1255    if status.repair_action != SyncRepairAction::Pull {
1256        return SyncDecision::ok(Message::SyncPullResult {
1257            status: wire_sync_status(status),
1258            units: Vec::new(),
1259            has_more: false,
1260        });
1261    }
1262    if request.since_lsn != cursor_lsn {
1263        return SyncDecision::error(
1264            SyncErrorClass::CursorLsnMismatch,
1265            format!(
1266                "sync pull sinceLsn {} does not match primary cursor LSN {cursor_lsn}",
1267                request.since_lsn
1268            ),
1269        );
1270    }
1271
1272    let identity = match read_identity(&data_dir) {
1273        Ok(identity) => identity,
1274        Err(err) => {
1275            return SyncDecision::error(SyncErrorClass::IdentityRead, err.to_string());
1276        }
1277    };
1278    let expected = identity.segment_identity();
1279    if request.database_id != expected.database_id
1280        || request.primary_generation != expected.primary_generation
1281        || request.wal_format_version != expected.wal_format_version
1282        || request.catalog_version != expected.catalog_version
1283        || request.segment_format_version != RETAINED_SEGMENT_FORMAT_VERSION
1284    {
1285        return SyncDecision::error(
1286            SyncErrorClass::IdentityOrFormatMismatch,
1287            "sync pull identity or format version mismatch; rebootstrap required",
1288        );
1289    }
1290
1291    let effective_max_units = request.max_units.min(MAX_SYNC_PULL_UNITS) as usize;
1292    let requested_through_lsn = request
1293        .since_lsn
1294        .saturating_add(request.max_units as u64)
1295        .min(remote_lsn)
1296        .min(status.servable_lsn.unwrap_or(request.since_lsn));
1297    let segment_dir = retained_segments_dir(&data_dir);
1298    if requested_through_lsn > request.since_lsn {
1299        if let Err(err) = validate_retained_tail_available(
1300            &segment_dir,
1301            expected,
1302            request.since_lsn,
1303            requested_through_lsn,
1304        ) {
1305            let mut rebootstrap_status = status;
1306            rebootstrap_status.stale = true;
1307            rebootstrap_status.repair_action = SyncRepairAction::Rebootstrap;
1308            rebootstrap_status.last_sync_error = Some(format!(
1309                "retained history is unavailable; rebootstrap required: {err}"
1310            ));
1311            return SyncDecision::ok(Message::SyncPullResult {
1312                status: wire_sync_status(rebootstrap_status),
1313                units: Vec::new(),
1314                has_more: false,
1315            });
1316        }
1317    }
1318
1319    let raw_units = match read_units_through(
1320        &segment_dir,
1321        expected,
1322        request.since_lsn,
1323        requested_through_lsn,
1324        effective_max_units,
1325    ) {
1326        Ok(units) => units,
1327        Err(err) => {
1328            return SyncDecision::error(SyncErrorClass::RetainedRead, err.to_string());
1329        }
1330    };
1331
1332    let mut selected_raw = Vec::new();
1333    let mut selected = Vec::new();
1334    let mut selected_bytes = 0u64;
1335    for unit in raw_units {
1336        let wire_unit = wire_retained_unit(unit.clone());
1337        let unit_bytes = match wire_unit.encoded_len() {
1338            Ok(bytes) => bytes,
1339            Err(message) => {
1340                return SyncDecision::error(SyncErrorClass::RetainedUnitEncoding, message);
1341            }
1342        };
1343        if selected_bytes.saturating_add(unit_bytes) > request.max_bytes {
1344            if selected.is_empty() {
1345                return SyncDecision::error(
1346                    SyncErrorClass::InvalidMaxBytes,
1347                    "sync pull maxBytes is too small for the next retained unit",
1348                );
1349            }
1350            break;
1351        }
1352        selected_bytes += unit_bytes;
1353        selected_raw.push(unit);
1354        selected.push(wire_unit);
1355    }
1356    if let Err(message) = trim_to_applyable_v1_prefix(&mut selected_raw, &mut selected) {
1357        return SyncDecision::error(SyncErrorClass::RetainedChunkNotApplyable, message);
1358    }
1359
1360    let fetchable_through_lsn = status.servable_lsn.unwrap_or(remote_lsn).min(remote_lsn);
1361    let has_more = selected
1362        .last()
1363        .is_some_and(|unit| unit.lsn < fetchable_through_lsn);
1364    SyncDecision::ok(Message::SyncPullResult {
1365        status: wire_sync_status(status),
1366        units: selected,
1367        has_more,
1368    })
1369}
1370
1371#[cfg(test)]
1372fn dispatch_sync_ack(
1373    engine: &Arc<RwLock<Engine>>,
1374    replica_id: String,
1375    applied_lsn: u64,
1376    observed_remote_lsn: u64,
1377    credential_authenticated: bool,
1378    principal: Option<&Principal>,
1379) -> Message {
1380    dispatch_sync_ack_decision(
1381        engine,
1382        replica_id,
1383        applied_lsn,
1384        observed_remote_lsn,
1385        credential_authenticated,
1386        principal,
1387    )
1388    .message
1389}
1390
1391fn dispatch_sync_ack_decision(
1392    engine: &Arc<RwLock<Engine>>,
1393    replica_id: String,
1394    applied_lsn: u64,
1395    observed_remote_lsn: u64,
1396    credential_authenticated: bool,
1397    principal: Option<&Principal>,
1398) -> SyncDecision {
1399    if let Err((class, message)) =
1400        check_sync_protocol_permitted(credential_authenticated, principal)
1401    {
1402        return SyncDecision::error(class, message);
1403    }
1404    if let Err(message) = validate_wire_replica_id(&replica_id) {
1405        return SyncDecision::error(SyncErrorClass::InvalidReplicaId, message);
1406    }
1407    if applied_lsn > observed_remote_lsn {
1408        return SyncDecision::error(
1409            SyncErrorClass::LsnAheadOfRemote,
1410            format!(
1411                "sync ack appliedLsn {applied_lsn} is ahead of observed remoteLsn {observed_remote_lsn}"
1412            ),
1413        );
1414    }
1415    let (data_dir, remote_lsn) = match sync_context(engine) {
1416        Ok(context) => context,
1417        Err(message) => return SyncDecision::error(SyncErrorClass::SyncContext, message),
1418    };
1419    if observed_remote_lsn > remote_lsn {
1420        return SyncDecision::error(
1421            SyncErrorClass::LsnAheadOfRemote,
1422            format!(
1423                "sync ack remoteLsn {observed_remote_lsn} is ahead of primary LSN {remote_lsn}"
1424            ),
1425        );
1426    }
1427    if let Err(message) =
1428        validate_sync_ack_applyable_boundary(&data_dir, &replica_id, applied_lsn, remote_lsn)
1429    {
1430        return SyncDecision::error(SyncErrorClass::AckValidation, message);
1431    }
1432    match acknowledge_replica_apply(&data_dir, &replica_id, applied_lsn, remote_lsn) {
1433        Ok(summary) => SyncDecision::ok(Message::SyncAckResult {
1434            previous_applied_lsn: summary.previous_applied_lsn,
1435            applied_lsn: summary.applied_lsn,
1436            remote_lsn: summary.remote_lsn,
1437            advanced: summary.advanced,
1438            status: wire_sync_status(summary.status),
1439        }),
1440        Err(err) => SyncDecision::error(SyncErrorClass::AckUpdate, err.to_string()),
1441    }
1442}
1443
1444async fn run_blocking_sync<T, F>(input: T, query_timeout: Duration, f: F) -> SyncDecision
1445where
1446    T: Send + 'static,
1447    F: FnOnce(T) -> SyncDecision + Send + 'static,
1448{
1449    let mut handle = tokio::task::spawn_blocking(move || f(input));
1450    tokio::select! {
1451        result = &mut handle => match result {
1452            Ok(decision) => decision,
1453            Err(err) => SyncDecision::error(SyncErrorClass::Internal, format!("internal error: {err}")),
1454        },
1455        _ = tokio::time::sleep(query_timeout) => match handle.await {
1456            Ok(decision) => decision,
1457            Err(err) => SyncDecision::error(SyncErrorClass::Internal, format!("internal error: {err}")),
1458        },
1459    }
1460}
1461
1462async fn execute_gated_sync<T, F>(context: SyncExecutionContext<'_>, input: T, f: F) -> Message
1463where
1464    T: Send + 'static,
1465    F: FnOnce(T) -> SyncDecision + Send + 'static,
1466{
1467    let SyncExecutionContext {
1468        tx_gate,
1469        connection_has_transaction,
1470        operation,
1471        log_context,
1472        metrics,
1473        query_timeout,
1474    } = context;
1475    let start = Instant::now();
1476    if connection_has_transaction {
1477        let decision = SyncDecision::error(
1478            SyncErrorClass::ActiveTransaction,
1479            "sync protocol is unavailable inside an active transaction",
1480        );
1481        let elapsed = start.elapsed();
1482        log_sync_decision(operation, &log_context, elapsed, &decision);
1483        metrics.record_sync_operation(operation, elapsed, SyncOutcome::Error);
1484        return decision.message;
1485    }
1486
1487    let permit = match tx_gate.acquire_owned().await {
1488        Ok(permit) => permit,
1489        Err(_) => {
1490            let decision =
1491                SyncDecision::error(SyncErrorClass::QueryExecution, "query execution error");
1492            let elapsed = start.elapsed();
1493            log_sync_decision(operation, &log_context, elapsed, &decision);
1494            metrics.record_sync_operation(operation, elapsed, SyncOutcome::Error);
1495            return decision.message;
1496        }
1497    };
1498    let decision = run_blocking_sync(input, query_timeout, f).await;
1499    drop(permit);
1500    match &decision.message {
1501        Message::SyncStatusResult { status } => {
1502            metrics.record_sync_repair_action(operation, sync_repair_label(status.repair_action));
1503        }
1504        Message::SyncPullResult { status, units, .. } => {
1505            metrics.record_sync_repair_action(operation, sync_repair_label(status.repair_action));
1506            metrics.record_sync_pull_payload(units.len() as u64, sync_pull_payload_bytes(units));
1507        }
1508        Message::SyncAckResult {
1509            advanced, status, ..
1510        } => {
1511            metrics.record_sync_repair_action(operation, sync_repair_label(status.repair_action));
1512            if *advanced {
1513                metrics.inc_sync_ack_advanced();
1514            }
1515        }
1516        _ => {}
1517    }
1518    let elapsed = start.elapsed();
1519    log_sync_decision(operation, &log_context, elapsed, &decision);
1520    metrics.record_sync_operation(
1521        operation,
1522        elapsed,
1523        sync_operation_outcome(&decision.message),
1524    );
1525    decision.message
1526}
1527
1528/// Acquire the TxGate for an explicit `begin`, bounded by `tx_wait_timeout`.
1529/// Overlapping explicit transactions queue behind the permit rather than being
1530/// rejected, but a connection gives up with a clear, client-facing error once
1531/// the wait elapses — so a transaction stalled (or held open) on another
1532/// connection can never block this one indefinitely. A timeout is recorded so
1533/// `powdb_tx_gate_timeouts_total` (and the error total) stay truthful.
1534async fn acquire_begin_permit(
1535    tx_gate: &TxGate,
1536    tx_wait_timeout: Duration,
1537    metrics: &Arc<Metrics>,
1538) -> Result<OwnedSemaphorePermit, Message> {
1539    match tokio::time::timeout(tx_wait_timeout, tx_gate.clone().acquire_owned()).await {
1540        Ok(Ok(permit)) => Ok(permit),
1541        Ok(Err(_)) => Err(Message::Error {
1542            message: "query execution error".into(),
1543        }),
1544        Err(_) => {
1545            metrics.inc_tx_gate_timeout();
1546            Err(Message::Error {
1547                message: format!(
1548                    "transaction gate timeout after {}ms waiting for concurrent transaction to complete",
1549                    tx_wait_timeout.as_millis()
1550                ),
1551            })
1552        }
1553    }
1554}
1555
1556/// Execute one wire query frame and return the response plus its un-waited
1557/// WAL durability ticket. The TxGate permit is managed here and — crucially —
1558/// is already released (bare statements, commit/rollback) by the time this
1559/// returns, so the caller's `finalize_durability` wait happens OUTSIDE the
1560/// gate and overlapping committers can share an fsync.
1561#[allow(clippy::too_many_arguments)]
1562async fn execute_wire_query(
1563    engine: Arc<RwLock<Engine>>,
1564    tx_gate: TxGate,
1565    tx_permit: &mut Option<OwnedSemaphorePermit>,
1566    query: String,
1567    principal: Option<Principal>,
1568    query_timeout: Duration,
1569    tx_wait_timeout: Duration,
1570    metrics: &Arc<Metrics>,
1571) -> (Message, Option<PendingDurability>) {
1572    match classify_query_transaction_control(&query) {
1573        Some(TransactionControl::Begin) => {
1574            if tx_permit.is_some() {
1575                return (
1576                    Message::Error {
1577                        message: sanitize_error(
1578                            "cannot begin: a transaction is already active on this connection",
1579                        ),
1580                    },
1581                    None,
1582                );
1583            }
1584            let permit = match acquire_begin_permit(&tx_gate, tx_wait_timeout, metrics).await {
1585                Ok(permit) => permit,
1586                Err(response) => return (response, None),
1587            };
1588            let (response, ticket) = run_blocking_query(
1589                engine,
1590                query,
1591                principal,
1592                query_timeout,
1593                metrics,
1594                |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
1595            )
1596            .await;
1597            if is_success_response(&response) {
1598                *tx_permit = Some(permit);
1599            }
1600            (response, ticket)
1601        }
1602        Some(TransactionControl::Commit | TransactionControl::Rollback) => {
1603            let (response, ticket) = run_blocking_query(
1604                engine,
1605                query,
1606                principal,
1607                query_timeout,
1608                metrics,
1609                |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
1610            )
1611            .await;
1612            if is_success_response(&response) {
1613                // Release the gate BEFORE the caller waits on the commit's
1614                // ticket: the engine work is done and WAL order is fixed, so
1615                // another connection's commit can start (and share the fsync)
1616                // while this one waits.
1617                tx_permit.take();
1618            }
1619            (response, ticket)
1620        }
1621        None if tx_permit.is_some() => {
1622            run_blocking_query(
1623                engine,
1624                query,
1625                principal,
1626                query_timeout,
1627                metrics,
1628                |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
1629            )
1630            .await
1631        }
1632        None => {
1633            let permit = match tx_gate.acquire_owned().await {
1634                Ok(permit) => permit,
1635                Err(_) => {
1636                    return (
1637                        Message::Error {
1638                            message: "query execution error".into(),
1639                        },
1640                        None,
1641                    )
1642                }
1643            };
1644            let out = run_blocking_query(
1645                engine,
1646                query,
1647                principal,
1648                query_timeout,
1649                metrics,
1650                |engine, query, principal| dispatch_query(&engine, &query, principal.as_ref()),
1651            )
1652            .await;
1653            drop(permit);
1654            out
1655        }
1656    }
1657}
1658
1659#[allow(clippy::too_many_arguments)]
1660async fn execute_wire_query_sql(
1661    engine: Arc<RwLock<Engine>>,
1662    tx_gate: TxGate,
1663    tx_permit: &mut Option<OwnedSemaphorePermit>,
1664    query: String,
1665    principal: Option<Principal>,
1666    query_timeout: Duration,
1667    tx_wait_timeout: Duration,
1668    metrics: &Arc<Metrics>,
1669) -> (Message, Option<PendingDurability>) {
1670    match classify_sql_transaction_control(&query) {
1671        Some(TransactionControl::Begin) => {
1672            if tx_permit.is_some() {
1673                return (
1674                    Message::Error {
1675                        message: sanitize_error(
1676                            "cannot begin: a transaction is already active on this connection",
1677                        ),
1678                    },
1679                    None,
1680                );
1681            }
1682            let permit = match acquire_begin_permit(&tx_gate, tx_wait_timeout, metrics).await {
1683                Ok(permit) => permit,
1684                Err(response) => return (response, None),
1685            };
1686            let (response, ticket) = run_blocking_query(
1687                engine,
1688                query,
1689                principal,
1690                query_timeout,
1691                metrics,
1692                |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
1693            )
1694            .await;
1695            if is_success_response(&response) {
1696                *tx_permit = Some(permit);
1697            }
1698            (response, ticket)
1699        }
1700        Some(TransactionControl::Commit | TransactionControl::Rollback) => {
1701            let (response, ticket) = run_blocking_query(
1702                engine,
1703                query,
1704                principal,
1705                query_timeout,
1706                metrics,
1707                |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
1708            )
1709            .await;
1710            if is_success_response(&response) {
1711                // See execute_wire_query: release the gate before the
1712                // caller's durability wait so commits can coalesce.
1713                tx_permit.take();
1714            }
1715            (response, ticket)
1716        }
1717        None if tx_permit.is_some() => {
1718            run_blocking_query(
1719                engine,
1720                query,
1721                principal,
1722                query_timeout,
1723                metrics,
1724                |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
1725            )
1726            .await
1727        }
1728        None => {
1729            let permit = match tx_gate.acquire_owned().await {
1730                Ok(permit) => permit,
1731                Err(_) => {
1732                    return (
1733                        Message::Error {
1734                            message: "query execution error".into(),
1735                        },
1736                        None,
1737                    )
1738                }
1739            };
1740            let out = run_blocking_query(
1741                engine,
1742                query,
1743                principal,
1744                query_timeout,
1745                metrics,
1746                |engine, query, principal| dispatch_sql_query(&engine, &query, principal.as_ref()),
1747            )
1748            .await;
1749            drop(permit);
1750            out
1751        }
1752    }
1753}
1754
1755// One over clippy's default arg limit: the metrics handle was threaded through
1756// to instrument the typed query result. Bundling these into a struct would add
1757// more noise than it removes for an internal dispatcher.
1758#[allow(clippy::too_many_arguments)]
1759async fn execute_wire_query_with_params(
1760    engine: Arc<RwLock<Engine>>,
1761    tx_gate: TxGate,
1762    tx_permit: &mut Option<OwnedSemaphorePermit>,
1763    query: String,
1764    params: Vec<WireParam>,
1765    principal: Option<Principal>,
1766    query_timeout: Duration,
1767    tx_wait_timeout: Duration,
1768    metrics: &Arc<Metrics>,
1769) -> (Message, Option<PendingDurability>) {
1770    match classify_params_transaction_control(&query, &params) {
1771        Some(TransactionControl::Begin) => {
1772            if tx_permit.is_some() {
1773                return (
1774                    Message::Error {
1775                        message: sanitize_error(
1776                            "cannot begin: a transaction is already active on this connection",
1777                        ),
1778                    },
1779                    None,
1780                );
1781            }
1782            let permit = match acquire_begin_permit(&tx_gate, tx_wait_timeout, metrics).await {
1783                Ok(permit) => permit,
1784                Err(response) => return (response, None),
1785            };
1786            let (response, ticket) = run_blocking_query(
1787                engine,
1788                (query, params),
1789                principal,
1790                query_timeout,
1791                metrics,
1792                |engine, (query, params), principal| {
1793                    dispatch_query_with_params(&engine, &query, &params, principal.as_ref())
1794                },
1795            )
1796            .await;
1797            if is_success_response(&response) {
1798                *tx_permit = Some(permit);
1799            }
1800            (response, ticket)
1801        }
1802        Some(TransactionControl::Commit | TransactionControl::Rollback) => {
1803            let (response, ticket) = run_blocking_query(
1804                engine,
1805                (query, params),
1806                principal,
1807                query_timeout,
1808                metrics,
1809                |engine, (query, params), principal| {
1810                    dispatch_query_with_params(&engine, &query, &params, principal.as_ref())
1811                },
1812            )
1813            .await;
1814            if is_success_response(&response) {
1815                // See execute_wire_query: release the gate before the
1816                // caller's durability wait so commits can coalesce.
1817                tx_permit.take();
1818            }
1819            (response, ticket)
1820        }
1821        None if tx_permit.is_some() => {
1822            run_blocking_query(
1823                engine,
1824                (query, params),
1825                principal,
1826                query_timeout,
1827                metrics,
1828                |engine, (query, params), principal| {
1829                    dispatch_query_with_params(&engine, &query, &params, principal.as_ref())
1830                },
1831            )
1832            .await
1833        }
1834        None => {
1835            let permit = match tx_gate.acquire_owned().await {
1836                Ok(permit) => permit,
1837                Err(_) => {
1838                    return (
1839                        Message::Error {
1840                            message: "query execution error".into(),
1841                        },
1842                        None,
1843                    )
1844                }
1845            };
1846            let out = run_blocking_query(
1847                engine,
1848                (query, params),
1849                principal,
1850                query_timeout,
1851                metrics,
1852                |engine, (query, params), principal| {
1853                    dispatch_query_with_params(&engine, &query, &params, principal.as_ref())
1854                },
1855            )
1856            .await;
1857            drop(permit);
1858            out
1859        }
1860    }
1861}
1862
1863/// A statement's metric sample whose recording is deferred until its WAL
1864/// durability obligation settles: a Full-mode fsync failure downgrades the
1865/// client's success reply to an error, and the metrics must tell the same
1866/// story (and the latency must include the wait the client observed).
1867struct DeferredQueryMetric {
1868    start: Instant,
1869    outcome: QueryOutcome,
1870    exceeded_timeout: bool,
1871}
1872
1873/// Durability ticket + the deferred metric of the statement that produced it.
1874type PendingDurability = (WalDurabilityTicket, DeferredQueryMetric);
1875
1876async fn run_blocking_query<T, F>(
1877    engine: Arc<RwLock<Engine>>,
1878    input: T,
1879    principal: Option<Principal>,
1880    query_timeout: Duration,
1881    metrics: &Arc<Metrics>,
1882    f: F,
1883) -> (Message, Option<PendingDurability>)
1884where
1885    T: Send + 'static,
1886    F: FnOnce(Arc<RwLock<Engine>>, T, Option<Principal>) -> DispatchOutcome + Send + 'static,
1887{
1888    let _in_flight = metrics.in_flight_guard();
1889    let start = Instant::now();
1890    let mut handle = tokio::task::spawn_blocking(move || f(engine, input, principal));
1891    let mut exceeded_timeout = false;
1892    let join_result = tokio::select! {
1893        result = &mut handle => result,
1894        _ = tokio::time::sleep(query_timeout) => {
1895            exceeded_timeout = true;
1896            // `spawn_blocking` tasks that have started cannot be aborted safely.
1897            // Wait for completion before replying so a client never receives a
1898            // timeout while the same query keeps running and possibly mutating
1899            // state in the background.
1900            handle.await
1901        }
1902    };
1903
1904    let (message, ticket, outcome) = match join_result {
1905        Ok((Ok(result), ticket)) => match query_result_to_message(result) {
1906            Ok(message) => (message, ticket, QueryOutcome::Ok),
1907            Err(e) => (
1908                Message::Error {
1909                    message: sanitize_error(&e.to_string()),
1910                },
1911                ticket,
1912                QueryOutcome::Error,
1913            ),
1914        },
1915        Ok((Err(e), ticket)) => {
1916            let outcome = if matches!(e, QueryError::MemoryLimitExceeded { .. }) {
1917                QueryOutcome::MemoryLimit
1918            } else {
1919                QueryOutcome::Error
1920            };
1921            (
1922                Message::Error {
1923                    message: sanitize_error(&e.to_string()),
1924                },
1925                ticket,
1926                outcome,
1927            )
1928        }
1929        Err(e) => (
1930            Message::Error {
1931                message: format!("internal error: {e}"),
1932            },
1933            None,
1934            QueryOutcome::Error,
1935        ),
1936    };
1937    match ticket {
1938        // The statement's durability (and thus its true outcome and the
1939        // latency the client observes) settles at batch end — defer the
1940        // metric to the settlement site instead of recording a success that
1941        // a failed fsync would falsify.
1942        Some(ticket) => (
1943            message,
1944            Some((
1945                ticket,
1946                DeferredQueryMetric {
1947                    start,
1948                    outcome,
1949                    exceeded_timeout,
1950                },
1951            )),
1952        ),
1953        None => {
1954            if exceeded_timeout {
1955                metrics.record_query(start.elapsed(), QueryOutcome::Timeout);
1956            } else {
1957                metrics.record_query(start.elapsed(), outcome);
1958            }
1959            (message, None)
1960        }
1961    }
1962}
1963
1964/// Settle a WAL durability ticket off the async path, AFTER the TxGate
1965/// permit has been dropped — that ordering is what lets committers on other
1966/// connections append (and share the fsync) while this one waits.
1967///
1968/// Returns `None` when the covering fsync succeeded, or `Some(client-facing
1969/// error message)` when it failed — in which case no statement the ticket
1970/// covers may be acknowledged as durable (it executed in memory only).
1971async fn settle_durability_ticket(ticket: WalDurabilityTicket) -> Option<String> {
1972    match tokio::task::spawn_blocking(move || ticket.wait()).await {
1973        Ok(Ok(())) => None,
1974        Ok(Err(e)) => Some(sanitize_error(&format!("WAL durability sync failed: {e}"))),
1975        Err(e) => Some(format!("internal error: {e}")),
1976    }
1977}
1978
1979fn is_success_response(msg: &Message) -> bool {
1980    matches!(
1981        msg,
1982        Message::ResultRows { .. }
1983            | Message::ResultScalar { .. }
1984            | Message::ResultOk { .. }
1985            | Message::ResultMessage { .. }
1986    )
1987}
1988
1989fn rollback_open_transaction(engine: Arc<RwLock<Engine>>, principal: Option<Principal>) {
1990    let (res, ticket) = dispatch_query(&engine, "rollback", principal.as_ref());
1991    let _ = res;
1992    // Rollback takes the sync-preserving path (no ticket), but settle one
1993    // defensively if it ever appears so the durability watermark stays honest.
1994    if let Some(ticket) = ticket {
1995        let _ = ticket.wait();
1996    }
1997}
1998
1999pub async fn handle_connection<S>(stream: S, opts: ConnOpts<'_>)
2000where
2001    S: AsyncRead + AsyncWrite + Unpin,
2002{
2003    let ConnOpts {
2004        engine,
2005        tx_gate,
2006        expected_password,
2007        users,
2008        shutdown_rx,
2009        idle_timeout,
2010        query_timeout,
2011        rate_limiter,
2012        peer_addr,
2013        metrics,
2014        tx_wait_timeout,
2015        db_name: server_db_name,
2016    } = opts;
2017
2018    let peer = peer_addr
2019        .map(|a| a.to_string())
2020        .unwrap_or_else(|| "unknown".into());
2021    let peer_ip = peer_addr.map(|a| a.ip());
2022
2023    let (reader, writer) = tokio::io::split(stream);
2024    let mut reader = BufReader::new(reader);
2025    let mut writer = BufWriter::new(writer);
2026
2027    // Wait for Connect message (with idle timeout).
2028    // Accept Ping messages before authentication so load balancers can
2029    // health-check without completing a full CONNECT handshake.
2030    // Uses the smaller pre-auth payload limit (4 KB) to prevent memory abuse.
2031    let connect_msg = loop {
2032        match tokio::time::timeout(idle_timeout, Message::read_from_preauth(&mut reader)).await {
2033            Ok(Ok(Some(Message::Ping))) => {
2034                debug!(peer = %peer, "pre-auth ping");
2035                if !write_msg(&mut writer, &Message::Pong).await {
2036                    return;
2037                }
2038                continue;
2039            }
2040            Ok(Ok(Some(msg))) => break msg,
2041            Ok(Ok(None)) => {
2042                debug!(peer = %peer, "client closed before CONNECT");
2043                return;
2044            }
2045            Ok(Err(e)) => {
2046                error!(peer = %peer, error = %e, "error reading CONNECT");
2047                return;
2048            }
2049            Err(_) => {
2050                warn!(peer = %peer, "idle timeout waiting for CONNECT");
2051                return;
2052            }
2053        }
2054    };
2055
2056    // The authenticated identity for this connection. Bound at connect time
2057    // and enforced on every query by `dispatch_query`.
2058    let principal: Option<Principal>;
2059    let credential_auth_configured = !users.is_empty() || expected_password.is_some();
2060    match connect_msg {
2061        Message::Connect {
2062            db_name,
2063            password,
2064            username,
2065        } => {
2066            // Check rate limiting before verifying credentials.
2067            if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
2068                if is_rate_limited(limiter, ip) {
2069                    warn!(peer = %peer, "rate limited: too many auth failures");
2070                    let err = Message::Error {
2071                        message: "too many auth failures, try again later".into(),
2072                    };
2073                    write_msg(&mut writer, &err).await;
2074                    return;
2075                }
2076            }
2077
2078            let outcome = authenticate_connect(
2079                &users,
2080                expected_password.as_ref().map(|p| p.as_str()),
2081                username.as_deref(),
2082                password.as_ref().map(|p| p.as_str()),
2083            );
2084
2085            match outcome {
2086                AuthOutcome::Rejected => {
2087                    warn!(peer = %peer, db = %db_name, "auth rejected");
2088                    metrics.inc_auth_failure();
2089                    // Record the failure for rate limiting.
2090                    if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
2091                        record_auth_failure(limiter, ip);
2092                    }
2093                    let err = Message::Error {
2094                        message: "authentication failed".into(),
2095                    };
2096                    write_msg(&mut writer, &err).await;
2097                    return;
2098                }
2099                AuthOutcome::Authenticated {
2100                    principal: auth_principal,
2101                } => {
2102                    // Auth succeeded — clear any prior failure count.
2103                    if let (Some(limiter), Some(ip)) = (rate_limiter, peer_ip) {
2104                        clear_auth_failures(limiter, ip);
2105                    }
2106                    match &auth_principal {
2107                        Some(p) => {
2108                            info!(peer = %peer, db = %db_name, user = %p.name, role = %p.role, "authenticated");
2109                        }
2110                        None => {
2111                            info!(peer = %peer, db = %db_name, "client connected");
2112                        }
2113                    }
2114                    principal = auth_principal;
2115                }
2116            }
2117
2118            // One process serves one database. When pinned to a name, reject a
2119            // CONNECT that explicitly asks for a different one (checked after
2120            // auth so db existence never leaks to unauthenticated clients).
2121            // When unpinned, accept anything but warn once per process so the
2122            // silent one-db-per-process mismatch is visible without spamming
2123            // the log on every pooled connection.
2124            match check_db_name(server_db_name.as_deref(), &db_name) {
2125                Ok(()) => {
2126                    if server_db_name.is_none() && !db_name.is_empty() && db_name != DEFAULT_DB_NAME
2127                    {
2128                        static NAMED_DB_WARNED: std::sync::atomic::AtomicBool =
2129                            std::sync::atomic::AtomicBool::new(false);
2130                        if !NAMED_DB_WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
2131                            warn!(
2132                                peer = %peer, db = %db_name,
2133                                "client requested a named database but this server serves a single global database; name ignored"
2134                            );
2135                        }
2136                    }
2137                }
2138                Err(msg) => {
2139                    warn!(peer = %peer, db = %db_name, "rejected: unknown database");
2140                    let err = Message::Error { message: msg };
2141                    write_msg(&mut writer, &err).await;
2142                    return;
2143                }
2144            }
2145
2146            let ok = Message::ConnectOk {
2147                version: env!("CARGO_PKG_VERSION").into(),
2148            };
2149            if !write_msg(&mut writer, &ok).await {
2150                return;
2151            }
2152        }
2153        _ => {
2154            warn!(peer = %peer, "first message was not CONNECT");
2155            let err = Message::Error {
2156                message: "expected CONNECT".into(),
2157            };
2158            write_msg(&mut writer, &err).await;
2159            return;
2160        }
2161    }
2162
2163    let mut tx_permit: Option<OwnedSemaphorePermit> = None;
2164    // A non-query frame decoded during read-ahead batching, carried over to
2165    // the next iteration of the main loop.
2166    let mut carry: Option<Message> = None;
2167
2168    // Main query loop with idle timeout and shutdown awareness.
2169    'conn: loop {
2170        let msg = if let Some(m) = carry.take() {
2171            m
2172        } else {
2173            tokio::select! {
2174                // Read next message with idle timeout.
2175                result = tokio::time::timeout(idle_timeout, Message::read_from(&mut reader)) => {
2176                    match result {
2177                        Ok(Ok(Some(msg))) => msg,
2178                        Ok(Ok(None)) => break,
2179                        Ok(Err(e)) => {
2180                            error!(peer = %peer, error = %e, "read error");
2181                            break;
2182                        }
2183                        Err(_) => {
2184                            info!(peer = %peer, "idle timeout, closing connection");
2185                            let err = Message::Error { message: "idle timeout".into() };
2186                            write_msg(&mut writer, &err).await;
2187                            break;
2188                        }
2189                    }
2190                }
2191                // If server is shutting down, notify client and close.
2192                _ = shutdown_rx.changed() => {
2193                    if *shutdown_rx.borrow() {
2194                        info!(peer = %peer, "server shutting down, closing connection");
2195                        let err = Message::Error { message: "server shutting down".into() };
2196                        write_msg(&mut writer, &err).await;
2197                        break;
2198                    }
2199                    continue;
2200                }
2201            }
2202        };
2203
2204        // Plain query frames take the batched path: a pipelining client's
2205        // whole burst is executed with ONE durability wait at the end
2206        // (durability generations are cumulative, so the newest statement's
2207        // ticket covers every earlier one). Everything else is handled one
2208        // frame at a time exactly as before.
2209        if matches!(
2210            msg,
2211            Message::Query { .. } | Message::QuerySql { .. } | Message::QueryWithParams { .. }
2212        ) {
2213            /// Read-ahead cap per batch: bounds unflushed responses and keeps
2214            /// the reply latency of the first statement bounded.
2215            const MAX_PIPELINE_BATCH: usize = 128;
2216            /// Byte cap on retained (unflushed) response payloads per batch:
2217            /// large row results stop read-ahead, so one connection can never
2218            /// hold gigabytes of replies hostage to the batch's durability
2219            /// wait.
2220            const MAX_PIPELINE_BATCH_BYTES: usize = 4 << 20;
2221
2222            /// How the read-ahead loop stopped, when it stopped the whole
2223            /// connection rather than just the batch.
2224            enum BatchFatal {
2225                Closed,
2226                ReadError,
2227            }
2228
2229            /// Approximate encoded size of a response, for the batch byte
2230            /// cap. Counts the dominant string payloads; exact per-frame
2231            /// overhead is irrelevant at the 4 MiB cap.
2232            fn approx_response_bytes(msg: &Message) -> usize {
2233                match msg {
2234                    Message::ResultRows { columns, rows } => {
2235                        columns.iter().map(|c| c.len() + 4).sum::<usize>()
2236                            + rows
2237                                .iter()
2238                                .map(|r| r.iter().map(|v| v.len() + 4).sum::<usize>())
2239                                .sum::<usize>()
2240                    }
2241                    Message::ResultScalar { value } => value.len(),
2242                    Message::ResultMessage { message } | Message::Error { message } => {
2243                        message.len()
2244                    }
2245                    _ => 16,
2246                }
2247            }
2248
2249            /// Whether the reader's buffered bytes hold at least one COMPLETE
2250            /// frame (6-byte header + payload). Read-ahead must never await
2251            /// the socket: blocking on a partial frame would hold the batch's
2252            /// durability settlement and unflushed replies hostage to a slow
2253            /// (or malicious) client, up to the idle timeout.
2254            fn complete_frame_buffered(buf: &[u8]) -> bool {
2255                buf.len() >= 6 && {
2256                    let payload_len =
2257                        u32::from_le_bytes(buf[2..6].try_into().expect("4-byte slice")) as usize;
2258                    buf.len() - 6 >= payload_len
2259                }
2260            }
2261
2262            let mut responses: Vec<Message> = Vec::new();
2263            let mut response_bytes: usize = 0;
2264            let mut last_ticket: Option<WalDurabilityTicket> = None;
2265            let mut deferred_metrics: Vec<DeferredQueryMetric> = Vec::new();
2266            let mut fatal: Option<BatchFatal> = None;
2267            let mut current = msg;
2268            loop {
2269                let (response, ticket) = match current {
2270                    Message::Query { query } => {
2271                        if query.len() > MAX_QUERY_LENGTH {
2272                            (
2273                                Message::Error {
2274                                    message: format!(
2275                                        "query too large: {} bytes (max {})",
2276                                        query.len(),
2277                                        MAX_QUERY_LENGTH
2278                                    ),
2279                                },
2280                                None,
2281                            )
2282                        } else {
2283                            debug!(peer = %peer, query = %query, "received query");
2284                            execute_wire_query(
2285                                engine.clone(),
2286                                tx_gate.clone(),
2287                                &mut tx_permit,
2288                                query,
2289                                principal.clone(),
2290                                query_timeout,
2291                                tx_wait_timeout,
2292                                &metrics,
2293                            )
2294                            .await
2295                        }
2296                    }
2297                    Message::QuerySql { query } => {
2298                        if query.len() > MAX_QUERY_LENGTH {
2299                            (
2300                                Message::Error {
2301                                    message: format!(
2302                                        "query too large: {} bytes (max {})",
2303                                        query.len(),
2304                                        MAX_QUERY_LENGTH
2305                                    ),
2306                                },
2307                                None,
2308                            )
2309                        } else {
2310                            debug!(peer = %peer, query = %query, "received SQL query");
2311                            execute_wire_query_sql(
2312                                engine.clone(),
2313                                tx_gate.clone(),
2314                                &mut tx_permit,
2315                                query,
2316                                principal.clone(),
2317                                query_timeout,
2318                                tx_wait_timeout,
2319                                &metrics,
2320                            )
2321                            .await
2322                        }
2323                    }
2324                    Message::QueryWithParams { query, params } => {
2325                        if query.len() > MAX_QUERY_LENGTH {
2326                            (
2327                                Message::Error {
2328                                    message: format!(
2329                                        "query too large: {} bytes (max {})",
2330                                        query.len(),
2331                                        MAX_QUERY_LENGTH
2332                                    ),
2333                                },
2334                                None,
2335                            )
2336                        } else {
2337                            debug!(peer = %peer, query = %query, n_params = params.len(), "received parameterized query");
2338                            execute_wire_query_with_params(
2339                                engine.clone(),
2340                                tx_gate.clone(),
2341                                &mut tx_permit,
2342                                query,
2343                                params,
2344                                principal.clone(),
2345                                query_timeout,
2346                                tx_wait_timeout,
2347                                &metrics,
2348                            )
2349                            .await
2350                        }
2351                    }
2352                    _ => unreachable!("batch loop only receives plain query frames"),
2353                };
2354                if let Some((t, m)) = ticket {
2355                    // Later tickets cover earlier generations — keep only the
2356                    // newest; the batch-end wait settles them all. Every
2357                    // deferred metric is kept: each records after settlement.
2358                    last_ticket = Some(t);
2359                    deferred_metrics.push(m);
2360                }
2361                response_bytes += approx_response_bytes(&response);
2362                responses.push(response);
2363
2364                // Read ahead only when a COMPLETE next frame is already
2365                // buffered (never await the socket mid-batch) and the
2366                // retained replies stay small. While an explicit transaction
2367                // is open the connection holds the TxGate, so batching would
2368                // only extend the exclusive window — flush instead.
2369                if tx_permit.is_some()
2370                    || responses.len() >= MAX_PIPELINE_BATCH
2371                    || response_bytes >= MAX_PIPELINE_BATCH_BYTES
2372                    || !complete_frame_buffered(reader.buffer())
2373                {
2374                    break;
2375                }
2376                // The full frame is buffered, so this returns without socket
2377                // I/O; the timeout is a defensive backstop only.
2378                match tokio::time::timeout(idle_timeout, Message::read_from(&mut reader)).await {
2379                    Ok(Ok(Some(
2380                        next @ (Message::Query { .. }
2381                        | Message::QuerySql { .. }
2382                        | Message::QueryWithParams { .. }),
2383                    ))) => {
2384                        // If another connection currently holds the TxGate,
2385                        // the next statement would block on the gate with
2386                        // this batch's replies still unflushed (pre-batching,
2387                        // they'd already have been written). Flush first and
2388                        // handle the frame on the next main-loop iteration.
2389                        // Benign TOCTOU: worst case is one early flush or one
2390                        // gate wait with an empty reply queue.
2391                        if tx_gate.available_permits() == 0 {
2392                            carry = Some(next);
2393                            break;
2394                        }
2395                        current = next;
2396                    }
2397                    Ok(Ok(Some(other))) => {
2398                        // Not a plain query — flush this batch, then handle
2399                        // the frame on the next main-loop iteration.
2400                        carry = Some(other);
2401                        break;
2402                    }
2403                    Ok(Ok(None)) => {
2404                        fatal = Some(BatchFatal::Closed);
2405                        break;
2406                    }
2407                    Ok(Err(e)) => {
2408                        error!(peer = %peer, error = %e, "read error");
2409                        fatal = Some(BatchFatal::ReadError);
2410                        break;
2411                    }
2412                    Err(_) => {
2413                        // Unreachable in practice: the frame was fully
2414                        // buffered, so read_from needs no socket I/O.
2415                        error!(peer = %peer, "timeout decoding fully-buffered frame");
2416                        fatal = Some(BatchFatal::ReadError);
2417                        break;
2418                    }
2419                }
2420            }
2421
2422            // ONE durability wait for the whole batch, then the deferred
2423            // metrics: a durability failure records Ok statements as errors,
2424            // and latency includes the settlement wait the client observed.
2425            let mut durability_failed = false;
2426            if let Some(ticket) = last_ticket {
2427                if let Some(message) = settle_durability_ticket(ticket).await {
2428                    // The covering fsync failed: nothing in this batch may be
2429                    // acknowledged as durable.
2430                    durability_failed = true;
2431                    for r in responses.iter_mut() {
2432                        if is_success_response(r) {
2433                            *r = Message::Error {
2434                                message: message.clone(),
2435                            };
2436                        }
2437                    }
2438                }
2439            }
2440            for m in deferred_metrics.drain(..) {
2441                let outcome = if m.exceeded_timeout {
2442                    QueryOutcome::Timeout
2443                } else if durability_failed && matches!(m.outcome, QueryOutcome::Ok) {
2444                    QueryOutcome::Error
2445                } else {
2446                    m.outcome
2447                };
2448                metrics.record_query(m.start.elapsed(), outcome);
2449            }
2450
2451            for r in &responses {
2452                if !write_msg(&mut writer, r).await {
2453                    break 'conn;
2454                }
2455            }
2456            match fatal {
2457                None => continue,
2458                Some(BatchFatal::Closed | BatchFatal::ReadError) => break,
2459            }
2460        }
2461
2462        let response = match msg {
2463            Message::Ping => {
2464                debug!(peer = %peer, "ping");
2465                Message::Pong
2466            }
2467            Message::SyncStatus { replica_id } => {
2468                let engine = engine.clone();
2469                let principal = principal.clone();
2470                let log_context = SyncLogContext::status(&replica_id);
2471                execute_gated_sync(
2472                    SyncExecutionContext {
2473                        tx_gate: tx_gate.clone(),
2474                        connection_has_transaction: tx_permit.is_some(),
2475                        operation: SyncOperation::Status,
2476                        log_context,
2477                        metrics: &metrics,
2478                        query_timeout,
2479                    },
2480                    (engine, replica_id, credential_auth_configured, principal),
2481                    |(engine, replica_id, credential_authenticated, principal)| {
2482                        dispatch_sync_status_decision(
2483                            &engine,
2484                            replica_id,
2485                            credential_authenticated,
2486                            principal.as_ref(),
2487                        )
2488                    },
2489                )
2490                .await
2491            }
2492            Message::SyncPull {
2493                replica_id,
2494                since_lsn,
2495                max_units,
2496                max_bytes,
2497                database_id,
2498                primary_generation,
2499                wal_format_version,
2500                catalog_version,
2501                segment_format_version,
2502            } => {
2503                let engine = engine.clone();
2504                let principal = principal.clone();
2505                let request = SyncPullRequest {
2506                    replica_id,
2507                    since_lsn,
2508                    max_units,
2509                    max_bytes,
2510                    database_id,
2511                    primary_generation,
2512                    wal_format_version,
2513                    catalog_version,
2514                    segment_format_version,
2515                };
2516                let log_context = SyncLogContext::pull(&request);
2517                execute_gated_sync(
2518                    SyncExecutionContext {
2519                        tx_gate: tx_gate.clone(),
2520                        connection_has_transaction: tx_permit.is_some(),
2521                        operation: SyncOperation::Pull,
2522                        log_context,
2523                        metrics: &metrics,
2524                        query_timeout,
2525                    },
2526                    (engine, request, credential_auth_configured, principal),
2527                    |(engine, request, credential_authenticated, principal)| {
2528                        dispatch_sync_pull_decision(
2529                            &engine,
2530                            request,
2531                            credential_authenticated,
2532                            principal.as_ref(),
2533                        )
2534                    },
2535                )
2536                .await
2537            }
2538            Message::SyncAck {
2539                replica_id,
2540                applied_lsn,
2541                remote_lsn,
2542            } => {
2543                let engine = engine.clone();
2544                let principal = principal.clone();
2545                let log_context = SyncLogContext::ack(&replica_id, applied_lsn, remote_lsn);
2546                execute_gated_sync(
2547                    SyncExecutionContext {
2548                        tx_gate: tx_gate.clone(),
2549                        connection_has_transaction: tx_permit.is_some(),
2550                        operation: SyncOperation::Ack,
2551                        log_context,
2552                        metrics: &metrics,
2553                        query_timeout,
2554                    },
2555                    (
2556                        engine,
2557                        replica_id,
2558                        applied_lsn,
2559                        remote_lsn,
2560                        credential_auth_configured,
2561                        principal,
2562                    ),
2563                    |(
2564                        engine,
2565                        replica_id,
2566                        applied_lsn,
2567                        observed_remote_lsn,
2568                        credential_authenticated,
2569                        principal,
2570                    )| {
2571                        dispatch_sync_ack_decision(
2572                            &engine,
2573                            replica_id,
2574                            applied_lsn,
2575                            observed_remote_lsn,
2576                            credential_authenticated,
2577                            principal.as_ref(),
2578                        )
2579                    },
2580                )
2581                .await
2582            }
2583            Message::Disconnect => {
2584                debug!(peer = %peer, "received DISCONNECT");
2585                break;
2586            }
2587            _ => Message::Error {
2588                message: "unexpected message type".into(),
2589            },
2590        };
2591
2592        if !write_msg(&mut writer, &response).await {
2593            break;
2594        }
2595    }
2596
2597    // Roll back any open transaction the client left behind on disconnect.
2598    // The permit must stay alive in `tx_permit` for the duration of the awaited
2599    // rollback and be released only afterwards — mirroring the query-timeout
2600    // path above. Using `tx_permit.take().is_some()` here would drop the permit
2601    // (freeing the TxGate) *before* the rollback runs, letting another
2602    // connection BEGIN a transaction that this stale rollback would then clobber.
2603    if tx_permit.is_some() {
2604        let engine = engine.clone();
2605        let principal = principal.clone();
2606        let _ =
2607            tokio::task::spawn_blocking(move || rollback_open_transaction(engine, principal)).await;
2608    }
2609    tx_permit.take();
2610
2611    info!(peer = %peer, "client disconnected");
2612}
2613
2614fn charge_response_bytes(total: &mut usize, bytes: usize) -> Result<(), QueryError> {
2615    *total = total.saturating_add(bytes);
2616    if *total > MAX_RESPONSE_PAYLOAD_SIZE {
2617        return Err(QueryError::Execution(format!(
2618            "result too large: encoded response exceeds {} bytes; add a limit or narrower projection",
2619            MAX_RESPONSE_PAYLOAD_SIZE
2620        )));
2621    }
2622    Ok(())
2623}
2624
2625fn query_result_to_message(result: QueryResult) -> Result<Message, QueryError> {
2626    match result {
2627        QueryResult::Rows { columns, rows } => {
2628            let mut encoded_bytes = 2usize; // column count
2629            let mut out_columns = Vec::with_capacity(columns.len());
2630            for col in columns {
2631                charge_response_bytes(&mut encoded_bytes, 4 + col.len())?;
2632                out_columns.push(col);
2633            }
2634            charge_response_bytes(&mut encoded_bytes, 4)?; // row count
2635
2636            let mut str_rows = Vec::with_capacity(rows.len());
2637            for row in rows {
2638                let mut str_row = Vec::with_capacity(row.len());
2639                for value in row {
2640                    let display = value_to_display(&value);
2641                    charge_response_bytes(&mut encoded_bytes, 4 + display.len())?;
2642                    str_row.push(display);
2643                }
2644                str_rows.push(str_row);
2645            }
2646            Ok(Message::ResultRows {
2647                columns: out_columns,
2648                rows: str_rows,
2649            })
2650        }
2651        QueryResult::Scalar(val) => Ok(Message::ResultScalar {
2652            value: value_to_display(&val),
2653        }),
2654        QueryResult::Modified(n) => Ok(Message::ResultOk { affected: n }),
2655        QueryResult::Created(name) => Ok(Message::ResultMessage {
2656            message: format!("type {name} created"),
2657        }),
2658        QueryResult::Executed { message } => Ok(Message::ResultMessage { message }),
2659    }
2660}
2661
2662// Canonical wire rendering lives on `Value` (`powdb_storage`) so the server,
2663// CLI, and embedded bindings render results identically. Kept as a thin alias
2664// to minimize churn at the call sites in this module.
2665fn value_to_display(v: &Value) -> String {
2666    v.to_wire_string()
2667}
2668
2669#[cfg(test)]
2670mod tests {
2671    use super::*;
2672    use powdb_storage::wal::WalRecordType;
2673    use powdb_sync::{
2674        write_identity_snapshot, write_segment_atomic, DatabaseIdentity, IdentitySnapshot,
2675        ReplicaCursor, RetainedSegment, RetainedUnit,
2676    };
2677
2678    // ---- Wire NULL rendering (Fix: remote protocol rendered NULL as `{}`) ----
2679
2680    #[test]
2681    fn null_serializes_as_null_bareword_on_wire() {
2682        assert_eq!(value_to_display(&Value::Empty), "null");
2683    }
2684
2685    // ---- Error sanitization allowlist ----
2686
2687    #[test]
2688    fn unique_violation_error_surfaces_to_remote_clients() {
2689        // The storage layer reports the actionable message; the server must
2690        // not replace it with the generic "query execution error".
2691        assert_eq!(
2692            sanitize_error("unique constraint violation on User.email"),
2693            "unique constraint violation on User.email"
2694        );
2695    }
2696
2697    #[test]
2698    fn internal_errors_stay_generic() {
2699        assert_eq!(
2700            sanitize_error("some internal io panic detail"),
2701            "query execution error"
2702        );
2703    }
2704
2705    // ---- Named-database gate (P-10) ----
2706
2707    #[test]
2708    fn db_name_unpinned_accepts_any_name() {
2709        for requested in ["", "default", "prod", "anything"] {
2710            assert!(
2711                check_db_name(None, requested).is_ok(),
2712                "rejected {requested}"
2713            );
2714        }
2715    }
2716
2717    #[test]
2718    fn db_name_pinned_accepts_match_empty_and_default_sentinel() {
2719        // The configured name, the empty name, and the client default sentinel
2720        // are all "no foreign database explicitly requested".
2721        assert!(check_db_name(Some("prod"), "prod").is_ok());
2722        assert!(check_db_name(Some("prod"), "").is_ok());
2723        assert!(check_db_name(Some("prod"), DEFAULT_DB_NAME).is_ok());
2724    }
2725
2726    #[test]
2727    fn db_name_pinned_rejects_foreign_with_clear_message() {
2728        let err = check_db_name(Some("prod"), "staging").unwrap_err();
2729        assert_eq!(err, "unknown database 'staging'; this server serves 'prod'");
2730    }
2731
2732    // ---- Explicit-transaction gate wait timeout (P-4) ----
2733
2734    #[tokio::test]
2735    async fn begin_permit_acquires_when_gate_is_free() {
2736        let gate = new_tx_gate();
2737        let metrics = Arc::new(Metrics::new());
2738        let permit = acquire_begin_permit(&gate, Duration::from_secs(5), &metrics)
2739            .await
2740            .expect("should acquire a free gate");
2741        assert_eq!(gate.available_permits(), 0, "permit must be held");
2742        drop(permit);
2743        assert_eq!(gate.available_permits(), 1, "permit must release on drop");
2744    }
2745
2746    #[tokio::test]
2747    async fn begin_permit_times_out_with_clear_error_and_truthful_metric() {
2748        let gate = new_tx_gate();
2749        let metrics = Arc::new(Metrics::new());
2750        // Hold the only permit so the next acquire must wait, then time out.
2751        let _held = gate.clone().acquire_owned().await.unwrap();
2752        let err = acquire_begin_permit(&gate, Duration::from_millis(25), &metrics)
2753            .await
2754            .expect_err("must time out while the gate is held");
2755        match err {
2756            Message::Error { message } => {
2757                assert!(
2758                    message.contains("transaction gate timeout after 25ms"),
2759                    "unexpected message: {message}"
2760                );
2761                assert!(
2762                    message.contains("waiting for concurrent transaction"),
2763                    "unexpected message: {message}"
2764                );
2765            }
2766            other => panic!("expected Error, got {other:?}"),
2767        }
2768        let rendered = metrics.render();
2769        assert!(rendered.contains("powdb_tx_gate_timeouts_total 1"));
2770        // A timed-out begin is a failed statement from the client's view.
2771        assert!(rendered.contains("powdb_queries_total{result=\"error\"} 1"));
2772    }
2773
2774    #[test]
2775    fn resource_limit_errors_surface_actionable_hints() {
2776        // These carry user-actionable guidance and leak no internal state, so
2777        // they must reach the client verbatim — not be masked to the generic
2778        // message. The exact strings come from QueryError's Display impl
2779        // (crates/query/src/result.rs).
2780        for msg in [
2781            "sort input exceeds row limit — add a LIMIT clause",
2782            "join result exceeds row limit",
2783            "query exceeded memory budget: requested 100 bytes, limit 50 bytes",
2784            "result too large: encoded response exceeds 1024 bytes; add a limit or narrower projection",
2785        ] {
2786            assert_eq!(sanitize_error(msg), msg, "should pass through verbatim");
2787        }
2788    }
2789
2790    #[test]
2791    fn oversized_result_is_rejected_before_wire_encoding() {
2792        let long = "x".repeat(MAX_RESPONSE_PAYLOAD_SIZE);
2793        let result = QueryResult::Rows {
2794            columns: vec!["payload".into()],
2795            rows: vec![vec![Value::Str(long)]],
2796        };
2797        let err = query_result_to_message(result).unwrap_err();
2798        assert!(
2799            err.to_string().starts_with("result too large"),
2800            "unexpected error: {err}"
2801        );
2802    }
2803
2804    // ---- Role enforcement (Fix: readonly role was not enforced) ----
2805
2806    fn parsed(q: &str) -> powdb_query::ast::Statement {
2807        parser::parse(q).unwrap()
2808    }
2809
2810    fn principal(role: &str) -> Option<Principal> {
2811        Some(Principal {
2812            name: "u".into(),
2813            role: role.into(),
2814        })
2815    }
2816
2817    #[test]
2818    fn readonly_can_read_but_not_write() {
2819        let p = principal("readonly");
2820        // Reads pass.
2821        assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
2822        assert!(check_statement_permitted(p.as_ref(), &parsed("count(User)")).is_ok());
2823        assert!(check_statement_permitted(p.as_ref(), &parsed("explain User")).is_ok());
2824        // Writes, DDL, and transaction control are denied.
2825        for q in [
2826            r#"insert User { name := "x" }"#,
2827            "User filter .id = 1 update { age := 2 }",
2828            "User filter .id = 1 delete",
2829            "drop User",
2830            "alter User add column c: str",
2831            "type T { required id: int }",
2832            "begin",
2833            "commit",
2834            "rollback",
2835        ] {
2836            let err = check_statement_permitted(p.as_ref(), &parsed(q))
2837                .expect_err(&format!("must deny: {q}"));
2838            assert!(
2839                err.to_string().contains("permission denied"),
2840                "unexpected error for {q}: {err}"
2841            );
2842        }
2843    }
2844
2845    #[test]
2846    fn readwrite_and_admin_have_full_query_access() {
2847        for role in ["readwrite", "admin"] {
2848            let p = principal(role);
2849            assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
2850            assert!(check_statement_permitted(
2851                p.as_ref(),
2852                &parsed(r#"insert User { name := "x" }"#)
2853            )
2854            .is_ok());
2855            assert!(check_statement_permitted(p.as_ref(), &parsed("drop User")).is_ok());
2856        }
2857    }
2858
2859    #[test]
2860    fn unknown_role_fails_closed_for_writes() {
2861        let p = principal("mystery");
2862        assert!(check_statement_permitted(p.as_ref(), &parsed("User")).is_ok());
2863        assert!(
2864            check_statement_permitted(p.as_ref(), &parsed(r#"insert User { name := "x" }"#))
2865                .is_err()
2866        );
2867    }
2868
2869    #[test]
2870    fn no_principal_means_full_access() {
2871        // Shared-password / open mode: no per-user identity, no restriction.
2872        assert!(check_statement_permitted(None, &parsed("drop User")).is_ok());
2873        assert!(check_statement_permitted(None, &parsed(r#"insert User { name := "x" }"#)).is_ok());
2874    }
2875
2876    fn store_with_alice() -> UserStore {
2877        let mut s = UserStore::new();
2878        s.create_user("alice", "pw", "readwrite").unwrap();
2879        s
2880    }
2881
2882    // ---- Empty store: legacy shared-password fallback ----
2883
2884    #[test]
2885    fn empty_store_no_password_is_open() {
2886        let s = UserStore::new();
2887        assert_eq!(
2888            authenticate_connect(&s, None, None, None),
2889            AuthOutcome::Authenticated { principal: None }
2890        );
2891        // Even a stray username/password is accepted (legacy open behavior).
2892        assert_eq!(
2893            authenticate_connect(&s, None, Some("x"), Some("y")),
2894            AuthOutcome::Authenticated { principal: None }
2895        );
2896    }
2897
2898    #[test]
2899    fn empty_store_correct_shared_password_succeeds() {
2900        let s = UserStore::new();
2901        assert_eq!(
2902            authenticate_connect(&s, Some("pw"), None, Some("pw")),
2903            AuthOutcome::Authenticated { principal: None }
2904        );
2905    }
2906
2907    #[test]
2908    fn empty_store_wrong_shared_password_rejected() {
2909        let s = UserStore::new();
2910        assert_eq!(
2911            authenticate_connect(&s, Some("pw"), None, Some("bad")),
2912            AuthOutcome::Rejected
2913        );
2914    }
2915
2916    #[test]
2917    fn empty_store_missing_password_rejected_when_expected() {
2918        let s = UserStore::new();
2919        assert_eq!(
2920            authenticate_connect(&s, Some("pw"), None, None),
2921            AuthOutcome::Rejected
2922        );
2923    }
2924
2925    #[test]
2926    fn empty_store_ignores_username_for_shared_password() {
2927        // A new client may send a username even against a shared-password
2928        // server; the username is ignored and the password still governs.
2929        let s = UserStore::new();
2930        assert_eq!(
2931            authenticate_connect(&s, Some("pw"), Some("whoever"), Some("pw")),
2932            AuthOutcome::Authenticated { principal: None }
2933        );
2934    }
2935
2936    // ---- Populated store: multi-user auth ----
2937
2938    #[test]
2939    fn user_auth_success_binds_principal() {
2940        let s = store_with_alice();
2941        assert_eq!(
2942            authenticate_connect(&s, None, Some("alice"), Some("pw")),
2943            AuthOutcome::Authenticated {
2944                principal: Some(Principal {
2945                    name: "alice".into(),
2946                    role: "readwrite".into(),
2947                })
2948            }
2949        );
2950    }
2951
2952    #[test]
2953    fn user_auth_wrong_password_rejected() {
2954        let s = store_with_alice();
2955        assert_eq!(
2956            authenticate_connect(&s, None, Some("alice"), Some("bad")),
2957            AuthOutcome::Rejected
2958        );
2959    }
2960
2961    #[test]
2962    fn user_auth_unknown_user_rejected() {
2963        let s = store_with_alice();
2964        assert_eq!(
2965            authenticate_connect(&s, None, Some("mallory"), Some("pw")),
2966            AuthOutcome::Rejected
2967        );
2968    }
2969
2970    #[test]
2971    fn user_auth_missing_username_rejected() {
2972        let s = store_with_alice();
2973        assert_eq!(
2974            authenticate_connect(&s, None, None, Some("pw")),
2975            AuthOutcome::Rejected
2976        );
2977    }
2978
2979    #[test]
2980    fn user_auth_missing_password_rejected() {
2981        let s = store_with_alice();
2982        assert_eq!(
2983            authenticate_connect(&s, Some("pw"), Some("alice"), None),
2984            AuthOutcome::Rejected
2985        );
2986    }
2987
2988    #[test]
2989    fn user_auth_ignores_shared_password_when_users_present() {
2990        // With users present, the shared password is irrelevant: supplying it as
2991        // the password without a valid user must NOT authenticate.
2992        let s = store_with_alice();
2993        assert_eq!(
2994            authenticate_connect(&s, Some("shared"), None, Some("shared")),
2995            AuthOutcome::Rejected
2996        );
2997    }
2998
2999    #[test]
3000    fn replica_fingerprint_is_stable_and_redacted() {
3001        let replica_id = "customer-prod-replica-a";
3002        let fingerprint = replica_fingerprint(replica_id);
3003        assert_eq!(fingerprint, replica_fingerprint(replica_id));
3004        assert_eq!(fingerprint, log_replica_fingerprint(replica_id));
3005        assert_ne!(fingerprint, replica_fingerprint("customer-prod-replica-b"));
3006        assert_eq!(fingerprint.len(), 16);
3007        assert!(fingerprint.chars().all(|c| c.is_ascii_hexdigit()));
3008        assert!(!fingerprint.contains("customer"));
3009        assert!(!fingerprint.contains("replica"));
3010        assert!(!fingerprint.contains(replica_id));
3011    }
3012
3013    #[test]
3014    fn invalid_replica_ids_use_fixed_log_fingerprint() {
3015        assert_eq!(log_replica_fingerprint(""), INVALID_REPLICA_FINGERPRINT);
3016        assert_eq!(
3017            log_replica_fingerprint("customer/prod/replica"),
3018            INVALID_REPLICA_FINGERPRINT
3019        );
3020        assert_eq!(
3021            log_replica_fingerprint(&"a".repeat(4096)),
3022            INVALID_REPLICA_FINGERPRINT
3023        );
3024    }
3025
3026    #[test]
3027    fn sync_error_classes_use_bounded_labels() {
3028        assert_eq!(SyncErrorClass::AuthRequired.as_label(), "auth_required");
3029        assert_eq!(
3030            SyncErrorClass::PermissionDenied.as_label(),
3031            "permission_denied"
3032        );
3033        assert_eq!(
3034            SyncErrorClass::IdentityOrFormatMismatch.as_label(),
3035            "identity_or_format_mismatch"
3036        );
3037        assert_eq!(SyncErrorClass::AckValidation.as_label(), "ack_validation");
3038        assert_eq!(SyncErrorClass::Internal.as_label(), "internal");
3039    }
3040
3041    fn sync_identity() -> DatabaseIdentity {
3042        DatabaseIdentity {
3043            database_id: *b"server-sync-test",
3044            primary_generation: 1,
3045        }
3046    }
3047
3048    fn retained_unit(lsn: u64) -> RetainedUnit {
3049        RetainedUnit {
3050            tx_id: 1,
3051            record_type: 4,
3052            lsn,
3053            data: lsn.to_le_bytes().to_vec(),
3054        }
3055    }
3056
3057    fn retained_unit_with(tx_id: u64, record_type: WalRecordType, lsn: u64) -> RetainedUnit {
3058        RetainedUnit {
3059            tx_id,
3060            record_type: record_type as u8,
3061            lsn,
3062            data: lsn.to_le_bytes().to_vec(),
3063        }
3064    }
3065
3066    fn write_sync_identity_and_tail(data_dir: &std::path::Path, through_lsn: u64) {
3067        let identity = sync_identity();
3068        write_identity_snapshot(data_dir, &IdentitySnapshot::from_identity(identity, 1)).unwrap();
3069        let units = (1..=through_lsn).map(retained_unit).collect();
3070        let segment = RetainedSegment::new(identity.segment_identity(), units).unwrap();
3071        write_segment_atomic(&retained_segments_dir(data_dir), &segment).unwrap();
3072    }
3073
3074    fn write_sync_identity_and_units(data_dir: &std::path::Path, units: Vec<RetainedUnit>) {
3075        let identity = sync_identity();
3076        write_identity_snapshot(data_dir, &IdentitySnapshot::from_identity(identity, 1)).unwrap();
3077        let segment = RetainedSegment::new(identity.segment_identity(), units).unwrap();
3078        write_segment_atomic(&retained_segments_dir(data_dir), &segment).unwrap();
3079    }
3080
3081    fn write_sync_identity_only(data_dir: &std::path::Path) {
3082        let identity = sync_identity();
3083        write_identity_snapshot(data_dir, &IdentitySnapshot::from_identity(identity, 1)).unwrap();
3084    }
3085
3086    fn admin_principal() -> Principal {
3087        Principal {
3088            name: "admin".into(),
3089            role: "admin".into(),
3090        }
3091    }
3092
3093    #[test]
3094    fn sync_protocol_requires_credential_auth_and_rejects_readonly() {
3095        let dir = tempfile::tempdir().unwrap();
3096        let engine = Arc::new(RwLock::new(Engine::new(dir.path()).unwrap()));
3097
3098        match dispatch_sync_status(&engine, "replica-a".into(), false, None) {
3099            Message::Error { message } => {
3100                assert!(message.contains("requires authentication"));
3101            }
3102            other => panic!("expected auth error, got {other:?}"),
3103        }
3104
3105        let readonly = Principal {
3106            name: "reader".into(),
3107            role: "readonly".into(),
3108        };
3109        match dispatch_sync_status(&engine, "replica-a".into(), true, Some(&readonly)) {
3110            Message::Error { message } => {
3111                assert!(message.contains("permission denied"));
3112            }
3113            other => panic!("expected permission error, got {other:?}"),
3114        }
3115    }
3116
3117    #[test]
3118    fn sync_status_pull_and_ack_use_server_remote_lsn() {
3119        let dir = tempfile::tempdir().unwrap();
3120        let mut engine = Engine::new(dir.path()).unwrap();
3121        engine
3122            .execute_powql("type SyncT { required id: int, v: str }")
3123            .unwrap();
3124        engine
3125            .execute_powql(r#"insert SyncT { id := 1, v := "one" }"#)
3126            .unwrap();
3127        let remote_lsn = engine.catalog().max_lsn();
3128        assert!(remote_lsn > 0);
3129        write_sync_identity_and_tail(dir.path(), remote_lsn);
3130        powdb_sync::upsert_replica_cursor(dir.path(), ReplicaCursor::active("replica-a", 0))
3131            .unwrap();
3132
3133        let engine = Arc::new(RwLock::new(engine));
3134        let principal = admin_principal();
3135        let status = match dispatch_sync_status(&engine, "replica-a".into(), true, Some(&principal))
3136        {
3137            Message::SyncStatusResult { status } => status,
3138            other => panic!("expected sync status, got {other:?}"),
3139        };
3140        assert_eq!(status.remote_lsn, remote_lsn);
3141        assert_eq!(status.servable_lsn, Some(remote_lsn));
3142        assert_eq!(status.unarchived_lsn, Some(0));
3143        assert_eq!(status.last_applied_lsn, Some(0));
3144        assert_eq!(status.repair_action, WireSyncRepairAction::Pull);
3145        assert!(status.stale);
3146
3147        let identity = sync_identity().segment_identity();
3148        let pull = SyncPullRequest {
3149            replica_id: "replica-a".into(),
3150            since_lsn: 0,
3151            max_units: MAX_SYNC_PULL_UNITS,
3152            max_bytes: MAX_SYNC_PULL_BYTES,
3153            database_id: identity.database_id,
3154            primary_generation: identity.primary_generation,
3155            wal_format_version: identity.wal_format_version,
3156            catalog_version: identity.catalog_version,
3157            segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3158        };
3159        let units = match dispatch_sync_pull(&engine, pull, true, Some(&principal)) {
3160            Message::SyncPullResult {
3161                status,
3162                units,
3163                has_more,
3164            } => {
3165                assert_eq!(status.repair_action, WireSyncRepairAction::Pull);
3166                assert!(!has_more);
3167                units
3168            }
3169            other => panic!("expected sync pull result, got {other:?}"),
3170        };
3171        assert_eq!(units.len() as u64, remote_lsn);
3172        assert_eq!(units.last().unwrap().lsn, remote_lsn);
3173
3174        let ack = match dispatch_sync_ack(
3175            &engine,
3176            "replica-a".into(),
3177            remote_lsn,
3178            remote_lsn,
3179            true,
3180            Some(&principal),
3181        ) {
3182            Message::SyncAckResult {
3183                previous_applied_lsn,
3184                applied_lsn,
3185                remote_lsn: ack_remote_lsn,
3186                advanced,
3187                status,
3188            } => {
3189                assert_eq!(previous_applied_lsn, 0);
3190                assert_eq!(applied_lsn, remote_lsn);
3191                assert_eq!(ack_remote_lsn, remote_lsn);
3192                assert!(advanced);
3193                status
3194            }
3195            other => panic!("expected sync ack result, got {other:?}"),
3196        };
3197        assert_eq!(ack.repair_action, WireSyncRepairAction::None);
3198        assert!(!ack.stale);
3199        assert_eq!(ack.lag_lsn, Some(0));
3200    }
3201
3202    #[test]
3203    fn sync_pull_and_ack_reject_transaction_cut_boundaries() {
3204        let dir = tempfile::tempdir().unwrap();
3205        let mut engine = Engine::new(dir.path()).unwrap();
3206        engine
3207            .execute_powql("type SyncT { required id: int }")
3208            .unwrap();
3209        for id in 1..=3 {
3210            engine
3211                .execute_powql(&format!("insert SyncT {{ id := {id} }}"))
3212                .unwrap();
3213        }
3214        let remote_lsn = engine.catalog().max_lsn();
3215        assert!(remote_lsn >= 3);
3216        write_sync_identity_and_units(
3217            dir.path(),
3218            vec![
3219                retained_unit_with(77, WalRecordType::Begin, 1),
3220                retained_unit_with(77, WalRecordType::Insert, 2),
3221                retained_unit_with(77, WalRecordType::Commit, 3),
3222            ],
3223        );
3224        powdb_sync::upsert_replica_cursor(dir.path(), ReplicaCursor::active("replica-a", 0))
3225            .unwrap();
3226
3227        let engine = Arc::new(RwLock::new(engine));
3228        let principal = admin_principal();
3229        let identity = sync_identity().segment_identity();
3230        let cut_pull = SyncPullRequest {
3231            replica_id: "replica-a".into(),
3232            since_lsn: 0,
3233            max_units: 2,
3234            max_bytes: MAX_SYNC_PULL_BYTES,
3235            database_id: identity.database_id,
3236            primary_generation: identity.primary_generation,
3237            wal_format_version: identity.wal_format_version,
3238            catalog_version: identity.catalog_version,
3239            segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3240        };
3241        match dispatch_sync_pull(&engine, cut_pull, true, Some(&principal)) {
3242            Message::Error { message } => assert!(message.contains("cuts through transaction")),
3243            other => panic!("expected transaction-cut pull error, got {other:?}"),
3244        }
3245
3246        let cut_bytes_pull = SyncPullRequest {
3247            replica_id: "replica-a".into(),
3248            since_lsn: 0,
3249            max_units: 3,
3250            max_bytes: 58,
3251            database_id: identity.database_id,
3252            primary_generation: identity.primary_generation,
3253            wal_format_version: identity.wal_format_version,
3254            catalog_version: identity.catalog_version,
3255            segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3256        };
3257        match dispatch_sync_pull(&engine, cut_bytes_pull, true, Some(&principal)) {
3258            Message::Error { message } => assert!(message.contains("cuts through transaction")),
3259            other => panic!("expected byte-capped transaction-cut pull error, got {other:?}"),
3260        }
3261
3262        let full_pull = SyncPullRequest {
3263            replica_id: "replica-a".into(),
3264            since_lsn: 0,
3265            max_units: 3,
3266            max_bytes: MAX_SYNC_PULL_BYTES,
3267            database_id: identity.database_id,
3268            primary_generation: identity.primary_generation,
3269            wal_format_version: identity.wal_format_version,
3270            catalog_version: identity.catalog_version,
3271            segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3272        };
3273        match dispatch_sync_pull(&engine, full_pull, true, Some(&principal)) {
3274            Message::SyncPullResult { units, .. } => {
3275                assert_eq!(units.len(), 3);
3276                assert_eq!(units.last().unwrap().lsn, 3);
3277            }
3278            other => panic!("expected complete transaction pull, got {other:?}"),
3279        }
3280
3281        match dispatch_sync_ack(
3282            &engine,
3283            "replica-a".into(),
3284            2,
3285            remote_lsn,
3286            true,
3287            Some(&principal),
3288        ) {
3289            Message::Error { message } => assert!(message.contains("cuts through transaction")),
3290            other => panic!("expected transaction-cut ack error, got {other:?}"),
3291        }
3292        let cursor = powdb_sync::read_replica_cursors(dir.path()).unwrap();
3293        assert_eq!(cursor[0].applied_lsn, 0);
3294
3295        match dispatch_sync_ack(
3296            &engine,
3297            "replica-a".into(),
3298            3,
3299            remote_lsn,
3300            true,
3301            Some(&principal),
3302        ) {
3303            Message::SyncAckResult {
3304                previous_applied_lsn,
3305                applied_lsn,
3306                advanced,
3307                ..
3308            } => {
3309                assert_eq!(previous_applied_lsn, 0);
3310                assert_eq!(applied_lsn, 3);
3311                assert!(advanced);
3312            }
3313            other => panic!("expected complete transaction ack, got {other:?}"),
3314        }
3315    }
3316
3317    #[test]
3318    fn sync_pull_byte_cap_returns_applyable_prefix_with_reused_tx_id() {
3319        let dir = tempfile::tempdir().unwrap();
3320        let mut engine = Engine::new(dir.path()).unwrap();
3321        engine
3322            .execute_powql("type SyncT { required id: int }")
3323            .unwrap();
3324        for id in 1..=6 {
3325            engine
3326                .execute_powql(&format!("insert SyncT {{ id := {id} }}"))
3327                .unwrap();
3328        }
3329        let remote_lsn = engine.catalog().max_lsn();
3330        assert!(remote_lsn >= 6);
3331        write_sync_identity_and_units(
3332            dir.path(),
3333            vec![
3334                retained_unit_with(1, WalRecordType::Begin, 1),
3335                retained_unit_with(1, WalRecordType::Insert, 2),
3336                retained_unit_with(1, WalRecordType::Commit, 3),
3337                retained_unit_with(1, WalRecordType::Begin, 4),
3338                retained_unit_with(1, WalRecordType::Insert, 5),
3339                retained_unit_with(1, WalRecordType::Commit, 6),
3340            ],
3341        );
3342        powdb_sync::upsert_replica_cursor(dir.path(), ReplicaCursor::active("replica-a", 0))
3343            .unwrap();
3344
3345        let engine = Arc::new(RwLock::new(engine));
3346        let principal = admin_principal();
3347        let identity = sync_identity().segment_identity();
3348        let pull = SyncPullRequest {
3349            replica_id: "replica-a".into(),
3350            since_lsn: 0,
3351            max_units: 6,
3352            max_bytes: 100,
3353            database_id: identity.database_id,
3354            primary_generation: identity.primary_generation,
3355            wal_format_version: identity.wal_format_version,
3356            catalog_version: identity.catalog_version,
3357            segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3358        };
3359
3360        match dispatch_sync_pull(&engine, pull, true, Some(&principal)) {
3361            Message::SyncPullResult {
3362                status,
3363                units,
3364                has_more,
3365            } => {
3366                assert_eq!(status.repair_action, WireSyncRepairAction::Pull);
3367                assert_eq!(units.len(), 3);
3368                assert_eq!(units.last().unwrap().lsn, 3);
3369                assert!(has_more);
3370            }
3371            other => panic!("expected byte-capped applyable prefix, got {other:?}"),
3372        }
3373    }
3374
3375    #[test]
3376    fn sync_pull_never_serves_units_beyond_server_remote_lsn() {
3377        let dir = tempfile::tempdir().unwrap();
3378        let mut engine = Engine::new(dir.path()).unwrap();
3379        engine
3380            .execute_powql("type SyncT { required id: int }")
3381            .unwrap();
3382        engine.execute_powql("insert SyncT { id := 1 }").unwrap();
3383        let remote_lsn = engine.catalog().max_lsn();
3384        assert!(remote_lsn > 0);
3385        write_sync_identity_and_tail(dir.path(), remote_lsn + 2);
3386        powdb_sync::upsert_replica_cursor(dir.path(), ReplicaCursor::active("replica-a", 0))
3387            .unwrap();
3388
3389        let engine = Arc::new(RwLock::new(engine));
3390        let principal = admin_principal();
3391        let identity = sync_identity().segment_identity();
3392        let pull = SyncPullRequest {
3393            replica_id: "replica-a".into(),
3394            since_lsn: 0,
3395            max_units: MAX_SYNC_PULL_UNITS,
3396            max_bytes: MAX_SYNC_PULL_BYTES,
3397            database_id: identity.database_id,
3398            primary_generation: identity.primary_generation,
3399            wal_format_version: identity.wal_format_version,
3400            catalog_version: identity.catalog_version,
3401            segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3402        };
3403
3404        match dispatch_sync_pull(&engine, pull, true, Some(&principal)) {
3405            Message::SyncPullResult {
3406                status,
3407                units,
3408                has_more,
3409            } => {
3410                assert_eq!(status.remote_lsn, remote_lsn);
3411                assert_eq!(status.servable_lsn, Some(remote_lsn));
3412                assert_eq!(status.unarchived_lsn, Some(0));
3413                assert_eq!(status.repair_action, WireSyncRepairAction::Pull);
3414                assert!(!has_more);
3415                assert_eq!(units.len() as u64, remote_lsn);
3416                assert_eq!(units.last().unwrap().lsn, remote_lsn);
3417                assert!(units.iter().all(|unit| unit.lsn <= remote_lsn));
3418            }
3419            other => panic!("expected capped sync pull result, got {other:?}"),
3420        }
3421    }
3422
3423    #[test]
3424    fn sync_status_reports_await_archive_when_primary_outruns_retained_tail() {
3425        let dir = tempfile::tempdir().unwrap();
3426        let mut engine = Engine::new(dir.path()).unwrap();
3427        engine
3428            .execute_powql("type SyncT { required id: int }")
3429            .unwrap();
3430        engine.execute_powql("insert SyncT { id := 1 }").unwrap();
3431        let remote_lsn = engine.catalog().max_lsn();
3432        assert!(remote_lsn > 0);
3433        write_sync_identity_only(dir.path());
3434        powdb_sync::upsert_replica_cursor(dir.path(), ReplicaCursor::active("replica-a", 0))
3435            .unwrap();
3436
3437        let engine = Arc::new(RwLock::new(engine));
3438        let principal = admin_principal();
3439        let identity = sync_identity().segment_identity();
3440        let status = match dispatch_sync_status(&engine, "replica-a".into(), true, Some(&principal))
3441        {
3442            Message::SyncStatusResult { status } => status,
3443            other => panic!("expected sync status, got {other:?}"),
3444        };
3445        assert_eq!(status.remote_lsn, remote_lsn);
3446        assert_eq!(status.servable_lsn, Some(0));
3447        assert_eq!(status.unarchived_lsn, Some(remote_lsn));
3448        assert_eq!(status.repair_action, WireSyncRepairAction::AwaitArchive);
3449        assert!(status
3450            .last_sync_error
3451            .as_deref()
3452            .unwrap()
3453            .contains("not yet archived"));
3454
3455        let pull = SyncPullRequest {
3456            replica_id: "replica-a".into(),
3457            since_lsn: 0,
3458            max_units: MAX_SYNC_PULL_UNITS,
3459            max_bytes: MAX_SYNC_PULL_BYTES,
3460            database_id: identity.database_id,
3461            primary_generation: identity.primary_generation,
3462            wal_format_version: identity.wal_format_version,
3463            catalog_version: identity.catalog_version,
3464            segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3465        };
3466        match dispatch_sync_pull(&engine, pull, true, Some(&principal)) {
3467            Message::SyncPullResult {
3468                status,
3469                units,
3470                has_more,
3471            } => {
3472                assert_eq!(status.repair_action, WireSyncRepairAction::AwaitArchive);
3473                assert!(units.is_empty());
3474                assert!(!has_more);
3475            }
3476            other => panic!("expected await-archive sync pull result, got {other:?}"),
3477        }
3478    }
3479
3480    #[test]
3481    fn sync_pull_serves_partial_retained_prefix_when_archive_lags_remote_lsn() {
3482        let dir = tempfile::tempdir().unwrap();
3483        let mut engine = Engine::new(dir.path()).unwrap();
3484        engine
3485            .execute_powql("type SyncT { required id: int }")
3486            .unwrap();
3487        engine.execute_powql("insert SyncT { id := 1 }").unwrap();
3488        engine.execute_powql("insert SyncT { id := 2 }").unwrap();
3489        let remote_lsn = engine.catalog().max_lsn();
3490        assert!(remote_lsn > 1);
3491        let servable_lsn = remote_lsn - 1;
3492        write_sync_identity_and_tail(dir.path(), servable_lsn);
3493        powdb_sync::upsert_replica_cursor(dir.path(), ReplicaCursor::active("replica-a", 0))
3494            .unwrap();
3495
3496        let engine = Arc::new(RwLock::new(engine));
3497        let principal = admin_principal();
3498        let identity = sync_identity().segment_identity();
3499        let pull = SyncPullRequest {
3500            replica_id: "replica-a".into(),
3501            since_lsn: 0,
3502            max_units: MAX_SYNC_PULL_UNITS,
3503            max_bytes: MAX_SYNC_PULL_BYTES,
3504            database_id: identity.database_id,
3505            primary_generation: identity.primary_generation,
3506            wal_format_version: identity.wal_format_version,
3507            catalog_version: identity.catalog_version,
3508            segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3509        };
3510
3511        match dispatch_sync_pull(&engine, pull, true, Some(&principal)) {
3512            Message::SyncPullResult {
3513                status,
3514                units,
3515                has_more,
3516            } => {
3517                assert_eq!(status.remote_lsn, remote_lsn);
3518                assert_eq!(status.servable_lsn, Some(servable_lsn));
3519                assert_eq!(status.unarchived_lsn, Some(1));
3520                assert_eq!(status.repair_action, WireSyncRepairAction::Pull);
3521                assert!(!has_more);
3522                assert_eq!(units.len() as u64, servable_lsn);
3523                assert_eq!(units.last().unwrap().lsn, servable_lsn);
3524                assert!(units.iter().all(|unit| unit.lsn <= servable_lsn));
3525            }
3526            other => panic!("expected partial sync pull result, got {other:?}"),
3527        }
3528    }
3529
3530    #[test]
3531    fn sync_pull_rejects_cursor_or_format_mismatch() {
3532        let dir = tempfile::tempdir().unwrap();
3533        let mut engine = Engine::new(dir.path()).unwrap();
3534        engine
3535            .execute_powql("type SyncT { required id: int }")
3536            .unwrap();
3537        engine.execute_powql("insert SyncT { id := 1 }").unwrap();
3538        let remote_lsn = engine.catalog().max_lsn();
3539        write_sync_identity_and_tail(dir.path(), remote_lsn);
3540        powdb_sync::upsert_replica_cursor(dir.path(), ReplicaCursor::active("replica-a", 0))
3541            .unwrap();
3542        let engine = Arc::new(RwLock::new(engine));
3543        let principal = admin_principal();
3544        let identity = sync_identity().segment_identity();
3545
3546        let wrong_cursor = SyncPullRequest {
3547            replica_id: "replica-a".into(),
3548            since_lsn: 1,
3549            max_units: 10,
3550            max_bytes: MAX_SYNC_PULL_BYTES,
3551            database_id: identity.database_id,
3552            primary_generation: identity.primary_generation,
3553            wal_format_version: identity.wal_format_version,
3554            catalog_version: identity.catalog_version,
3555            segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION,
3556        };
3557        match dispatch_sync_pull(&engine, wrong_cursor, true, Some(&principal)) {
3558            Message::Error { message } => assert!(message.contains("does not match")),
3559            other => panic!("expected cursor mismatch error, got {other:?}"),
3560        }
3561
3562        let wrong_format = SyncPullRequest {
3563            replica_id: "replica-a".into(),
3564            since_lsn: 0,
3565            max_units: 10,
3566            max_bytes: MAX_SYNC_PULL_BYTES,
3567            database_id: identity.database_id,
3568            primary_generation: identity.primary_generation,
3569            wal_format_version: identity.wal_format_version,
3570            catalog_version: identity.catalog_version,
3571            segment_format_version: RETAINED_SEGMENT_FORMAT_VERSION + 1,
3572        };
3573        match dispatch_sync_pull(&engine, wrong_format, true, Some(&principal)) {
3574            Message::Error { message } => assert!(message.contains("rebootstrap required")),
3575            other => panic!("expected format mismatch error, got {other:?}"),
3576        }
3577    }
3578}