diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/search/ClickHouseSearchIndex.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/search/ClickHouseSearchIndex.java index ed3ff4a3..246557c1 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/search/ClickHouseSearchIndex.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/search/ClickHouseSearchIndex.java @@ -81,13 +81,24 @@ public class ClickHouseSearchIndex implements SearchIndex { String sortColumn = SORT_FIELD_MAP.getOrDefault(request.sortField(), "start_time"); String sortDir = "asc".equalsIgnoreCase(request.sortDir()) ? "ASC" : "DESC"; + // Composite-cursor callers (afterExecutionId set) need a deterministic tiebreak inside + // same-millisecond groups so the client-side last-row pick matches ClickHouse's row order. + // Without this, a same-start_time tail >LIMIT can silently drop rows: the page ends mid-ms, + // the cursor advances past the returned lastRowId, and the skipped rows with smaller + // execution_id values never reappear. Other callers (UI/stats) keep the unchanged + // single-column ORDER BY — they don't use the composite cursor. + String orderBy = sortColumn + " " + sortDir; + if (request.afterExecutionId() != null) { + orderBy += ", execution_id " + sortDir; + } + String dataSql = "SELECT execution_id, route_id, instance_id, application_id, " + "status, start_time, end_time, duration_ms, correlation_id, " + "error_message, error_stacktrace, diagram_content_hash, attributes, " + "has_trace_data, is_replay, " + "input_body, output_body, input_headers, output_headers, root_cause_message " + "FROM executions FINAL WHERE " + whereClause - + " ORDER BY " + sortColumn + " " + sortDir + + " ORDER BY " + orderBy + " LIMIT ? OFFSET ?"; List dataParams = new ArrayList<>(params);