rossoha opened a new pull request, #864:
URL: https://github.com/apache/arrow-rs-object-store/pull/864

   # feat: add conditional deletes (`ObjectStore::delete_opts`)
   
   Addresses #298.
   
   ## Summary
   
   Adds a backend-neutral API for compare-and-swap deletes: an object is deleted
   only if its current version matches the version the caller last observed.
   
   ```text
   delete object
   ONLY IF
   current object version == expected version
   ```
   
   Conditional delete provides optimistic concurrency control. A backend that
   supports the supplied precondition performs the check **atomically with the
   deletion**, as part of a single conditional request. Implementations must not
   emulate it with a separate metadata read followed by an unconditional delete 
—
   that sequence has a race between the two requests.
   
   ## API
   
   ```rust
   pub struct DeleteOptions {
       /// If set, the object is deleted only if its current version matches
       /// otherwise returning `Error::Precondition`
       pub precondition: Option<UpdateVersion>,
       pub extensions: Extensions,
   }
   
   trait ObjectStore {
       async fn delete_opts(&self, location: &Path, options: DeleteOptions) -> 
Result<()>;
   }
   ```
   
   Usage:
   
   ```rust
   let meta = store.head(&path).await?;
   let options = 
DeleteOptions::new().with_precondition(Some(UpdateVersion::from(meta)));
   store.delete_opts(&path, options).await?; // Err(Precondition) if it changed
   ```
   
   `UpdateVersion` (the existing `e_tag` + `version` identity, already used by
   `PutMode::Update`) is reused rather than introducing a second version model; 
each
   backend picks the field its native mechanism needs. `From<ObjectMeta> for
   UpdateVersion` is added for ergonomics.
   
   ### `delete` / `delete_opts` / `delete_stream`
   
   - `delete_opts` is a **provided** trait method, so existing implementations 
keep
     compiling. Its default returns `Error::NotSupported` when a precondition is
     supplied, and otherwise behaves exactly like today's single-object delete
     (drives `delete_stream` with one location).
   - `ObjectStoreExt::delete` is now `delete_opts(location, 
DeleteOptions::default())`,
     so unconditionally deleting is unchanged for every backend, including the 
S3
     bulk `DeleteObjects` path.
   - `delete_stream` is deliberately **unchanged**: it is a bulk API, whereas
     conditional delete is per-object. Bulk conditional deletes are out of scope
     here and can be added later without breaking this design.
   
   ## Backends
   
   | Backend | Mechanism | Status |
   | --- | --- | --- |
   | GCS | `x-goog-if-generation-match` with `ObjectMeta::version` (generation) 
| implemented |
   | S3 | `If-Match` with the ETag, reusing the existing `S3ConditionalPut` 
gate | implemented |
   | Azure | `If-Match` with the ETag on `Delete Blob` | implemented |
   | InMemory | ETag compared and the entry removed under the same lock | 
implemented |
   | Local FS | not atomic, returns `NotSupported`; unconditional delete 
unchanged | not supported |
   | HTTP/WebDAV | default implementation / `NotImplemented` | not supported |
   
   Errors reuse the existing taxonomy: a failed precondition surfaces as
   `Error::Precondition` (the status mapping for `412` already exists), so 
callers
   can distinguish "object does not exist" (`NotFound`) from "object changed"
   (`Precondition`). Backends that cannot evaluate the precondition return
   `NotSupported`/`NotImplemented` instead of silently ignoring it.
   
   For S3, some S3-compatible endpoints do not implement `If-Match` on
   `DeleteObject` and answer `501 Not Implemented`; that is surfaced as
   `Error::NotSupported` so callers can fall back to an unconditional delete, 
rather
   than reporting an opaque transport error. `S3ConditionalPut::Disabled` also
   disables conditional deletes, consistent with conditional puts.
   
   ## Performance
   
   No additional round trips. `delete(path)` remains one request (bulk
   `DeleteObjects` on S3, since the existing single-object behavior is 
preserved),
   and a conditional delete is a single conditional `DELETE`, never `HEAD` + 
`DELETE`.
   
   ## Compatibility
   
   Additive: new types plus a defaulted trait method, no signature changes, no
   behavior change for existing paths. This makes it suitable for a minor 
release
   (`0.14.x`) rather than requiring `0.15.0`.
   
   One practical consequence: wrapper stores that use
   `#[deny(clippy::missing_trait_methods)]` — the pattern recommended in this
   crate's own docs — must forward `delete_opts` explicitly once the default 
exists.
   This PR does that for `ChunkedStore`, `LimitStore`, `PrefixStore` and
   `ThrottledStore`.
   
   ## Testing
   
   - Trait-level integration test (`integration::conditional_delete`): 
unconditional
     delete, correct-version delete, stale-version delete returning 
`Precondition`
     with the newer object and its content untouched, and a 
`NotSupported`/`NotImplemented`
     path that asserts the object is left alone while ordinary deletes keep 
working.
     Wired into the in-memory, local filesystem, `Box`/`Arc`, S3, Azure and GCS 
suites.
   - Request-shape unit tests with the mock HTTP server for GCS, S3 and Azure:
     exactly one `DELETE` carrying the expected precondition header (a second 
queued
     handler fails the test, proving there is no preceding `HEAD`), `412` 
surfacing
     as `Error::Precondition`, and — for S3 — that an unconditional delete still
     uses bulk `DeleteObjects`.
   - A stateful stub reproducing GCS semantics (stale generation → `412`, 
matching
     generation → `204` and object gone).
   - Verified live against Azurite (Azure): stale version → `412 Precondition
     Failed`, newer object preserved, matching version deletes. LocalStack 
reports
     `501` for `If-Match` on `DELETE` and the test skips via `NotSupported`.
   
   ## Notes for reviewers
   
   1. The fake GCS server used by the default integration job silently 
**ignores**
      `ifGenerationMatch` on delete, so the GCS conditional-delete scenario only
      runs when pointed at the real GCS endpoint (same gating as the existing
      `ifGenerationMatch`-dependent tests).
   2. Open question: keep S3 conditional deletes gated behind the existing
      `S3ConditionalPut`, or introduce a separate knob?
   3. Open question: is `DeleteOptions::precondition` the preferred shape, or 
would
      maintainers prefer a `DeleteMode`-style enum (as with 
`PutMode`/`CopyMode`)?
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to