Skip to main content

powdb_query/
result.rs

1use std::fmt;
2
3use powdb_storage::types::Value;
4
5/// The result of executing a query.
6#[derive(Debug)]
7pub enum QueryResult {
8    Rows {
9        columns: Vec<String>,
10        rows: Vec<Vec<Value>>,
11    },
12    Scalar(Value),   // count, avg, etc.
13    Modified(u64),   // insert/update/delete — number of rows affected
14    Created(String), // DDL — type name created
15    Executed {
16        message: String,
17    }, // DDL — alter/drop feedback
18}
19
20impl QueryResult {
21    pub fn row_count(&self) -> usize {
22        match self {
23            QueryResult::Rows { rows, .. } => rows.len(),
24            QueryResult::Scalar(_) => 1,
25            QueryResult::Modified(n) => *n as usize,
26            QueryResult::Created(_) => 0,
27            QueryResult::Executed { .. } => 0,
28        }
29    }
30}
31
32/// Typed error enum for query execution failures.
33///
34/// Replaces the previous `Result<QueryResult, String>` pattern with
35/// structured variants that callers can programmatically match on.
36/// The `From<String>` impl enables gradual migration — existing
37/// `Err(format!(...))` sites continue to compile via `?` propagation.
38#[derive(Debug, Clone, PartialEq)]
39pub enum QueryError {
40    /// Table does not exist.
41    TableNotFound(String),
42    /// Column does not exist on table.
43    ColumnNotFound { table: String, column: String },
44    /// Type mismatch in expression.
45    TypeError(String),
46    /// Join result exceeded MAX_JOIN_ROWS.
47    JoinLimitExceeded,
48    /// A fallback nested-loop join would evaluate more candidate pairs than
49    /// the measured safety cap.
50    NestedLoopPairLimitExceeded {
51        left_rows: usize,
52        right_rows: usize,
53        limit: usize,
54    },
55    /// Sort exceeded MAX_SORT_ROWS.
56    SortLimitExceeded,
57    /// Per-query memory budget exceeded during materialization (sort buffer,
58    /// join build side, GROUP BY hash table, or IN-list). Returned cleanly so
59    /// the server process is never OOM-killed by a crafted query.
60    MemoryLimitExceeded {
61        limit_bytes: usize,
62        requested_bytes: usize,
63    },
64    /// Parse error (wraps parser error).
65    Parse(String),
66    /// Index-related error.
67    IndexError(String),
68    /// View-related error.
69    ViewError(String),
70    /// WAL or I/O error.
71    StorageError(String),
72    /// Readonly path needs write lock (internal sentinel).
73    ReadonlyNeedsWrite,
74    /// The engine was opened read-only (snapshot serving) and the statement
75    /// requires a writer. Unlike [`QueryError::ReadonlyNeedsWrite`], this is a
76    /// terminal error with an operator-facing message: there is no writer to
77    /// escalate to in this mode.
78    ReadonlyMode,
79    /// The per-query deadline elapsed before execution finished. Returned as a
80    /// clean early-return from an unbounded executor loop so the query releases
81    /// its locks instead of running to completion. `timeout_ms` is the
82    /// configured per-query timeout.
83    Timeout { timeout_ms: u64 },
84    /// Execution was cancelled cooperatively (e.g. the issuing client
85    /// disconnected). Like [`QueryError::Timeout`], a clean early-return.
86    Cancelled,
87    /// Generic execution error (catch-all for migration).
88    Execution(String),
89}
90
91impl fmt::Display for QueryError {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        match self {
94            QueryError::TableNotFound(t) => write!(f, "table '{t}' not found"),
95            QueryError::ColumnNotFound { table, column } => {
96                if table.is_empty() {
97                    write!(f, "column '{column}' not found")
98                } else {
99                    write!(f, "column '{column}' not found in table '{table}'")
100                }
101            }
102            QueryError::TypeError(msg) => write!(f, "type mismatch: {msg}"),
103            QueryError::JoinLimitExceeded => write!(f, "join result exceeds row limit"),
104            QueryError::NestedLoopPairLimitExceeded {
105                left_rows,
106                right_rows,
107                limit,
108            } => {
109                let pairs = left_rows.checked_mul(*right_rows);
110                match pairs {
111                    Some(pairs) => write!(
112                        f,
113                        "nested-loop join would evaluate {pairs} candidate pairs, above the {limit} pair limit; add an equi-key to ON, index/filter an input, reduce the joined row counts, or raise the cap via POWDB_MAX_NESTED_LOOP_PAIRS"
114                    ),
115                    None => write!(
116                        f,
117                        "nested-loop join candidate count overflows usize ({left_rows} x {right_rows}), above the {limit} pair limit; add an equi-key to ON, index/filter an input, reduce the joined row counts, or raise the cap via POWDB_MAX_NESTED_LOOP_PAIRS"
118                    ),
119                }
120            }
121            QueryError::SortLimitExceeded => {
122                write!(f, "sort input exceeds row limit — add a LIMIT clause")
123            }
124            QueryError::MemoryLimitExceeded {
125                limit_bytes,
126                requested_bytes,
127            } => write!(
128                f,
129                "query exceeded memory budget: requested {requested_bytes} bytes, limit {limit_bytes} bytes"
130            ),
131            QueryError::Parse(msg) => write!(f, "{msg}"),
132            QueryError::IndexError(msg) => write!(f, "{msg}"),
133            QueryError::ViewError(msg) => write!(f, "{msg}"),
134            QueryError::StorageError(msg) => write!(f, "{msg}"),
135            QueryError::ReadonlyNeedsWrite => {
136                write!(f, "__POWDB_READONLY_NEEDS_WRITE__")
137            }
138            QueryError::ReadonlyMode => write!(
139                f,
140                "readonly mode: statement requires a writer (this database was opened read-only for snapshot serving; refresh materialized views before snapshotting a read-only directory)"
141            ),
142            QueryError::Timeout { timeout_ms } => {
143                write!(f, "query timeout after {timeout_ms}ms")
144            }
145            QueryError::Cancelled => write!(f, "query cancelled by client disconnect"),
146            QueryError::Execution(msg) => write!(f, "{msg}"),
147        }
148    }
149}
150
151impl std::error::Error for QueryError {}
152
153impl From<String> for QueryError {
154    fn from(s: String) -> Self {
155        QueryError::Execution(s)
156    }
157}
158
159impl From<&str> for QueryError {
160    fn from(s: &str) -> Self {
161        QueryError::Execution(s.to_string())
162    }
163}