= upsert()
:description: Fluent upsert builder (ECMModelUpsertSync/Async) backed by dms.XMLImport. Configurable via search(), action0/1/multiple(), and files(). Returns (id, type_id, hits, action).
:keywords: upsert, XMLImport, ECMModelUpsertSync, ECMModelUpsertAsync, search, action0, action1, insert, update
:page-tags: api, dms

`upsert()` returns a fluent builder that wraps `dms.XMLImport`.
The server first searches for an existing object using the conditions configured via `.search()` and
then performs the configured action based on the number of results.

== Signature

[tabs]
====
Sync::
+
[source,python]
----
ecm.dms.upsert(
    model: T,
    folder_id: int | None = None,
    register_id: int | None = None,
    *,
    check_mandatory: bool = True,
) -> ECMModelUpsertSync[T]
----

Async::
+
[source,python]
----
ecm.dms.upsert(
    model: T,
    folder_id: int | None = None,
    register_id: int | None = None,
    *,
    check_mandatory: bool = True,
) -> ECMModelUpsertAsync[T]
----
====

== Parameters

[cols="1,1,3"]
|===
|Parameter |Default |Description

|`model`
|—
|Populated model instance (subclass of `ECMFolderModel`, `ECMRegisterModel`, or `ECMDocumentModel`) whose field values are written to the server.

|`folder_id`
|`None`
|ID of the parent folder. Only relevant for `ECMRegisterModel` and `ECMDocumentModel`.

|`register_id`
|`None`
|ID of the parent register. For `ECMDocumentModel`: the register the document is filed in. For `ECMRegisterModel`: the parent register.

|`check_mandatory`
|`True`
|When `.execute()` is called, validates that all fields declared with `mandatory=True` have a non-`None` value. When `False`, skips the client-side check and sends `CHECKOBLIGATION=0` to the server.
|===

== Builder methods

=== `.search(*conditions)`

Defines the search conditions for the server-side duplicate check.

[IMPORTANT]
====
Only the `field_name` and `value` of each condition are used — operators are ignored.
The server always performs equality matching in the `<Search>` section.
====

[source,python]
----
.search(InvoiceFolder.Title == "Invoice 2024", InvoiceFolder.Year == 2024)
----

=== `.action0(value)`, `.action1(value)`, `.action_multiple(value)`

Configures the action depending on the number of search results:

[cols="1,1,1,3"]
|===
|Method |Result count |Default |Valid values

|`.action0(value)`
|0 results
|`"INSERT"`
|`"INSERT"`, `"NONE"`, `"ERROR"`

|`.action1(value)`
|exactly 1 result
|`"UPDATE"`
|`"UPDATE"`, `"INSERT"`, `"NONE"`

|`.action_multiple(value)`
|more than 1 result
|`"ERROR"`
|`"ERROR"`, `"UPDATE"`, `"INSERT"`, `"NONE"`
|===

Meaning of action values:

`"INSERT"`:: Create a new object.
`"UPDATE"`:: Update the found object with the field values from `model`.
`"NONE"`:: Perform no action. `object_id` and `object_type_id` are `-1`.
`"ERROR"`:: Abort with an error.

=== `.files(value, replace=True)`

Attaches files to the upsert operation. Only valid for `ECMDocumentModel`.

[cols="1,1,3"]
|===
|Parameter |Default |Description

|`value`
|—
|`list[JobRequestFile]` — list of files to attach.

|`replace`
|`True`
|When `True`, uploaded files replace existing files on the document (`REPLACEFILES=1`). When `False`, files are appended.
|===

[NOTE]
====
Calling `.files()` on a non-document model raises `TypeError`.
====

=== `.execute()`

Executes the upsert and returns a 4-tuple:

[cols="1,1,3"]
|===
|Element |Type |Description

|`object_id`
|`int`
|ID of the inserted or updated object. `-1` on error or when no action was performed.

|`object_type_id`
|`int`
|Object type ID. `-1` on error.

|`hits`
|`int`
|Number of objects found by the server-side search.

|`action`
|`str`
|Action actually performed: `"INSERT"`, `"UPDATE"`, `"NONE"`, or `"ERROR"`.
|===

== Exceptions

`ValueError`:: A mandatory field has no value and `check_mandatory=True`.
`TypeError`:: `.files()` was called for a non-`ECMDocumentModel`.

== Examples

=== Basic folder upsert

Inserts a folder or updates it if it already exists:

[tabs]
====
Sync::
+
[source,python]
----
object_id, type_id, hits, action = (
    ecm.dms.upsert(InvoiceFolder(Title="Invoice 2024", Year=2024))
    .search(InvoiceFolder.Title == "Invoice 2024")
    .execute()
)
print(action)  # "INSERT" or "UPDATE"
----

Async::
+
[source,python]
----
object_id, type_id, hits, action = await (
    ecm.dms.upsert(InvoiceFolder(Title="Invoice 2024", Year=2024))
    .search(InvoiceFolder.Title == "Invoice 2024")
    .execute()
)
print(action)  # "INSERT" or "UPDATE"
----
====

=== Insert only, never update

Using `action1("NONE")` skips the action when an existing object is found:

[tabs]
====
Sync::
+
[source,python]
----
object_id, type_id, hits, action = (
    ecm.dms.upsert(InvoiceFolder(Title="Invoice 2024", Year=2024))
    .search(InvoiceFolder.Title == "Invoice 2024")
    .action1("NONE")
    .execute()
)
----

Async::
+
[source,python]
----
object_id, type_id, hits, action = await (
    ecm.dms.upsert(InvoiceFolder(Title="Invoice 2024", Year=2024))
    .search(InvoiceFolder.Title == "Invoice 2024")
    .action1("NONE")
    .execute()
)
----
====

=== Register with location

[tabs]
====
Sync::
+
[source,python]
----
object_id, type_id, hits, action = (
    ecm.dms.upsert(
        InvoiceRegister(Name="2024"),
        folder_id=42,
    )
    .search(InvoiceRegister.Name == "2024")
    .execute()
)
----

Async::
+
[source,python]
----
object_id, type_id, hits, action = await (
    ecm.dms.upsert(
        InvoiceRegister(Name="2024"),
        folder_id=42,
    )
    .search(InvoiceRegister.Name == "2024")
    .execute()
)
----
====

=== Document with file attachment

[tabs]
====
Sync::
+
[source,python]
----
from datetime import date
from ecmind_blue_client.rpc import JobRequestFile

object_id, type_id, hits, action = (
    ecm.dms.upsert(
        InvoiceDocument(Title="Invoice 2024", InvoiceDate=date(2024, 3, 1)),
        folder_id=42,
    )
    .search(InvoiceDocument.Title == "Invoice 2024")
    .files([JobRequestFile("invoice.pdf", pdf_bytes)], replace=True)
    .execute()
)
----

Async::
+
[source,python]
----
from datetime import date
from ecmind_blue_client.rpc import JobRequestFile

object_id, type_id, hits, action = await (
    ecm.dms.upsert(
        InvoiceDocument(Title="Invoice 2024", InvoiceDate=date(2024, 3, 1)),
        folder_id=42,
    )
    .search(InvoiceDocument.Title == "Invoice 2024")
    .files([JobRequestFile("invoice.pdf", pdf_bytes)], replace=True)
    .execute()
)
----
====

=== Disable mandatory field check

[tabs]
====
Sync::
+
[source,python]
----
object_id, type_id, hits, action = (
    ecm.dms.upsert(
        InvoiceFolder(Year=2024),
        check_mandatory=False,
    )
    .search(InvoiceFolder.Year == 2024)
    .execute()
)
----

Async::
+
[source,python]
----
object_id, type_id, hits, action = await (
    ecm.dms.upsert(
        InvoiceFolder(Year=2024),
        check_mandatory=False,
    )
    .search(InvoiceFolder.Year == 2024)
    .execute()
)
----
====

== See also

* xref:api/dms/insert.adoc[insert()] — Simple insert without duplicate check
* xref:api/dms/update.adoc[update()] — Simple update without duplicate check
* xref:api/dms/select.adoc[select()] — Query objects with a typed model
