hfutatzhanghb commented on code in PR #7818:
URL: https://github.com/apache/opendal/pull/7818#discussion_r3467352617


##########
core/core/src/docs/rfcs/7818_rename_if_not_exists.md:
##########
@@ -0,0 +1,450 @@
+- Proposal Name: `rename_if_not_exists`
+- Start Date: 2026-06-24
+- RFC PR: [apache/opendal#7818](https://github.com/apache/opendal/pull/7818)
+
+# Summary
+
+Extend rename with an `if_not_exists` option:
+
+```rust
+op.rename_with("staging/file", "published/file")
+    .if_not_exists(true)
+    .await?;
+```
+
+The existing `rename` API keeps its overwrite semantics. When
+`if_not_exists` is enabled, rename succeeds only if the destination does not
+exist. A destination conflict returns `ConditionNotMatch` without modifying the
+source or destination.
+
+# Motivation
+
+OpenDAL defines `rename` as an overwrite operation. Some applications also need
+an atomic publish primitive: move a completed staging file into place only when
+no other writer has already published that destination.
+
+A caller cannot implement this safely with `stat` followed by `rename`. Another
+writer can create the destination after `stat` reports that it is absent but
+before rename runs. A service configuration flag is also unsuitable because it
+would make the meaning of the same `rename` call depend on backend construction
+rather than an explicit call-site condition.
+
+OpenDAL already models the equivalent destination condition for write and copy
+through options:
+
+```rust
+op.write_with("path", content)
+    .if_not_exists(true)
+    .await?;
+
+op.copy_with("source", "target")
+    .if_not_exists(true)
+    .await?;
+```
+
+Rename should follow the same public API and error model.
+
+# Guide-level explanation
+
+Use `rename` when the destination may be replaced:
+
+```rust
+use opendal::{Operator, Result};
+
+async fn replace(op: Operator) -> Result<()> {
+    op.rename("staging/file", "published/file").await?;
+    Ok(())
+}
+```
+
+Use `rename_with(...).if_not_exists(true)` when an existing destination must be
+preserved:
+
+```rust
+use opendal::{ErrorKind, Operator, Result};
+
+async fn publish(op: Operator) -> Result<()> {
+    match op
+        .rename_with("staging/file", "published/file")
+        .if_not_exists(true)
+        .await
+    {
+        Ok(()) => Ok(()),
+        Err(err) if err.kind() == ErrorKind::ConditionNotMatch => Err(err),
+        Err(err) => Err(err),
+    }
+}
+```
+
+The conditional operation has the following outcomes:
+
+- If the destination does not exist, the source is renamed to the destination.
+- If the destination exists, the operation returns `ConditionNotMatch` and
+  leaves both paths unchanged.
+- If the service cannot enforce the destination condition atomically, the
+  operation returns `Unsupported`.
+- If source and destination are the same path, the operation returns
+  `IsSameFile`, matching normal rename.
+
+Users can inspect `Capability::rename_with_if_not_exists` before enabling the
+option.
+
+Blocking users configure the same condition through `RenameOptions`:
+
+```rust
+use opendal::blocking;
+use opendal::options::RenameOptions;
+use opendal::Result;
+
+fn publish(op: blocking::Operator) -> Result<()> {
+    let mut options = RenameOptions::default();
+    options.if_not_exists = true;
+    op.rename_options("staging/file", "published/file", options)?;
+    Ok(())
+}
+```
+
+# Reference-level explanation
+
+## Public API
+
+Add `RenameOptions`:
+
+```rust
+#[derive(Debug, Clone, Default, Eq, PartialEq)]
+pub struct RenameOptions {
+    pub if_not_exists: bool,
+}
+```
+
+The asynchronous operator exposes:
+
+```rust
+impl Operator {
+    pub async fn rename(&self, from: &str, to: &str) -> Result<()>;
+
+    pub fn rename_with(
+        &self,
+        from: &str,
+        to: &str,
+    ) -> FutureRename<impl Future<Output = Result<()>>>;
+
+    pub async fn rename_options(
+        &self,
+        from: &str,
+        to: &str,
+        options: impl Into<RenameOptions>,
+    ) -> Result<()>;
+}
+```
+
+`rename` delegates to `rename_options` with default options. `FutureRename`
+provides:
+
+```rust
+impl<F: Future<Output = Result<()>>> FutureRename<F> {
+    pub fn if_not_exists(self, value: bool) -> Self;
+}
+```
+
+The blocking operator follows existing blocking options APIs:
+
+```rust
+impl blocking::Operator {
+    pub fn rename_options(
+        &self,
+        from: &str,
+        to: &str,
+        options: RenameOptions,
+    ) -> Result<()>;
+}
+```
+
+No standalone `rename_if_not_exists` method is added. The options API matches
+write and copy and leaves room for future composable rename conditions.
+
+The rename API follows the copy API at every layer:
+
+| Layer | Copy | Rename |
+| --- | --- | --- |
+| Default operation | `copy` | `rename` |
+| Fluent options | `copy_with(...).if_not_exists(true)` | 
`rename_with(...).if_not_exists(true)` |
+| Options struct | `CopyOptions` | `RenameOptions` |
+| Explicit options call | `copy_options` | `rename_options` |
+| Raw arguments | `OpCopy::if_not_exists()` | `OpRename::if_not_exists()` |
+| Capability | `copy_with_if_not_exists` | `rename_with_if_not_exists` |
+
+This RFC does not add a public `copy_if_not_exists`-style standalone method,
+because copy itself exposes the condition through its options APIs.
+
+## Service API
+
+`RenameOptions` converts into the raw `OpRename`:
+
+```rust
+pub struct OpRename {
+    if_not_exists: bool,
+}
+```
+
+The `Service::rename` signature does not change. Services inspect
+`OpRename::if_not_exists()` to select their native overwrite or no-overwrite
+operation.
+
+Add a capability field:
+
+```rust
+pub struct Capability {
+    pub rename: bool,
+    pub rename_with_if_not_exists: bool,
+}
+```
+
+`rename_with_if_not_exists` is meaningful only when `rename` is also supported.
+The correctness check returns `Unsupported` before dispatch when the option is
+enabled but the service does not advertise the capability.
+
+## Error semantics
+
+`if_not_exists` is a destination precondition, so a destination conflict maps
+to `ConditionNotMatch`. This is consistent with existing write and copy
+behavior.
+
+Backend errors caused by the native no-replace condition must be translated to
+`ConditionNotMatch`, even when the backend or operating system reports a native
+error such as `AlreadyExists`.
+
+Other errors retain their normal meanings. For example, a missing source
+returns `NotFound`, a directory passed where a file is required returns the
+corresponding directory error, and an unsupported condition returns
+`Unsupported`.
+
+## Atomicity requirement
+
+A service must advertise `rename_with_if_not_exists` only when the destination
+condition and rename are enforced by one native operation. Performing a
+destination check followed by an overwriting rename has a time-of-check to
+time-of-use race and does not satisfy the capability.
+
+When the destination condition fails:
+
+- The source remains at its original path.
+- The destination remains unchanged.
+
+When the operation succeeds:
+
+- The source no longer exists at its original path.
+- The destination contains the source data.
+
+This RFC does not require rename to be crash-atomic beyond the guarantees of 
the
+underlying service. It requires the no-replace decision to be atomic with
+respect to competing destination creation.
+
+## Service analysis
+
+### Audit scope
+
+At the time of this RFC, 19 services advertise `Capability::rename`. The table
+below audits all of them, plus S3 as the representative object store without a
+native rename operation.
+
+The "assessment" column describes whether the current backend primitive could
+support `rename_with_if_not_exists`. It does not enable the capability. A
+backend must still demonstrate that the destination check and move are one
+atomic service operation and map destination conflicts to
+`ConditionNotMatch`.
+
+| Service | Current rename implementation | Assessment for `if_not_exists` |
+| --- | --- | --- |
+| `aliyun_drive` | Deletes the destination, moves by file ID, then updates the 
name | Not suitable as implemented; overwrite is a multi-request workflow |

Review Comment:
   Good catch. The key distinction: overwrite rename does NOT need atomicity 
because its contract is simply "the destination ends up with the source 
content." A multi-step delete+rename achieves this. But if_not_exists IS 
conditional — it must check "destination absent" AND move atomically, or a 
concurrent writer slips in. I have added a paragraph before the service 
analysis section explaining that a backend with non-atomic overwrite may still 
support conditional rename when its native move rejects conflicts atomically. 
HDFS is the concrete example.



-- 
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