select() and select_lol()

Both methods return a chainable query builder for the same typed models, over two different server query formats:

select() (HOL, Hierarchical Object List)

Returns an ECMModelQuerySync / ECMModelQueryAsync. The server answers with an object-based XML tree, which is what carries the hierarchy and every optional extra: file information, base parameters, variants, remarks, child and parent objects.

select_lol() (LOL, Linear Object List)

Returns an ECMModelQueryLolSync / ECMModelQueryLolAsync. The server answers with a compact, column-oriented rowset, which is typically faster for large flat result sets but carries neither the hierarchy nor those extras.

Conditions, sorting, paging, field restriction, full-text search, rights and the hit list transport behave identically in both modes, so everything on this page applies to both unless it is marked HOL only or LOL only.

ECM Model reference — for a full description of all system properties (system.id, system.rights, system.base_params, system.file_properties, etc.) and how change tracking works.

1. Choosing a mode

Table 1. What the two modes can do
Capability select() (HOL) select_lol() (LOL)

Performance

Slower

Faster for large, flat result sets

Sorting

Available

Available

.file_properties()

Available

Not available

.base_params()

Available (structured)

Not available (only via system fields)

.variants()

Available

Not available

.icons()

Available

Ignored (server does not return icons in LOL mode)

.with_children() / .with_parents()

Available

Not available

Table field values

Typed, with row_id

Untyped raw strings, row_id always None

If file information, base parameters, variants, or hierarchical queries are needed, select() is the right choice.

In practice the mode follows from what the result has to carry:

  • Sync

  • Async

# Recommended for: large sets, index data only (no variants/file properties/hierarchy)
for doc in (
    ecm.dms.select_lol(InvoiceDocument)
    .where(InvoiceDocument.Year == 2024)
    .order_by(InvoiceDocument.Title.ASC)
    .pagesize(500)
    .stream()
):
    print(doc.system.id, doc.Title)

# Recommended for: file info, base params, variants, hierarchies → use select()
for doc in (
    ecm.dms.select(InvoiceDocument)
    .where(InvoiceDocument.Year == 2024)
    .file_properties()
    .stream()
):
    print(doc.system.file_properties.extension)
# Recommended for: large sets, index data only (no variants/file properties/hierarchy)
async for doc in (
    ecm.dms.select_lol(InvoiceDocument)
    .where(InvoiceDocument.Year == 2024)
    .order_by(InvoiceDocument.Title.ASC)
    .pagesize(500)
    .stream()
):
    print(doc.system.id, doc.Title)

# Recommended for: file info, base params, variants, hierarchies → use select()
async for doc in (
    ecm.dms.select(InvoiceDocument)
    .where(InvoiceDocument.Year == 2024)
    .file_properties()
    .stream()
):
    print(doc.system.file_properties.extension)

2. Signature

Both builders are created synchronously, in the async variant as well. The await belongs on the terminal call (.execute()) or on the iteration (async for … .stream()).

  • Sync

  • Async

ecm.dms.select(model_class: type[T]) -> ECMModelQuerySync[T]
ecm.dms.select_lol(model_class: type[T]) -> ECMModelQueryLolSync[T]
ecm.dms.select(model_class: type[T]) -> ECMModelQueryAsync[T]
ecm.dms.select_lol(model_class: type[T]) -> ECMModelQueryLolAsync[T]

3. Parameters

Name Type Description

model_class

type[ECMFolderModel | ECMRegisterModel | ECMDocumentModel]

The model class describing the object type. Can also be created dynamically via make_folder_model() / make_register_model() / make_document_model().

4. Query builder methods

The builder methods are chainable. The Mode column says where a method exists:

Method Mode Description

.where(*conditions)

both

Add filter conditions. Multiple arguments are combined with AND. Conditions can be combined with & (AND) and | (OR).

.order_by(*sort_orders)

both

Set the sort order. Argument position determines sort priority. Each argument is an ECMSortOrder created via .ASC / .DESC on an ECMField, including system fields (Model.system.id.DESC).

.limit(n)

both

Maximum total number of results across all pages (LOL: maps to MaxHits).

.pagesize(n)

both

Number of objects per server request (default: 1000). Affects efficiency for large result sets.

.offset(n)

both

Zero-based start offset. Skips the first n results.

.fields(*fields)

both

Return only the specified fields (sets field_schema="MIN"). Index fields and system fields are accepted. Calling without arguments resets the restriction.

.fulltext(term, **engine_options)

both

Add a <Fulltext> condition for the queried object type. Requires a full-text engine configured on the server. Calling multiple times replaces the previous setting.

.garbage_mode()

both

Return only objects from the recycle bin.

.rights()

both

Include access rights data (populates obj.system.rights). The insert quotas are requested along with them, because the server otherwise always reports insert as denied.

.result_as_file()

both

Request the hit list as a response file instead of the XML parameter (Flags=16). Off by default. See the Hit list transport section.

.execute()

both

Execute the query and return all results as a list (all pages in memory).

.stream()

both

Execute the query page by page and return a generator. Recommended for large result sets.

.base_params()

HOL only

Include audit metadata (populates obj.system.base_params): creator, modification date, etc.

.file_properties()

HOL only

Include file properties (populates obj.system.file_properties, documents only).

.variants()

HOL only

Include document variant data.

.remarks()

HOL only

Include remarks for returned objects.

.icons()

HOL only

Include icon IDs for returned objects. In LOL mode the call is ignored, because the server returns no icons there.

.with_children(*specs) / .with_parents(*specs)

HOL only

Switch to a HOL query that includes child or parent objects. Returns an ECMModelQueryHolSync.

.execute() loads all objects into memory before processing begins. With large result sets this can cause memory problems.

.stream() works page by page — but paging in enaio is not transactional. If objects are modified while iterating, pages may contain inconsistent state or objects may appear twice. In that case .execute() should be preferred.

5. Filter conditions (where)

Conditions are passed to .where(). Multiple arguments in a single .where() call are automatically combined with AND. For OR combinations, conditions are joined with the | operator; for AND with &.

Every comparison needs its own parentheses

| and & bind tighter than == in Python, so an unparenthesised combination does not do what it reads like:

# TypeError: unsupported operand type(s) for |: 'str' and 'ECMField'
# Python reads this as: Status == ("Open" | InvoiceFolder.Status) == "In Progress"
.where(InvoiceFolder.Status == "Open" | InvoiceFolder.Status == "In Progress")

# Correct: each comparison in its own parentheses
.where((InvoiceFolder.Status == "Open") | (InvoiceFolder.Status == "In Progress"))

The keywords or and and are not a way around this. Python cannot overload them: they call bool() on an operand and return one of the operands unchanged, so or would evaluate to its first condition and and to its second, dropping the other. Conditions therefore refuse to be used as a truth value:

# TypeError: ECM conditions cannot be combined with 'and', 'or' or 'not' ...
.where((InvoiceFolder.Status == "Open") or (InvoiceFolder.Status == "In Progress"))

# Correct
.where((InvoiceFolder.Status == "Open") | (InvoiceFolder.Status == "In Progress"))

The same applies to not condition and to if condition:. Only | and & build a condition group. For a set of values against one field, .in_() avoids the question entirely.

The condition value must match the declared field type

On a generated model the comparison operators are typed against the field: an ECMField[bool] takes True / False, not "1". A type checker reports the mismatch — for == and != on the .where() call, for <, , >, >= directly on the operator.

.where(InvoiceDocument.Paid == True)   # correct
.where(InvoiceDocument.Paid == "1")    # type error (both spellings do reach the server as 1)

Two calibrations keep working code working: a datetime field also takes a plain date, and a catalog field takes either its generated enum member or the plain string. None and the server-side placeholders (DmsQuerySpecialValue, DmsQueryParamValue, DmsQueryLinkedValue) are accepted on every field.

Dynamic models from model_by_name() / make_*_model() declare no field types, so their conditions are not checked — Model["Paid"] == True and Model["Paid"] == "1" are both accepted. Table-field columns are resolved by name and stay unchecked as well.

5.1. Comparison operators

Syntax Operator Example

Field == value

Equality

InvoiceFolder.Year == 2024

Field != value

Inequality

InvoiceFolder.Status != "Archived"

Field < value

Less than

InvoiceFolder.Year < 2024

Field ⇐ value

Less than or equal

InvoiceFolder.Year ⇐ 2024

Field > value

Greater than

InvoiceFolder.Year > 2020

Field >= value

Greater than or equal

InvoiceFolder.Year >= 2020

Field.in_(v1, v2, …​)

Matches one of the values

InvoiceFolder.Year.in_(2022, 2023, 2024)

Field.not_in(v1, v2, …​)

Matches none of the values

InvoiceFolder.Status.not_in("Open", "Draft")

Field.between(lower, upper)

Between two values (inclusive)

InvoiceFolder.Year.between(2020, 2024)

5.2. AND combinations

Multiple arguments in .where() are combined with AND. The & operator can also be used explicitly:

# Variant 1: multiple arguments → AND
ecm.dms.select(InvoiceFolder).where(
    InvoiceFolder.Year >= 2020,
    InvoiceFolder.Status == "Approved",
)

# Variant 2: explicit & → same result
ecm.dms.select(InvoiceFolder).where(
    (InvoiceFolder.Year >= 2020) & (InvoiceFolder.Status == "Approved")
)

5.3. OR combinations

Conditions are combined into an OR group using |:

ecm.dms.select(InvoiceFolder).where(
    (InvoiceFolder.Status == "Open") | (InvoiceFolder.Status == "In Progress")
)

For value sets, .in_() is more concise:

ecm.dms.select(InvoiceFolder).where(
    InvoiceFolder.Status.in_("Open", "In Progress")
)

5.4. Mixed AND/OR groups

& and | can be nested arbitrarily. Python parentheses control the evaluation order:

  • Sync

  • Async

# (Year >= 2020 AND Year <= 2024) AND (Status = "Open" OR Status = "In Progress")
for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(
        (InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
        InvoiceFolder.Status.in_("Open", "In Progress"),
    )
    .stream()
):
    print(folder.Title, folder.Year)
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(
        (InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
        InvoiceFolder.Status.in_("Open", "In Progress"),
    )
    .stream()
):
    print(folder.Title, folder.Year)

5.5. between()

.between(lower, upper) is a compact alternative to >= + :

  • Sync

  • Async

for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year.between(2020, 2024))
    .stream()
):
    print(folder.Title)
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year.between(2020, 2024))
    .stream()
):
    print(folder.Title)

5.6. Table-field column conditions

The conditions work in both modes. What differs is the result: LOL returns table-field values as untyped raw strings and row.row_id is always None, while HOL returns them typed. Table field values are returned as untyped raw strings and are not converted to the declared Python type. row_id is always None for table rows returned from LOL queries.

Conditions on a table-field column (a column of a sub-table / multi-field) are written by accessing the column on the model’s ECMTableField (e.g. Invoice.Positions). The builder then emits a <TableCondition> / <TableColumn> rather than a flat field condition, so the server filters on the table column instead of rejecting an unknown object field.

# any row whose ArticleNo == "A-100"
ecm.dms.select(Invoice).where(Invoice.Positions.ArticleNo == "A-100").execute()

produces:

<ConditionObject internal_name="Invoice">
    <TableCondition internal_name="Positions">
        <TableColumn internal_name="ArticleNo" operator="=">
            <Value>A-100</Value>
        </TableColumn>
    </TableCondition>
</ConditionObject>

All comparison and collection operators work (==, !=, <, , >, >=, .in_(), .not_in(), .between()), and a table-column condition can be combined with ordinary field conditions in the same .where() call.

To restrict the condition to a single row, subscript the table field with the server row number — Invoice.Positions[3] (the explicit alias Invoice.Positions.row(3) is equivalent):

# Quantity in row 3 == 5
ecm.dms.select(Invoice).where(Invoice.Positions[3].Quantity == 5).execute()
<TableCondition internal_name="Positions" row="3">
    <TableColumn internal_name="Quantity" operator="="><Value>5</Value></TableColumn>
</TableCondition>

Subscripting is class-level query syntax. At instance level obj.Positions[i] is a 0-based index into the loaded rows. Without a row restriction, two separate table-column conditions on the same table mean "some row matches A" and "some row matches B" — possibly different rows. Table-column conditions require a typed model (generated or hand-declared); they are not available on dynamic models created via make_folder_model().

5.7. Combined queries across multiple object types

HOL only. select_lol() offers no equivalent, see Choosing a mode.

Conditions in .where() can reference fields from different object types within the same cabinet. The server returns only those objects where all conditions are satisfied — regardless of which object type a condition refers to.

The following combinations are supported:

  • Search for a document with conditions on fields of the document itself, its register, and its folder.

  • Search for a register or folder that contains a child object matching a given condition.

When registers are nested, only the immediately enclosing register is available; parent registers above it are not.

Search for a document — with conditions on register and folder:

  • Sync

  • Async

from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Register, Unittest_DMS_Document

for doc in (
    ecm.dms.select(Unittest_DMS_Document)
    .where(
        Unittest_DMS_Document.StringField == "Invoice",
        Unittest_DMS_Register.Name == "Incoming invoices",
        Unittest_DMS.Name == "Supplier GmbH",
    )
    .stream()
):
    print(doc.system.id, doc.Name)
async for doc in (
    ecm.dms.select(Unittest_DMS_Document)
    .where(
        Unittest_DMS_Document.StringField == "Invoice",
        Unittest_DMS_Register.Name == "Incoming invoices",
        Unittest_DMS.Name == "Supplier GmbH",
    )
    .stream()
):
    print(doc.system.id, doc.Name)

Search for a folder — that contains a document matching a condition:

  • Sync

  • Async

from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Document

for folder in (
    ecm.dms.select(Unittest_DMS)
    .where(
        Unittest_DMS.Name == "Supplier GmbH",
        Unittest_DMS_Document.StringField == "Invoice",
    )
    .stream()
):
    print(folder.system.id, folder.Name)
async for folder in (
    ecm.dms.select(Unittest_DMS)
    .where(
        Unittest_DMS.Name == "Supplier GmbH",
        Unittest_DMS_Document.StringField == "Invoice",
    )
    .stream()
):
    print(folder.system.id, folder.Name)

5.8. System fields as conditions

Besides index fields, system fields can also be used in .where() (and .order_by()). For this the system attribute is access-aware:

  • Instance (obj.system.id) returns the loaded value.

  • Class (Model.system.id) returns an ECMField usable in conditions like an index field.

This lets you use the same dotted path inside a query. Most common case — load all documents inside a known folder via a cross-type condition on the folder ID (building on the previous section):

  • Sync

  • Async

docs = (
    ecm.dms.select(Unittest_DMS_Document)
    .where(Unittest_DMS.system.id == folder.system.id)  # left: class (ECMField), right: instance value (int)
    .execute()
)
docs = await (
    ecm.dms.select(Unittest_DMS_Document)
    .where(Unittest_DMS.system.id == folder.system.id)
    .execute()
)

The condition is bound to the object type it is called on (Unittest_DMS) and placed into its own ConditionObject like any cross-type condition. System fields automatically receive the system="1" attribute — identical to Unittest_DMS["OBJECT_ID"]:

<ConditionObject internal_name="Unittest_DMS">
    <FieldCondition internal_name="OBJECT_ID" operator="=" system="1">
        <Value>4711</Value>
    </FieldCondition>
</ConditionObject>

Only system fields backed by a single column are queryable. Available on all object types: id, owner_guid, creator, creation_date, last_modifier, last_modified, creation_time, deleted_at, plus the subset system.base_params.{creator, creation_date, modifier, modified_date, links_count, locked_user_id}. Type-dependent: system.folder_id, system.parent_register_id (register); system.folder_id, system.register_id, system.register_type_id, system.system_id, system.foreign_id, system.lock_user_id, system.archivist, system.archiving_date and system.file_properties.{count, size} (document).

Generic alternative — any system field via item access: Model["NAME"] (subscript on the model class) returns an ECMField for any field name. If NAME is a SystemFields member, system="1" is set automatically. This also covers system fields without a .system property (OBJECT_FLAGS, OBJECT_RETENTION, …) and works for dynamic models without declared fields:

ecm.dms.select(Unittest_DMS_Document).where(Unittest_DMS["OBJECT_ID"] == folder.system.id)   # = Unittest_DMS.system.id
ecm.dms.select(Unittest_DMS_Document).where(Unittest_DMS["OBJECT_FLAGS"] == 0)

Item access is untyped (ECMField[str]); Model.system.<field> is typed and discoverable but limited to the curated subset. Both produce the same condition.

Also as a sort field: The same system fields work in .order_by() (HOL and LOL). The result field element automatically receives system="1":

ecm.dms.select(Unittest_DMS_Document).order_by(Unittest_DMS_Document.system.id.DESC)
<Field internal_name="OBJECT_ID" system="1" sortpos="1" sortorder="DESC"/>

Aggregate values without a queryable server column (system.rights, system.name, system.is_modified, system.base_params.version, system.file_properties.extension, etc.) are only available on loaded instances and cannot be used as conditions. Full list of all system properties: ECM model reference.

The per-object-type flavour shown here works in both modes. The archive-wide variant (<FulltextQuery>, hits across several object types) is HOL only.

.fulltext(term) adds a <Fulltext> condition to the ConditionObject of the queried object type. It is an additional restriction and can be combined with .where(…​). Requires a configured full-text engine on the enaio® server, with the object type indexed.

If no search term is supplied or no search engine is configured, the server responds with an error in the <Messages> block of the result XML (e.g. "The full-text search could not be performed because no search text was specified.").

  • Sync

  • Async

for doc in (
    ecm.dms.select(MedicalLetter)
    .fulltext("Meningitis")
    .where(MedicalLetter.SeniorPhysician == "Müller")
    .stream()
):
    print(doc.system.id, doc.Type)
async for doc in (
    ecm.dms.select(MedicalLetter)
    .fulltext("Meningitis")
    .where(MedicalLetter.SeniorPhysician == "Müller")
    .stream()
):
    print(doc.system.id, doc.Type)

6.1. RetrievalWare engine parameters

Optional RetrievalWare engine attributes can be passed as keyword arguments. They only take effect when RetrievalWare is the full-text engine in use on the server. Parameters left as None use the server defaults.

Parameter Server default Description

mode

PATTERN

Search mode. Allowed values: BOOLEAN, PATTERN, CONCEPT.

expansion_level

4

Word expansion level for thesaurus lookups.

fuzzy_spell_half_words

False

Enable fuzzy spelling for half-words.

fuzzy_spell_threshold

0

Similarity threshold for fuzzy spelling.

max_fuzzy_spell

15

Maximum number of fuzzy spelling hits.

max_reg_expr

4

Maximum number of regular expression expansions.

warn_max_reg_expr

False

Emit a warning when the expansion limit is reached.

word_expansion_limit

20

Maximum number of word expansions.

ecm.dms.select(MedicalLetter).fulltext(
    "Meningitis OR Encephalitis",
    mode="BOOLEAN",
    expansion_level=2,
).execute()

7. Sorting

The sort order is set via .order_by(). Each argument is an ECMSortOrder produced by .ASC or .DESC on an ECMField at class level. Argument position determines sort priority.

  • Sync

  • Async

# Primary sort by year descending, secondary by title ascending
for folder in (
    ecm.dms.select(InvoiceFolder)
    .order_by(InvoiceFolder.Year.DESC, InvoiceFolder.Title.ASC)
    .stream()
):
    print(folder.Year, folder.Title)
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .order_by(InvoiceFolder.Year.DESC, InvoiceFolder.Title.ASC)
    .stream()
):
    print(folder.Year, folder.Title)

7.1. Keyset pagination with system fields

.fields() and .order_by() also accept system fields via class-level access (Model.system.<field>, see the model reference). System fields automatically receive the system="1" attribute in the generated <Field> element; a field that is both selected and sorted on produces a single <Field> entry.

This enables, for example, stable keyset pagination over the object ID:

  • Sync

  • Async

cursor: int | None = None
batch = 200
while True:
    query = (
        ecm.dms.select_lol(InvoiceDocument)
        .fields(InvoiceDocument.system.id)
        .order_by(InvoiceDocument.system.id.DESC)
        .limit(batch)
        .pagesize(batch)
    )
    if cursor is not None:
        query = query.where(InvoiceDocument.system.id < cursor)
    docs = query.execute()
    if not docs:
        break
    for doc in docs:
        ...  # process batch
    cursor = docs[-1].system.id
cursor: int | None = None
batch = 200
while True:
    query = (
        ecm.dms.select_lol(InvoiceDocument)
        .fields(InvoiceDocument.system.id)
        .order_by(InvoiceDocument.system.id.DESC)
        .limit(batch)
        .pagesize(batch)
    )
    if cursor is not None:
        query = query.where(InvoiceDocument.system.id < cursor)
    docs = await query.execute()
    if not docs:
        break
    for doc in docs:
        ...  # process batch
    cursor = docs[-1].system.id

8. Pagination

.limit(), .pagesize() and .offset() restrict the result set and control internal page navigation.

Method Default Description

.limit(n)

unlimited

Maximum total number of results across all pages.

.pagesize(n)

1000

Number of objects per server request. Smaller values reduce per-page memory usage but increase the number of requests. Large values can overload the server — see the warning below.

.offset(n)

0

Skip the first n results. Useful for manual page navigation.

  • Sync

  • Async

# Page 3 with 20 entries each (offset = 2 × 20 = 40), sorted by year descending
for folder in (
    ecm.dms.select(InvoiceFolder)
    .order_by(InvoiceFolder.Year.DESC)
    .limit(20)
    .offset(40)
    .stream()
):
    print(folder.system.id, folder.Title)
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .order_by(InvoiceFolder.Year.DESC)
    .limit(20)
    .offset(40)
    .stream()
):
    print(folder.system.id, folder.Title)

A pagesize that is too large can cause the server to crash. Memory consumption per page grows proportionally to the number of objects multiplied by the requested metadata. The combination of large pages with .rights(), .base_params(), .file_properties() or .variants() is particularly critical, as the server performs additional database queries for each object.

As a rule of thumb: keep the default of 1000 and only adjust when a proven performance problem exists — and then reduce rather than increase.

9. Restricting fields (fields)

The restriction applies to index fields. The standard system fields, among them the location fields of the object, stay in the request in both modes, see Folder and register of a hit.

.fields() loads only the specified index fields from the server. The server internally sets field_schema="MIN" and transfers only the explicitly listed fields. System fields (system.id, system.name, etc.) and sort fields are always included regardless of this list.

This significantly reduces the amount of data transferred when only a few fields are needed. Fields that were not requested are None on the returned object.

Fields can be passed as ECMField class attributes or as internal field name strings. Calling without arguments resets the restriction and returns all fields again.

  • Sync

  • Async

# Load only Title and Year — all other fields are None
for folder in (
    ecm.dms.select(InvoiceFolder)
    .fields(InvoiceFolder.Title, InvoiceFolder.Year)
    .stream()
):
    print(folder.Title, folder.Year)
    # folder.Status is None (not loaded)
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .fields(InvoiceFolder.Title, InvoiceFolder.Year)
    .stream()
):
    print(folder.Title, folder.Year)

Fields can alternatively be passed as strings when no typed model is available:

  • Sync

  • Async

from ecmind_blue_client.ecm.model import make_folder_model

InvoiceFolder = make_folder_model("InvoiceFolder")

for folder in (
    ecm.dms.select(InvoiceFolder)
    .fields("Title", "Year")
    .stream()
):
    print(folder["Title"], folder["Year"])
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .fields("Title", "Year")
    .stream()
):
    print(folder["Title"], folder["Year"])

10. Hit list transport

dms.GetResultList returns the hit list in one of two ways: inline as the BASE64 parameter XML (Flags=0), or as a response file (Flags=16). The client asks for the parameter by default because that saves one network round trip per page: the server writes parameter block and result in a single pass, whereas the file variant makes it write twice and the client wait for the 32-byte file header.

Measured against a test server at 20 ms RTT:

Scenario Response file Parameter Difference

First page, pagesize(10)

94.5 ms

53.3 ms

-44 %

1471 rows, pagesize(10)

13,020 ms

8,238 ms

-37 %

1471 rows, pagesize(1000)

339 ms

278 ms

-18 %

Page size matters far more than the transport: the same 1471 rows cost 146 server requests at pagesize(10) and two at pagesize(1000). The default of 1000 is deliberately high, and lowering it is the most expensive mistake available in a list query.

.result_as_file() remains for the case where a single page grows beyond the pool client’s file_cache_byte_limit (32 MiB by default). The RPC layer then spools the response to a temporary file, and .stream() / .execute() read it incrementally from there, so the hit list never has to fit in memory. Below that limit the file is buffered in memory anyway and the option only costs the extra round trip.

# default: inline, one round trip per page
for folder in ecm.dms.select_lol(InvoiceFolder).pagesize(1000).stream():
    ...

# very large single page: via a temporary file, constant memory
for folder in ecm.dms.select_lol(InvoiceFolder).pagesize(200_000).result_as_file().stream():
    ...

.stream() processes each page incrementally and discards every row once its model has been built, which keeps memory flat regardless of page size (measured: 1.2 MB for a 44 MB response document with 74,250 rows, against 223 MB for building the full XML tree). .execute() naturally ends up holding every model, so prefer .stream() for very large result sets.

What an incremental read cannot reach is everything that follows the hits in document order, namely <Statistics> and <Messages>. The query methods never touch those; code that needs them should use DmsContentParser directly.

11. Rights information

.rights() retrieves the access rights of the logged-in user for each object. The rights are available in obj.system.rights as an ECMModelRights instance.

.rights() additionally requests the insert quotas (object_inserts), because the server otherwise reports the insert right as denied for every object. This costs further server queries per object and needs to be accounted for with large pages.

Attribute Description

insert

May create child objects (registers/documents in a folder, documents in a register).

edit_metadata

May change the index fields of the object.

read_file

May read the file of the document.

edit_file

May modify the file of the document.

delete

May delete the object.

  • Sync

  • Async

for folder in ecm.dms.select(InvoiceFolder).rights().stream():
    r = folder.system.rights
    if r.edit_metadata:
        print(f"{folder.system.id}: editing allowed")
    if not r.delete:
        print(f"{folder.system.id}: deletion not permitted")
async for folder in ecm.dms.select(InvoiceFolder).rights().stream():
    r = folder.system.rights
    if r.edit_metadata:
        print(f"{folder.system.id}: editing allowed")
    if not r.delete:
        print(f"{folder.system.id}: deletion not permitted")

12. Base parameters

In LOL mode the same values are reachable as system fields:

.base_params() is not available — obj.system.base_params is always None. Audit metadata is only accessible when the corresponding system fields are declared via ecm_system_fields on the model:

System field Meaning

OBJECT_CRID

Creator ID

OBJECT_CRDATE

Creation date

OBJECT_USERGUID

Owner GUID

OBJECT_MODIFYUSER

Last modifier

OBJECT_MODIFYTIME

Last modification date

OBJECT_LINKS

Link count

OBJECT_TXTNOTICECOUNT

Text note count

HOL only. select_lol() offers no equivalent, see Choosing a mode.

.base_params() retrieves administrative information stored by the server for each object. The data is available in obj.system.base_params as an ECMModelBaseParams instance.

Attribute Description

creator

Username of the creator.

creation_date

Date of creation.

owner

Current owner of the object.

modifier

Username of the last modifier.

modified_date

Timestamp of the last modification.

links_count

Number of links.

text_notice_count

Number of text notices.

  • Sync

  • Async

for folder in ecm.dms.select(InvoiceFolder).base_params().stream():
    bp = folder.system.base_params
    print(f"Created by {bp.creator} on {bp.creation_date}")
    print(f"Last modified by {bp.modifier} on {bp.modified_date}")
async for folder in ecm.dms.select(InvoiceFolder).base_params().stream():
    bp = folder.system.base_params
    print(f"Created by {bp.creator} on {bp.creation_date}")
    print(f"Last modified by {bp.modifier} on {bp.modified_date}")

13. File properties

In LOL mode file information is limited to what the system fields carry:

.file_properties() is not available. Partial file metadata is only accessible via system fields explicitly declared on the model:

System field Meaning

OBJECT_FILESIZE

File size in bytes

OBJECT_COUNT

Number of files

OBJECT_DOCPAGECOUNT

Number of document pages

File extension, MIME type, and MIME type group are never returned by the server in LOL mode.

HOL only. select_lol() offers no equivalent, see Choosing a mode.

.file_properties() retrieves metadata about the file of a document. Only available for ECMDocumentModel types; obj.system.file_properties returns an ECMModelFileProperties instance.

Attribute Description

count

Number of files (primary and secondary files).

size

File size in bytes.

extension

File extension (e.g. pdf, docx).

mimetype

MIME type (e.g. application/pdf).

mimetypegroup

MIME group (e.g. application).

iconid

ID of the file type icon.

documentpagecount

Number of pages (if known).

  • Sync

  • Async

for doc in ecm.dms.select(InvoiceDocument).file_properties().stream():
    fp = doc.system.file_properties
    print(f"{doc.system.id}: {fp.extension}, {fp.size} bytes, {fp.documentpagecount} pages")
async for doc in ecm.dms.select(InvoiceDocument).file_properties().stream():
    fp = doc.system.file_properties
    print(f"{doc.system.id}: {fp.extension}, {fp.size} bytes, {fp.documentpagecount} pages")

14. Variants

HOL only. select_lol() offers no equivalent, see Choosing a mode.

.variants() retrieves the version branches of a document from the W-module. Only relevant for document types with the W-module enabled. The result is available in obj.system.variants as a list of ECMModelDocumentVariant instances.

Each variant has the attributes doc_id, doc_ver, is_active, doc_parent, children and level (nesting depth, 0 for the entries in obj.system.variants).

  • Sync

  • Async

for doc in ecm.dms.select(InvoiceDocument).variants().stream():
    for variant in doc.system.variants:
        active = "✓" if variant.is_active else " "
        print(f"[{active}] {variant.doc_ver} (ID {variant.doc_id})")
async for doc in ecm.dms.select(InvoiceDocument).variants().stream():
    for variant in doc.system.variants:
        active = "✓" if variant.is_active else " "
        print(f"[{active}] {variant.doc_ver} (ID {variant.doc_id})")

15. Remarks

HOL only. select_lol() offers no equivalent, see Choosing a mode.

.remarks() includes the text notices of an object in the response. The data is available in obj.system.remarks.

  • Sync

  • Async

for folder in ecm.dms.select(InvoiceFolder).remarks().stream():
    for remark in folder.system.remarks:
        print(remark)
async for folder in ecm.dms.select(InvoiceFolder).remarks().stream():
    for remark in folder.system.remarks:
        print(remark)

16. Recycle bin

.garbage_mode() restricts the query to deleted objects. Without this call only non-deleted objects are returned.

  • Sync

  • Async

# List all deleted invoice folders
for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
    print(f"Deleted: {folder.system.id} – {folder.Title}")
async for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
    print(f"Deleted: {folder.system.id} – {folder.Title}")

17. Folder and register of a hit

The location system fields of a document (SDSTA_ID, SDREG_ID, SDREG_TYPE) are part of the standard field set of both modes, so they need no .fields() entry and are readable through the system namespace of each hit:

  • Sync

  • Async

for document in ecm.dms.select_lol(InvoiceDocument).stream():
    print(document.system.id, document.system.folder_id, document.system.register_id)
async for document in ecm.dms.select_lol(InvoiceDocument).stream():
    print(document.system.id, document.system.folder_id, document.system.register_id)

That also holds when .fields() narrows the result: it restricts the index fields, while the system block stays as it is. On a register model the equivalent fields are REG_STAID (system.folder_id) and REG_PARID (system.parent_register_id).

The same fields work as conditions and as sort criteria, for example every document filed in one known folder:

  • Sync

  • Async

documents = (
    ecm.dms.select_lol(InvoiceDocument)
    .where(InvoiceDocument.system.folder_id == folder.system.id)
    .order_by(InvoiceDocument.system.id.ASC)
    .execute()
)
documents = await (
    ecm.dms.select_lol(InvoiceDocument)
    .where(InvoiceDocument.system.folder_id == folder.system.id)
    .order_by(InvoiceDocument.system.id.ASC)
    .execute()
)

Note the asymmetry: on the left of the comparison stands class access, which yields an ECMField, on the right an instance value. A system field without a .system property is reachable through item access, InvoiceDocument["SDSTA_ID"].

18. Child objects (with_children)

HOL only. select_lol() offers no equivalent, see Choosing a mode.

.with_children() switches to a HOL query that returns each main object together with its child objects. The call returns an ECMModelQueryHolSync. .limit() must be set before execute() or stream() because HOL responses do not support pagination.

Each result is an ECMHolResult with:

  • .main — the main object (e.g. the folder)

  • .children_of(ECMChildSpec(ChildModel)) — list of child objects of that type

  • Sync

  • Async

from ecmind_blue_client.ecm.model import ECMChildSpec

results = (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year == 2024)
    .with_children(ECMChildSpec(InvoiceDocument))
    .limit(100)
    .execute()
)
for r in results:
    print(f"Folder {r.main.Title}:")
    for doc in r.children_of(ECMChildSpec(InvoiceDocument)):
        print(f"  Document {doc.system.id}")
from ecmind_blue_client.ecm.model import ECMChildSpec

results = await (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year == 2024)
    .with_children(ECMChildSpec(InvoiceDocument))
    .limit(100)
    .execute()
)
for r in results:
    print(f"Folder {r.main.Title}:")
    for doc in r.children_of(ECMChildSpec(InvoiceDocument)):
        print(f"  Document {doc.system.id}")

19. Parent objects (with_parents)

HOL only. select_lol() offers no equivalent, see Choosing a mode.

.with_parents() inverts the response structure: the outermost parent type (first spec) becomes the main object result.main. The originally queried object and all intermediate levels are accessible via .children_of().

Specs are passed from outermost to innermost (e.g. Folder first, then Register).

  • Sync

  • Async

from ecmind_blue_client.ecm.model import ECMChildSpec, ECMParentSpec

results = (
    ecm.dms.select(InvoiceDocument)
    .where(InvoiceDocument.Status == "Approved")
    .with_parents(ECMParentSpec(InvoiceFolder), ECMParentSpec(InvoiceRegister))
    .limit(50)
    .execute()
)
for r in results:
    folder   = r.main                                          # InvoiceFolder
    register = r.children_of(ECMChildSpec(InvoiceRegister))[0]
    doc      = r.children_of(ECMChildSpec(InvoiceDocument))[0]
    print(f"{folder.Title} → {register.Title} → Doc {doc.system.id}")
from ecmind_blue_client.ecm.model import ECMChildSpec, ECMParentSpec

results = await (
    ecm.dms.select(InvoiceDocument)
    .where(InvoiceDocument.Status == "Approved")
    .with_parents(ECMParentSpec(InvoiceFolder), ECMParentSpec(InvoiceRegister))
    .limit(50)
    .execute()
)
for r in results:
    folder   = r.main
    register = r.children_of(ECMChildSpec(InvoiceRegister))[0]
    doc      = r.children_of(ECMChildSpec(InvoiceDocument))[0]
    print(f"{folder.Title} → {register.Title} → Doc {doc.system.id}")

20. Examples

20.1. Basic query with filter and sorting

  • Sync

  • Async

from ecmind_blue_client.ecm.model import ECMFolderModel, ECMField

class InvoiceFolder(ECMFolderModel):
    _internal_name_ = "InvoiceFolder"
    Title: ECMField[str]
    Year:  ECMField[int]

for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year >= 2020)
    .order_by(InvoiceFolder.Year.DESC)
    .stream()
):
    print(folder.system.id, folder.Title)
from ecmind_blue_client.ecm.model import ECMFolderModel, ECMField

class InvoiceFolder(ECMFolderModel):
    _internal_name_ = "InvoiceFolder"
    Title: ECMField[str]
    Year:  ECMField[int]

async for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(InvoiceFolder.Year >= 2020)
    .order_by(InvoiceFolder.Year.DESC)
    .stream()
):
    print(folder.system.id, folder.Title)

20.2. Combined conditions (AND / OR)

  • Sync

  • Async

for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(
        (InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
        InvoiceFolder.Title == "Invoice",
    )
    .stream()
):
    print(folder.Title, folder.Year)
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .where(
        (InvoiceFolder.Year >= 2020) & (InvoiceFolder.Year <= 2024),
        InvoiceFolder.Title == "Invoice",
    )
    .stream()
):
    print(folder.Title, folder.Year)

20.3. Limit, offset and page size

  • Sync

  • Async

# At most 50 results, starting at position 100, in batches of 25 per server request
for folder in (
    ecm.dms.select(InvoiceFolder)
    .limit(50)
    .offset(100)
    .pagesize(25)
    .stream()
):
    print(folder.system.id)
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .limit(50)
    .offset(100)
    .pagesize(25)
    .stream()
):
    print(folder.system.id)

20.4. Rights and audit metadata

  • Sync

  • Async

for folder in (
    ecm.dms.select(InvoiceFolder)
    .rights()
    .base_params()
    .stream()
):
    print(folder.system.rights)
    print(folder.system.base_params)
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .rights()
    .base_params()
    .stream()
):
    print(folder.system.rights)
    print(folder.system.base_params)

20.5. Selective fields

Use .fields() to load only the specified fields from the server. This significantly reduces the amount of data transferred when only a few fields are needed.

  • Sync

  • Async

for folder in (
    ecm.dms.select(InvoiceFolder)
    .fields(InvoiceFolder.Title, InvoiceFolder.Year)
    .stream()
):
    print(folder.Title, folder.Year)
    # folder.OtherField would be None (not loaded)
async for folder in (
    ecm.dms.select(InvoiceFolder)
    .fields(InvoiceFolder.Title, InvoiceFolder.Year)
    .stream()
):
    print(folder.Title, folder.Year)

20.6. Querying the recycle bin

  • Sync

  • Async

# Only deleted objects
for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
    print(folder.system.id, folder.Title)
async for folder in ecm.dms.select(InvoiceFolder).garbage_mode().stream():
    print(folder.system.id, folder.Title)

20.7. Generic model (without a class definition)

When the object type is only known at runtime, a model can be created dynamically:

  • Sync

  • Async

from ecmind_blue_client.ecm.model import make_folder_model

InvoiceFolder = make_folder_model("InvoiceFolder")

for folder in ecm.dms.select(InvoiceFolder).stream():
    print(folder.system.id, folder["Title"])
from ecmind_blue_client.ecm.model import make_folder_model

InvoiceFolder = make_folder_model("InvoiceFolder")

async for folder in ecm.dms.select(InvoiceFolder).stream():
    print(folder.system.id, folder["Title"])

20.8. Combined query across document, register and folder

HOL only. select_lol() offers no equivalent, see Choosing a mode.
  • Sync

  • Async

from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Register, Unittest_DMS_Document

for doc in (
    ecm.dms.select(Unittest_DMS_Document)
    .where(
        Unittest_DMS_Document.StringField == "Invoice",
        Unittest_DMS_Register.Name == "Incoming invoices",
        Unittest_DMS.Name == "Supplier GmbH",
    )
    .stream()
):
    print(doc.system.id, doc.Name)
from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Register, Unittest_DMS_Document

async for doc in (
    ecm.dms.select(Unittest_DMS_Document)
    .where(
        Unittest_DMS_Document.StringField == "Invoice",
        Unittest_DMS_Register.Name == "Incoming invoices",
        Unittest_DMS.Name == "Supplier GmbH",
    )
    .stream()
):
    print(doc.system.id, doc.Name)

20.9. Search for a folder containing a matching child object

HOL only. select_lol() offers no equivalent, see Choosing a mode.
  • Sync

  • Async

from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Document

for folder in (
    ecm.dms.select(Unittest_DMS)
    .where(
        Unittest_DMS.Name == "Supplier GmbH",
        Unittest_DMS_Document.StringField == "Invoice",
    )
    .stream()
):
    print(folder.system.id, folder.Name)
from tests.models.Unittest_DMS import Unittest_DMS, Unittest_DMS_Document

async for folder in (
    ecm.dms.select(Unittest_DMS)
    .where(
        Unittest_DMS.Name == "Supplier GmbH",
        Unittest_DMS_Document.StringField == "Invoice",
    )
    .stream()
):
    print(folder.system.id, folder.Name)

20.10. A flat LOL query

  • Sync

  • Async

for folder in (
    ecm.dms.select_lol(InvoiceFolder)
    .where(InvoiceFolder.Year >= 2020)
    .order_by(InvoiceFolder.Year.DESC)
    .stream()
):
    print(folder.system.id, folder.Title)
async for folder in (
    ecm.dms.select_lol(InvoiceFolder)
    .where(InvoiceFolder.Year >= 2020)
    .order_by(InvoiceFolder.Year.DESC)
    .stream()
):
    print(folder.system.id, folder.Title)