List services in Cluster ERP share three small core modules: core/search.py for text search, core/pagination.py for counts on large tables, and core/bulk_operations.py for bulk deactivation. This page lists their functions and constants. The HTTP parameters that list routes expose are documented on API conventions.
Scope and source¶
- Defined in:
src/cluster_erp/core/search.py,src/cluster_erp/core/pagination.py,src/cluster_erp/core/bulk_operations.py - Count: 7 public functions and 2 constants in
search.py; 3 functions and 1 constant inpagination.py; 2 functions and 2 schemas inbulk_operations.py(read from the files). Usage counts below were made withgrep -rlw <name>oversrc/cluster_erpandmigrations, excluding the defining file.
Text search¶
The module docstring states two problems it solves: user-typed % and _ must be literal in LIKE, and the database collation folds only ASCII letters, so upper-case non-ASCII search terms did not match (src/cluster_erp/core/search.py:1-33).
| Name | Kind | Behaviour | Users | Source |
|---|---|---|---|---|
escape_like(term) |
function | Escapes the escape character, %, and _ |
used by text_match |
src/cluster_erp/core/search.py:58-65 |
NORMALIZED_SQL |
constant | The SQL text of the normalised expression, for migrations that build matching indexes | 3 files | src/cluster_erp/core/search.py:76-82 |
normalized(expr) |
function | translate(lower(expr COLLATE "und-x-icu"),...): Unicode lower case through ICU, and the dotted and dotless i forms folded to i |
used by text_match |
src/cluster_erp/core/search.py:85-88 |
text_match(column, term) |
function | normalized(column) LIKE %normalized(escaped term)% |
69 files | src/cluster_erp/core/search.py:91-97 |
fold(value) |
function | The same normalisation in Python, for lists filtered after the query | used by text_contains |
src/cluster_erp/core/search.py:100-111 |
text_contains(haystack, term) |
function | Python-side substring match with fold |
1 file | src/cluster_erp/core/search.py:114-118 |
FENCE_MAX_MATCHES |
constant | 20,000; above it a search page walks the recency index | used by recency_page |
src/cluster_erp/core/search.py:143 |
execute_page(session, stmt, force_index_walk) |
function | Runs a page query, optionally with bitmap scans turned off for the transaction | 3 files | src/cluster_erp/core/search.py:146-161 |
recency_page(base, entity, order_attr,...) |
function | Builds a newest-first page; with a search and at most FENCE_MAX_MATCHES uncapped matches it filters through a materialised CTE first |
3 files | src/cluster_erp/core/search.py:164-195 |
The docstring states that accent folding is not done on purpose: the unaccent extension is not installed, and ignoring accents would merge distinct letters in some languages (src/cluster_erp/core/search.py:31-33). The literals of the fold are embedded in the SQL text, not bound, so that the expression matches an index built on the same expression (src/cluster_erp/core/search.py:68-74).
Capped counts¶
| Name | Kind | Behaviour | Users | Source |
|---|---|---|---|---|
DEFAULT_COUNT_CAP |
constant | 10,000 | n/a | src/cluster_erp/core/pagination.py:23 |
configure(cap) |
function | Installs the process-wide cap from list_count_cap at start-up; 0 means exact counts |
called by create_app |
src/cluster_erp/core/pagination.py:27-30, src/cluster_erp/server/app.py:1311-1313 |
count_capped(session, base, cap=None, prefer_index=False) |
function | Counts at most cap + 1 rows and returns (total, capped); prefer_index turns sequential scans off for the transaction |
14 files | src/cluster_erp/core/pagination.py:33-69 |
capped_total(session, count_stmt, cap=None, prefer_index=False) |
function | The same for an existing select(func.count(...)) statement |
58 files | src/cluster_erp/core/pagination.py:72-84 |
The docstring states the reason: on a table of millions of rows a full count(*) costs hundreds of milliseconds, while the page itself costs under a millisecond, so the pager shows "10 000+" instead (src/cluster_erp/core/pagination.py:1-13).
Bulk operations¶
| Name | Kind | Behaviour | Source |
|---|---|---|---|
BulkDeleteRequest |
schema | ids: list[UUID] |
src/cluster_erp/core/bulk_operations.py:12-13 |
BulkDeleteResponse |
schema | deleted_count: int |
src/cluster_erp/core/bulk_operations.py:16-17 |
bulk_soft_delete(session, model_class, ids, deleted_by=None) |
function | Loads the live rows and sets deleted_at (and deleted_by) on each through the ORM; returns the count |
src/cluster_erp/core/bulk_operations.py:20-55 |
orm_delete_where(session, model_class, *where) |
function | Physically deletes matching rows through the ORM and flushes; for child rows only | src/cluster_erp/core/bulk_operations.py:58-75 |
Both functions go through the ORM so that the change-history hook records every row (src/cluster_erp/core/bulk_operations.py:26-34, src/cluster_erp/core/bulk_operations.py:59-66). See soft delete and deactivation.
Constraints and validation¶
SET LOCALandRESETchange planner settings only for the current transaction (src/cluster_erp/core/pagination.py:60-66,src/cluster_erp/core/search.py:155-161).recency_pageappliesLIMITandOFFSETonly whenlimitis greater than 0 (src/cluster_erp/core/search.py:193-194).
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| capped count | count_capped |
A row count that stops at a configured cap and reports that it stopped. | tavanlı sayım | src/cluster_erp/core/pagination.py:33 |
| text match | text_match |
A case-insensitive, language-independent substring filter. | metin eşleşmesi | src/cluster_erp/core/search.py:91 |
Related pages¶
Verified against commit 92f8051 on 2026-11-13.