дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(database): search and pagination on result query (#1432)
Adds a search bar to query result grids (match case / words / regex, previous/next occurrence, Filter rows) and a pager for console results (first/previous/next/last, page size). Later pages re-run the read-only query with a skip, which every driver reads past without decoding, so only one page crosses IPC; Last counts rows with the statement wrapped so a trailing line comment can't swallow the closing parenthesis. Closes #1431 Co-authored-by: kidp2h <[email protected]>
Цей коміт міститься в:
1 джерело
7856fabbfe
коміт
75c4ca0be8
22 files changed
+973
-97
No files matched your search
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Pure helpers of the result views: IDEA's local filter and client-side sort
|
||||
* of loaded rows, the Aggregates of a selection, and the series of the chart.
|
||||
* of loaded rows, search in the grid, paging of console results, the
|
||||
* Aggregates of a selection, and the series of the chart.
|
||||
*/
|
||||
import type { Cell } from "./model";
|
||||
|
||||
@@ -192,3 +193,114 @@ export function niceTicks(min: number, max: number, count = 5): number[] {
|
||||
if (out.at(-1)! < hi) out.push(Number((out.at(-1)! + step).toPrecision(12)));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** IDEA's search options of the grid's search bar. */
|
||||
export interface SearchOptions {
|
||||
caseSensitive: boolean;
|
||||
wholeWord: boolean;
|
||||
regex: boolean;
|
||||
}
|
||||
|
||||
const REGEX_SPECIALS = /[.*+?^${}()|[\]\\]/g;
|
||||
const WORD_CHAR = /[\p{L}\p{N}_$]/u;
|
||||
|
||||
/** A test of a cell's text against `query`; `null` without a query, `"invalid"` for a bad regex. */
|
||||
export function cellMatcher(query: string, options: SearchOptions): ((text: string) => boolean) | null | "invalid" {
|
||||
if (!query) return null;
|
||||
const source = options.regex ? query : query.replace(REGEX_SPECIALS, "\\$&");
|
||||
let re: RegExp;
|
||||
try {
|
||||
re = new RegExp(source, options.caseSensitive ? "gu" : "giu");
|
||||
} catch {
|
||||
return "invalid";
|
||||
}
|
||||
// Words: a match with no word character on either side (no lookbehind: older WebKit lacks it).
|
||||
return (text) => {
|
||||
re.lastIndex = 0;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(text))) {
|
||||
const start = m.index;
|
||||
const end = start + m[0].length;
|
||||
if (end === start) {
|
||||
// Step a whole code point: with the `u` flag a lastIndex inside a surrogate pair
|
||||
// snaps back to the pair's start, which would match the same empty string forever.
|
||||
re.lastIndex = start + ((text.codePointAt(start) ?? 0) > 0xffff ? 2 : 1);
|
||||
if (!options.wholeWord) return true;
|
||||
continue;
|
||||
}
|
||||
if (!options.wholeWord || (!WORD_CHAR.test(text[start - 1] ?? "") && !WORD_CHAR.test(text[end] ?? "")))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/** The cells matching `test`, row by row; `row` is the position in `rows` (display order). NULL never matches. */
|
||||
export function findCells(
|
||||
rows: readonly number[],
|
||||
colCount: number,
|
||||
value: (row: number, col: number) => Cell,
|
||||
test: (text: string) => boolean,
|
||||
): Array<{ row: number; col: number }> {
|
||||
const out: Array<{ row: number; col: number }> = [];
|
||||
rows.forEach((r, row) => {
|
||||
for (let col = 0; col < colCount; col++) {
|
||||
const v = value(r, col);
|
||||
if (v !== null && test(v)) out.push({ row, col });
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Whether any cell of loaded row `r` matches `test`. */
|
||||
export function rowMatches(
|
||||
r: number,
|
||||
colCount: number,
|
||||
value: (row: number, col: number) => Cell,
|
||||
test: (text: string) => boolean,
|
||||
): boolean {
|
||||
for (let col = 0; col < colCount; col++) {
|
||||
const v = value(r, col);
|
||||
if (v !== null && test(v)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface PageState {
|
||||
/** Zero-based page shown. */
|
||||
page: number;
|
||||
pageSize: number;
|
||||
/** Rows on the page. */
|
||||
rows: number;
|
||||
/** More rows follow the page. */
|
||||
hasMore: boolean;
|
||||
/** Counted rows of the whole query, when known. */
|
||||
total: number | null;
|
||||
}
|
||||
|
||||
export interface PageWindow {
|
||||
/** One-based numbers of the first and last row shown (0 and 0 on an empty page). */
|
||||
first: number;
|
||||
last: number;
|
||||
/** Rows of the whole query: known once counted or on reaching the end. */
|
||||
total: number | null;
|
||||
/** Zero-based last page, when the total is known. */
|
||||
lastPage: number | null;
|
||||
}
|
||||
|
||||
/** What the pager shows ("1-500 of 501+"). */
|
||||
export function pageWindow({ page, pageSize, rows, hasMore, total }: PageState): PageWindow {
|
||||
const offset = page * pageSize;
|
||||
const known = total ?? (hasMore ? null : offset + rows);
|
||||
return {
|
||||
first: rows ? offset + 1 : 0,
|
||||
last: offset + rows,
|
||||
total: known,
|
||||
lastPage: known === null ? null : Math.max(0, Math.ceil(known / pageSize) - 1),
|
||||
};
|
||||
}
|
||||
|
||||
/** A page is read by running the query again: the backend reads past the rows before it and returns only the page. */
|
||||
export function pageFetch(page: number, pageSize: number): { maxRows: number; skip: number } {
|
||||
return { maxRows: pageSize, skip: page * pageSize };
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача