Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 122 additions & 6 deletions MIGRATING-0.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This is the consolidated migration guide for the v0.3.0 bundle of breaking and additive changes. Each section corresponds to a bundle PR; the guide grows as each PR lands. The bundle ships as one major bump after the last PR merges.

> Each bundle PR uses `chore:` Conventional Commit prefix to defer release-please from cutting an early version. After all PRs merge, a single `feat!:` commit with a `BREAKING CHANGE:` footer triggers v0.3.0.
> Each bundle PR uses `chore:` Conventional Commit prefix to defer release-please from cutting an early version. After all PRs merge, a single `feat:` commit with a `BREAKING CHANGE:` footer triggers v0.3.0.

---

Expand All @@ -26,7 +26,7 @@ The public `hyperdb_api::Error` type was redesigned into a flat enum per the [Mi
```rust
pub enum Error {
// Connection / transport
Connection { message: String, source: Option<std::io::Error> },
Connection { message: String, source: Option<std::io::Error>, sqlstate: Option<String> },
Authentication(String),
Tls(String),

Expand All @@ -38,9 +38,9 @@ pub enum Error {
Io(std::io::Error),

// Lifecycle
Closed(String),
Closed { message: String, sqlstate: Option<String> },
Timeout(String),
Cancelled(String),
Cancelled { message: String, sqlstate: Option<String> },

// Type / value
Conversion(String),
Expand All @@ -52,6 +52,7 @@ pub enum Error {
InvalidTableDefinition(String),
NotFound(String),
AlreadyExists(String),
InvalidOperation(String), // added in Follow-up B

// Column / row mapping
Column { name: String, kind: ColumnErrorKind },
Expand Down Expand Up @@ -97,15 +98,18 @@ Error::invalid_name("...");
Error::invalid_table_definition("...");
Error::not_found("...");
Error::already_exists("...");
Error::invalid_operation("...");
```

Pattern-matching uses the PascalCase variant names (e.g. `Error::Conversion(msg)`); only construction switches to snake_case. Forward-compatibility for new struct-variant fields relies on going through these constructors — `#[non_exhaustive]` on individual struct variants is forbidden by Rust E0639.

### Behavioral note: SQLSTATE on non-server errors

`Error::sqlstate()` now returns `Some(...)` only for [`Error::Server`]. Previously it could return `Some` for any wrapped `client::Error` whose internal type carried a SQLSTATE code, including some `Cancelled`, `Closed`, and `Connection` paths that arrived from the server with codes like `57014` (`query_canceled`), `57P03` (`cannot_connect_now`), or `08*` connection-class codes.
> **Updated by Follow-up C below.** v0.3.0 ships with structured SQLSTATE on `Server`, `Connection`, `Closed`, and `Cancelled`. Use `Error::sqlstate()` on any of these variants to retrieve the code; or destructure the variant directly to read it.

After v0.3.0 those SQLSTATE codes are folded into the variant's message string (still visible to humans via `Display`) but are not surfaced by `Error::sqlstate()`. If you branch on those codes, parse them out of the message string or open a follow-up issue requesting structured SQLSTATE on `Connection`/`Closed`/`Cancelled`/`Timeout` variants.
`Error::sqlstate()` returns `Some(...)` for [`Error::Server`] (Query-class codes), [`Error::Connection`] (typically `08*`), [`Error::Closed`] (typically `57P0*` shutdown codes), and [`Error::Cancelled`] (typically `57014` `query_canceled`) when the underlying server provided a code. Other variants always return `None`.

If you have callers that previously parsed SQLSTATE out of the message string for `Cancelled` / `Closed` / `Connection`, switch them to destructuring or `Error::sqlstate()` — see the Follow-up C section below for recipes.

### Migration recipes

Expand Down Expand Up @@ -373,6 +377,118 @@ The proc-macro lives in a new `hyperdb-api-derive` workspace crate (Rust require

---

## Follow-up C — Structured SQLSTATE on `Cancelled` / `Closed` / `Connection`

Reverses the v0.3.0 "non-Server SQLSTATE drops to message" caveat. SQLSTATE codes that arrive via cancellation (`57014`), connection-class (`08*`), or close-class wire errors (`57P01` admin shutdown, `57P02` crash shutdown) are now exposed structurally via the variant's `sqlstate` field, and `Error::sqlstate()` returns them too — no more parsing the message string.

### Variant shape changes

```rust
// Before
Error::Cancelled(String)
Error::Closed(String)
Error::Connection { message: String, source: Option<std::io::Error> }

// After
Error::Cancelled { message: String, sqlstate: Option<String> }
Error::Closed { message: String, sqlstate: Option<String> }
Error::Connection { message: String, source: Option<std::io::Error>, sqlstate: Option<String> }
```

`Error::Cancelled` and `Error::Closed` are now struct variants instead of tuple variants. Match arms that destructured them as tuples must switch to struct-pattern syntax.

### Migration recipes

**Pattern-match for the message** — before:

```rust
match err {
Error::Cancelled(msg) => log::warn!("cancelled: {msg}"),
Error::Closed(msg) => log::warn!("closed: {msg}"),
other => return Err(other),
}
```

after:

```rust
match err {
Error::Cancelled { message, sqlstate } => log::warn!("cancelled: {message} ({sqlstate:?})"),
Error::Closed { message, sqlstate } => log::warn!("closed: {message} ({sqlstate:?})"),
other => return Err(other),
}
```

If you only need the message, use `..` to elide other fields: `Error::Cancelled { message, .. }`.

**Read SQLSTATE structurally** — new in Follow-up C:

```rust
if let Error::Cancelled { sqlstate: Some(code), .. } = &err {
if code == "57014" { /* user cancellation, distinct from timeout */ }
}

// Or via the helper:
match err.sqlstate() {
Some("08006") => /* connection failure */,
Some("57014") => /* query_canceled */,
Some("57P01") => /* admin shutdown */,
_ => {}
}
```

### Constructors

The existing `Error::cancelled(msg)`, `Error::closed(msg)`, and `Error::connection(msg)` keep working — they default `sqlstate: None`. Three new constructors carry SQLSTATE:

```rust
Error::cancelled_with_sqlstate(message, sqlstate); // both impl Into<String>
Error::closed_with_sqlstate(message, sqlstate);
Error::connection_with_sqlstate(message, sqlstate);
```

`Error::connection_with_io(message, io_err)` is unchanged — it still defaults `sqlstate: None`. If you have an `io::Error` cause *and* a SQLSTATE, construct via the struct-expression form (forward-compatibility caveat: future field additions may break it; prefer adding a constructor if this combination becomes common).

---

## Follow-up B — `Error::InvalidOperation` for caller-API misuse

A new `Error::InvalidOperation(String)` variant separates caller-API misuse from `Error::Internal`. `Error::Internal` is now reserved for true library invariant violations the caller could not have triggered; misuse of caller-facing methods (mixing two mutually exclusive insertion modes, calling `insert_record_batches()` before `insert_data()`, etc.) returns `Error::InvalidOperation`.

### Affected sites

`ArrowInserter` state-machine errors that were previously `Error::Internal` are now `Error::InvalidOperation`:

- Mixing `insert_data()` / `insert_record_batches()` / `insert_raw()` with `insert_batch()` (and vice versa).
- Calling `insert_record_batches()` before `insert_data()` has sent the schema.
- Calling `insert_data()` after the schema has already been sent.

### Migration recipe

Match arms that previously caught `Error::Internal { .. }` for any of these caller-misuse cases must now match `Error::InvalidOperation(_)`:

```rust
// Before
match err {
Error::Internal { message } if message.starts_with("Cannot mix") => /* user-API misuse */,
Error::Internal { .. } => /* invariant violation */,
other => return Err(other),
}

// After
match err {
Error::InvalidOperation(_) => /* user-API misuse, caller bug */,
Error::Internal { .. } => /* library invariant violation, hyperdb-api bug */,
other => return Err(other),
}
```

### Constructor

`Error::invalid_operation(message: impl Into<String>)`. `String`-shaped tuple variant, matching the `Error::Conversion` / `Error::Config` / `Error::FeatureNotSupported` pattern — no `.to_string()` ceremony needed.

---

## #70 (continued) — Ergonomic constructors across all workspace error types

The same ergonomic-constructor pattern was applied to every error type in the workspace that user code might construct, so call sites no longer need `.to_string()` ceremony for string-literal arguments.
Expand Down
24 changes: 12 additions & 12 deletions hyperdb-api/src/arrow_inserter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,15 +301,15 @@ impl<'conn> ArrowInserter<'conn> {
}

if self.insert_mode == Some(InsertMode::BatchIpc) {
return Err(Error::internal(
return Err(Error::invalid_operation(
"Cannot mix insert_data() with insert_batch(). \
Use either raw IPC methods (insert_data/insert_record_batches) \
or RecordBatch methods (insert_batch), not both.",
));
}

if self.schema_sent {
return Err(Error::internal(
return Err(Error::invalid_operation(
"Arrow schema was already sent. Use insert_record_batches() for subsequent chunks without schema, \
or use insert_data() only once with the complete Arrow IPC stream.",
));
Expand Down Expand Up @@ -386,15 +386,15 @@ impl<'conn> ArrowInserter<'conn> {
}

if self.insert_mode == Some(InsertMode::BatchIpc) {
return Err(Error::internal(
return Err(Error::invalid_operation(
"Cannot mix insert_record_batches() with insert_batch(). \
Use either raw IPC methods (insert_data/insert_record_batches) \
or RecordBatch methods (insert_batch), not both.",
));
}

if !self.schema_sent {
return Err(Error::internal(
return Err(Error::invalid_operation(
"No Arrow schema has been sent yet. Call insert_data() first with a complete \
Arrow IPC stream that includes the schema.",
));
Expand Down Expand Up @@ -436,9 +436,9 @@ impl<'conn> ArrowInserter<'conn> {
///
/// # Errors
///
/// - Returns [`Error::Internal`] if a previous `insert_batch` call already
/// locked the inserter into `RecordBatch` IPC mode — raw IPC and
/// `RecordBatch` paths cannot be mixed.
/// - Returns [`Error::InvalidOperation`] if a previous `insert_batch`
/// call already locked the inserter into `RecordBatch` IPC mode —
/// raw IPC and `RecordBatch` paths cannot be mixed.
/// - Returns [`Error::FeatureNotSupported`] / [`Error::Server`] if the lazy COPY
/// session fails to open.
/// - Returns [`Error::Server`] / [`Error::Io`] if the server rejects
Expand All @@ -449,7 +449,7 @@ impl<'conn> ArrowInserter<'conn> {
}

if self.insert_mode == Some(InsertMode::BatchIpc) {
return Err(Error::internal(
return Err(Error::invalid_operation(
"Cannot mix insert_raw() with insert_batch(). \
Use either raw IPC methods (insert_data/insert_record_batches/insert_raw) \
or RecordBatch methods (insert_batch), not both.",
Expand Down Expand Up @@ -611,9 +611,9 @@ impl<'conn> ArrowInserter<'conn> {
///
/// # Errors
///
/// - Returns [`Error::Internal`] if a previous raw-IPC call locked this
/// inserter into the other mode — raw IPC and `RecordBatch` paths
/// cannot be mixed.
/// - Returns [`Error::InvalidOperation`] if a previous raw-IPC call
/// locked this inserter into the other mode — raw IPC and
/// `RecordBatch` paths cannot be mixed.
/// - Returns [`Error::FeatureNotSupported`] / [`Error::Server`] if the lazy COPY
/// session cannot be opened.
/// - Returns [`Error::Conversion`] wrapping the underlying Arrow IPC
Expand All @@ -630,7 +630,7 @@ impl<'conn> ArrowInserter<'conn> {
/// `Some`, so the unwrap is unreachable.
pub fn insert_batch(&mut self, batch: &arrow::record_batch::RecordBatch) -> Result<()> {
if self.insert_mode == Some(InsertMode::RawIpc) {
return Err(Error::internal(
return Err(Error::invalid_operation(
"Cannot mix insert_batch() with raw IPC methods. \
Use either RecordBatch methods (insert_batch) \
or raw IPC methods (insert_data/insert_record_batches/insert_raw), not both.",
Expand Down
Loading
Loading