feat(grid): add database value filter
This commit is contained in:
parent
190b5b4ad8
commit
4c48263793
|
|
@ -92,7 +92,7 @@ import { buildTableSelectSql, quoteTableIdentifier } from "@/lib/tableSelectSql"
|
|||
import { uuid } from "@/lib/utils";
|
||||
import { resolveHeaderColumnType } from "@/lib/dataGridColumnType";
|
||||
import { canEditExistingTableRows, canUseKeylessRowPredicate, hiveTablePropertiesIndicateTransactional, isClickHouseExistingRowReadonlyColumn, isHiddenGridColumn, isTdengineExistingRowReadonlyColumn, usesSyntheticRowIdKey } from "@/lib/tableEditing";
|
||||
import { buildDataGridContextFilterCondition, buildDataGridCountSql, buildHiveTablePropertiesSql, type DataGridContextFilterMode } from "@/lib/dataGridSql";
|
||||
import { buildDataGridColumnDistinctValuesSql, buildDataGridContextFilterCondition, buildDataGridCountSql, buildHiveTablePropertiesSql, type DataGridContextFilterMode } from "@/lib/dataGridSql";
|
||||
import {
|
||||
buildVisibleTransposeRows,
|
||||
nextAppendedTransposeState,
|
||||
|
|
@ -125,7 +125,7 @@ import { CANVAS_DATA_GRID_ROW_HEIGHT, drawCanvasDataGrid } from "@/lib/canvasDat
|
|||
import { dataGridSaveActionMode, dataGridSaveToolbarState } from "@/lib/dataGridSaveUi";
|
||||
import { EDITOR_FONT_FAMILY_CSS_VAR } from "@/lib/editorThemes";
|
||||
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/safeStorage";
|
||||
import { appendColumnValueFilterCondition, buildColumnValueFilterCondition, combineWhereInputs, filterModeNeedsValue, parseFilterValue } from "@/lib/dataGridColumnFilter";
|
||||
import { appendColumnValueFilterCondition, buildColumnValueFilterCondition, buildColumnValuesFilterCondition, combineWhereInputs, filterModeNeedsValue, parseFilterValue } from "@/lib/dataGridColumnFilter";
|
||||
import { clampSearchSplitWidth } from "@/lib/dataGridSearchSplit";
|
||||
import { MAX_RESULT_PAGE_SIZE, MIN_RESULT_PAGE_SIZE, normalizeResultPageSize, resultPageSizeMenuOptions } from "@/lib/paginationPageSize";
|
||||
import { allNullColumnIndexes, filterColumnVisibilityOptions, hiddenColumnIndexesWithAllNullColumns, invertedHiddenColumnIndexes, nextHiddenColumnIndexes, removeAutoHiddenColumnIndexes, visibleColumnIndexesForFilter } from "@/lib/dataGridColumnVisibility";
|
||||
|
|
@ -521,9 +521,19 @@ const orderBySuggestionStyle = computed(() => ({
|
|||
top: `${orderBySuggestionPosition.value.top}px`,
|
||||
}));
|
||||
|
||||
type LocalFilterMode = "local" | "server";
|
||||
type LocalFilterOption = {
|
||||
key: string;
|
||||
label: string;
|
||||
count: number | null;
|
||||
value: CellValue;
|
||||
};
|
||||
|
||||
type LocalColumnFilterDraft = {
|
||||
columnIndex: number;
|
||||
values: Set<string>;
|
||||
mode: LocalFilterMode;
|
||||
touched: boolean;
|
||||
};
|
||||
|
||||
type FilterMode = DataGridContextFilterMode;
|
||||
|
|
@ -544,6 +554,15 @@ const headerSortMenuOpenColumn = ref<number | null>(null);
|
|||
const headerPanelDismissGuardUntil = ref(0);
|
||||
const localFilterSearch = ref("");
|
||||
const localFilterDraft = ref<LocalColumnFilterDraft | null>(null);
|
||||
const SERVER_COLUMN_FILTER_LIMIT = 1000;
|
||||
const SERVER_COLUMN_FILTER_DEBOUNCE_MS = 300;
|
||||
const serverFilterLoading = ref(false);
|
||||
const serverFilterError = ref("");
|
||||
const serverFilterOptions = ref<LocalFilterOption[]>([]);
|
||||
const serverFilterLimited = ref(false);
|
||||
const serverFilterValueByKey = ref<Map<string, CellValue>>(new Map());
|
||||
let serverFilterRequestId = 0;
|
||||
let serverFilterSearchTimer: ReturnType<typeof window.setTimeout> | undefined;
|
||||
const filterBuilderOpen = ref(false);
|
||||
const filterModeOptions: Array<{ value: FilterMode; labelKey: string }> = [
|
||||
{ value: "equals", labelKey: "grid.filterBuilderEquals" },
|
||||
|
|
@ -635,13 +654,13 @@ const localFilteredRows = computed(() => {
|
|||
return indices;
|
||||
});
|
||||
|
||||
function buildLocalFilterOptions(columnIndex: number) {
|
||||
const byKey = new Map<string, { key: string; label: string; count: number; value: CellValue }>();
|
||||
function buildLocalFilterOptions(columnIndex: number): LocalFilterOption[] {
|
||||
const byKey = new Map<string, LocalFilterOption>();
|
||||
const addValue = (value: CellValue) => {
|
||||
const key = localFilterKey(value);
|
||||
const current = byKey.get(key);
|
||||
if (current) {
|
||||
current.count += 1;
|
||||
current.count = (current.count ?? 0) + 1;
|
||||
} else {
|
||||
byKey.set(key, { key, label: localFilterLabel(value, columnIndex), count: 1, value });
|
||||
}
|
||||
|
|
@ -662,12 +681,14 @@ function buildLocalFilterOptions(columnIndex: number) {
|
|||
}
|
||||
|
||||
const localFilterAllOptions = computed(() => {
|
||||
if (localFilterDraft.value?.mode === "server") return serverFilterOptions.value;
|
||||
const columnIndex = localFilterDraft.value?.columnIndex;
|
||||
if (columnIndex === undefined) return [];
|
||||
return buildLocalFilterOptions(columnIndex);
|
||||
});
|
||||
|
||||
const localFilterOptions = computed(() => {
|
||||
if (localFilterDraft.value?.mode === "server") return serverFilterOptions.value;
|
||||
const query = localFilterSearch.value.trim().toLowerCase();
|
||||
return localFilterAllOptions.value.filter((option) => !query || option.label.toLowerCase().includes(query)).slice(0, 500);
|
||||
});
|
||||
|
|
@ -695,14 +716,119 @@ const canApplyTypedLocalFilterValue = computed(() => {
|
|||
return !localFilterAllOptions.value.some((option) => option.label.toLowerCase() === normalized);
|
||||
});
|
||||
|
||||
function openLocalFilter(colIdx: number) {
|
||||
function openLocalFilter(colIdx: number, requestedMode: LocalFilterMode = "local") {
|
||||
localFilterSearch.value = "";
|
||||
const allKeys = buildLocalFilterOptions(colIdx).map((option) => option.key);
|
||||
const mode: LocalFilterMode = requestedMode === "server" && canUseServerColumnFilter.value ? "server" : "local";
|
||||
const allKeys = mode === "server" ? [] : buildLocalFilterOptions(colIdx).map((option) => option.key);
|
||||
localFilterDraft.value = {
|
||||
columnIndex: colIdx,
|
||||
values: new Set(localColumnFilters.value[colIdx] ?? allKeys),
|
||||
values: new Set(mode === "server" ? allKeys : (localColumnFilters.value[colIdx] ?? allKeys)),
|
||||
mode,
|
||||
touched: false,
|
||||
};
|
||||
localFilterOpenColumn.value = colIdx;
|
||||
if (mode === "server") {
|
||||
resetServerFilterState();
|
||||
void loadServerFilterValues(colIdx, "");
|
||||
} else {
|
||||
resetServerFilterState();
|
||||
}
|
||||
}
|
||||
|
||||
function resetServerFilterState() {
|
||||
serverFilterRequestId++;
|
||||
if (serverFilterSearchTimer !== undefined) {
|
||||
window.clearTimeout(serverFilterSearchTimer);
|
||||
serverFilterSearchTimer = undefined;
|
||||
}
|
||||
serverFilterLoading.value = false;
|
||||
serverFilterError.value = "";
|
||||
serverFilterOptions.value = [];
|
||||
serverFilterLimited.value = false;
|
||||
serverFilterValueByKey.value = new Map();
|
||||
}
|
||||
|
||||
function serverFilterOptionFromRow(row: QueryResult["rows"][number], columnIndex: number): LocalFilterOption {
|
||||
const value = (row[0] ?? null) as CellValue;
|
||||
const countValue = Number(row[1]);
|
||||
const count = Number.isFinite(countValue) ? countValue : null;
|
||||
return {
|
||||
key: localFilterKey(value),
|
||||
label: localFilterLabel(value, columnIndex),
|
||||
count,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
function serverFilterOptionsFromResult(result: QueryResult, columnIndex: number): LocalFilterOption[] {
|
||||
const byKey = new Map<string, LocalFilterOption>();
|
||||
for (const row of result.rows) {
|
||||
const option = serverFilterOptionFromRow(row, columnIndex);
|
||||
const current = byKey.get(option.key);
|
||||
if (current) {
|
||||
current.count = (current.count ?? 0) + (option.count ?? 0);
|
||||
} else {
|
||||
byKey.set(option.key, option);
|
||||
}
|
||||
}
|
||||
return [...byKey.values()];
|
||||
}
|
||||
|
||||
function syncServerFilterDraft(columnIndex: number, options: LocalFilterOption[]) {
|
||||
const draft = localFilterDraft.value;
|
||||
if (!draft || draft.mode !== "server" || draft.columnIndex !== columnIndex) return;
|
||||
if (draft.touched) return;
|
||||
localFilterDraft.value = {
|
||||
...draft,
|
||||
values: new Set(options.map((option) => option.key)),
|
||||
};
|
||||
}
|
||||
|
||||
async function loadServerFilterValues(columnIndex: number, searchValue: string) {
|
||||
if (!canUseServerColumnFilter.value || !props.connectionId) return;
|
||||
const columnName = props.result.columns[columnIndex];
|
||||
if (!columnName) return;
|
||||
const requestId = ++serverFilterRequestId;
|
||||
serverFilterLoading.value = true;
|
||||
serverFilterError.value = "";
|
||||
serverFilterLimited.value = false;
|
||||
try {
|
||||
const tableMeta = await waitForTableMeta();
|
||||
if (!tableMeta) return;
|
||||
const columnInfo = tableMeta.columns.find((column) => column.name === columnName);
|
||||
const sql = await buildDataGridColumnDistinctValuesSql({
|
||||
databaseType: resolvedDatabaseType.value,
|
||||
schema: tableMeta.schema,
|
||||
tableName: tableMeta.tableName,
|
||||
columnName,
|
||||
columnInfo,
|
||||
whereInput: currentWhereInput(),
|
||||
searchValue: searchValue.trim() || undefined,
|
||||
limit: SERVER_COLUMN_FILTER_LIMIT,
|
||||
includeCounts: true,
|
||||
});
|
||||
const result = await api.executeQuery(props.connectionId, props.database ?? "", sql, tableMeta.schema ?? props.schema, undefined, {
|
||||
maxRows: SERVER_COLUMN_FILTER_LIMIT,
|
||||
fetchSize: SERVER_COLUMN_FILTER_LIMIT,
|
||||
pageSize: SERVER_COLUMN_FILTER_LIMIT,
|
||||
});
|
||||
if (requestId !== serverFilterRequestId || localFilterOpenColumn.value !== columnIndex) return;
|
||||
const options = serverFilterOptionsFromResult(result, columnIndex);
|
||||
const nextValueByKey = new Map(serverFilterValueByKey.value);
|
||||
for (const option of options) nextValueByKey.set(option.key, option.value);
|
||||
serverFilterValueByKey.value = nextValueByKey;
|
||||
serverFilterOptions.value = options;
|
||||
serverFilterLimited.value = result.truncated === true || result.rows.length >= SERVER_COLUMN_FILTER_LIMIT;
|
||||
syncServerFilterDraft(columnIndex, options);
|
||||
} catch (e: any) {
|
||||
if (requestId !== serverFilterRequestId) return;
|
||||
serverFilterOptions.value = [];
|
||||
serverFilterError.value = String(e?.message || e);
|
||||
} finally {
|
||||
if (requestId === serverFilterRequestId) {
|
||||
serverFilterLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function guardHeaderPanelDismiss() {
|
||||
|
|
@ -713,13 +839,13 @@ function shouldIgnoreHeaderPanelClose(columnIndex: number, openColumn: number |
|
|||
return compactColumnHeaderActions.value && openColumn === columnIndex && Date.now() < headerPanelDismissGuardUntil.value;
|
||||
}
|
||||
|
||||
function openCompactLocalFilter(colIdx: number) {
|
||||
function openCompactLocalFilter(colIdx: number, mode: LocalFilterMode = "local") {
|
||||
headerActionMenuOpenColumn.value = null;
|
||||
guardHeaderPanelDismiss();
|
||||
nextTick(() => {
|
||||
window.setTimeout(() => {
|
||||
guardHeaderPanelDismiss();
|
||||
openLocalFilter(colIdx);
|
||||
openLocalFilter(colIdx, mode);
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
|
@ -737,20 +863,35 @@ function compactColumnActionMenuItems(columnName: string) {
|
|||
value: "localFilter",
|
||||
icon: Filter,
|
||||
},
|
||||
...(canUseServerColumnFilter.value
|
||||
? [
|
||||
{
|
||||
label: t("grid.databaseValueFilter"),
|
||||
value: "serverFilter",
|
||||
icon: Database,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
||||
function columnFilterPanelTitle(columnName: string): string {
|
||||
return localFilterDraft.value?.mode === "server" ? t("grid.databaseValueFilterFor", { column: columnName }) : t("grid.localFilterFor", { column: columnName });
|
||||
}
|
||||
|
||||
function selectCompactColumnAction(value: string, columnIndex: number) {
|
||||
if (value === "formatter") {
|
||||
openCompactColumnFormatter(columnIndex);
|
||||
} else if (value === "localFilter") {
|
||||
openCompactLocalFilter(columnIndex);
|
||||
openCompactLocalFilter(columnIndex, "local");
|
||||
} else if (value === "serverFilter") {
|
||||
openCompactLocalFilter(columnIndex, "server");
|
||||
}
|
||||
}
|
||||
|
||||
function handleLocalFilterOpenChange(value: boolean, columnIndex: number) {
|
||||
if (value) {
|
||||
openLocalFilter(columnIndex);
|
||||
openLocalFilter(columnIndex, "local");
|
||||
} else if (!shouldIgnoreHeaderPanelClose(columnIndex, localFilterOpenColumn.value)) {
|
||||
closeLocalFilter();
|
||||
}
|
||||
|
|
@ -760,6 +901,7 @@ function closeLocalFilter() {
|
|||
localFilterOpenColumn.value = null;
|
||||
localFilterDraft.value = null;
|
||||
localFilterSearch.value = "";
|
||||
resetServerFilterState();
|
||||
}
|
||||
|
||||
function formatterKeyForColumn(column: string): string | null {
|
||||
|
|
@ -924,7 +1066,7 @@ function toggleLocalFilterValue(key: string) {
|
|||
const next = new Set(draft.values);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
localFilterDraft.value = { ...draft, values: next };
|
||||
localFilterDraft.value = { ...draft, values: next, touched: true };
|
||||
}
|
||||
|
||||
function toggleAllLocalFilterOptions() {
|
||||
|
|
@ -937,12 +1079,16 @@ function toggleAllLocalFilterOptions() {
|
|||
} else {
|
||||
visibleKeys.forEach((key) => next.add(key));
|
||||
}
|
||||
localFilterDraft.value = { ...draft, values: next };
|
||||
localFilterDraft.value = { ...draft, values: next, touched: true };
|
||||
}
|
||||
|
||||
async function applyLocalFilter() {
|
||||
const draft = localFilterDraft.value;
|
||||
if (!draft) return;
|
||||
if (draft.mode === "server") {
|
||||
await applyServerColumnFilter(draft);
|
||||
return;
|
||||
}
|
||||
if (canApplyTypedLocalFilterValue.value && localFilterDraftIsAllSelected.value && localFilterOptions.value.length === 0) {
|
||||
await applyTypedLocalFilterValue();
|
||||
return;
|
||||
|
|
@ -964,6 +1110,40 @@ async function applyLocalFilter() {
|
|||
resetGridVerticalScroll();
|
||||
}
|
||||
|
||||
async function applyServerColumnFilter(draft: LocalColumnFilterDraft) {
|
||||
if (!draft.touched && !localFilterSearch.value.trim()) {
|
||||
closeLocalFilter();
|
||||
return;
|
||||
}
|
||||
if (canApplyTypedLocalFilterValue.value && serverFilterOptions.value.length === 0) {
|
||||
await applyTypedLocalFilterValue();
|
||||
return;
|
||||
}
|
||||
const columnName = props.result.columns[draft.columnIndex];
|
||||
if (!columnName) return;
|
||||
const values = [...draft.values].flatMap((key) => {
|
||||
if (!serverFilterValueByKey.value.has(key)) return [];
|
||||
return [serverFilterValueByKey.value.get(key)!];
|
||||
});
|
||||
if (values.length === 0) {
|
||||
closeLocalFilter();
|
||||
return;
|
||||
}
|
||||
const condition = await buildColumnValuesFilterCondition({
|
||||
databaseType: resolvedDatabaseType.value,
|
||||
columnName,
|
||||
columnInfo: props.tableMeta?.columns.find((column) => column.name === columnName),
|
||||
values,
|
||||
});
|
||||
if (!condition) return;
|
||||
const next = { ...localColumnFilters.value };
|
||||
delete next[draft.columnIndex];
|
||||
localColumnFilters.value = next;
|
||||
whereFilterInput.value = appendColumnValueFilterCondition(whereFilterInput.value, condition);
|
||||
closeLocalFilter();
|
||||
await applyWhereFilter();
|
||||
}
|
||||
|
||||
async function applyTypedLocalFilterValue() {
|
||||
const draft = localFilterDraft.value;
|
||||
if (!draft) return;
|
||||
|
|
@ -996,6 +1176,17 @@ function clearLocalFilter(colIdx?: number) {
|
|||
resetGridVerticalScroll();
|
||||
}
|
||||
|
||||
watch(localFilterSearch, (value) => {
|
||||
const draft = localFilterDraft.value;
|
||||
if (!draft || draft.mode !== "server" || localFilterOpenColumn.value !== draft.columnIndex) return;
|
||||
if (serverFilterSearchTimer !== undefined) {
|
||||
window.clearTimeout(serverFilterSearchTimer);
|
||||
}
|
||||
serverFilterSearchTimer = window.setTimeout(() => {
|
||||
void loadServerFilterValues(draft.columnIndex, value);
|
||||
}, SERVER_COLUMN_FILTER_DEBOUNCE_MS);
|
||||
});
|
||||
|
||||
function defaultStructuredFilterRule(): StructuredFilterRule {
|
||||
return {
|
||||
id: uuid(),
|
||||
|
|
@ -2506,6 +2697,7 @@ const queryEditReadyTargetLabel = computed(() => props.tableMeta?.tableName ?? p
|
|||
const showKeylessEditWarning = computed(() => !!props.editable && !!props.tableMeta && canUseKeylessRowPredicate(props.databaseType, props.tableMeta.primaryKeys ?? []));
|
||||
const canShowWhereSearch = computed(() => !!props.onExecuteSql && !isResultsContext.value);
|
||||
const canUseWhereSearch = computed(() => !!props.tableMeta && !!props.onExecuteSql && !isResultsContext.value);
|
||||
const canUseServerColumnFilter = computed(() => canUseWhereSearch.value && !!props.connectionId && !!props.tableMeta);
|
||||
type DataGridTableMeta = NonNullable<typeof props.tableMeta>;
|
||||
const hiveTableTransactional = ref<boolean | undefined>(undefined);
|
||||
const canEditExistingRows = computed(() => !!props.customSaveHandler || canEditExistingTableRows(props.databaseType, hiveTableTransactional.value, props.tableMeta?.primaryKeys ?? []));
|
||||
|
|
@ -6637,6 +6829,9 @@ onUnmounted(() => {
|
|||
finishCellSelection();
|
||||
clearTimeout(highlightedColumnTimer);
|
||||
clearTimeout(_searchTimer);
|
||||
if (serverFilterSearchTimer !== undefined) {
|
||||
window.clearTimeout(serverFilterSearchTimer);
|
||||
}
|
||||
clearInterval(_loadingTimer);
|
||||
});
|
||||
|
||||
|
|
@ -7842,7 +8037,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
</PopoverTrigger>
|
||||
<PopoverContent align="start" side="bottom" class="w-[300px] max-w-[calc(100vw-2rem)] gap-0 overflow-hidden rounded-xl border bg-popover p-0 text-popover-foreground shadow-xl" @click.stop @keydown.stop>
|
||||
<div class="border-b bg-muted/40 px-2 py-1.5 text-center text-xs font-semibold">
|
||||
{{ t("grid.localFilterFor", { column: col.name }) }}
|
||||
{{ columnFilterPanelTitle(col.name) }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 border-b px-2 py-1.5">
|
||||
<Search class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
|
|
@ -7855,6 +8050,14 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
<span>{{ t("grid.value") }}</span>
|
||||
<span class="text-right">{{ t("grid.count") }}</span>
|
||||
</div>
|
||||
<div v-if="localFilterDraft?.mode === 'server' && (serverFilterLoading || serverFilterError || serverFilterLimited)" class="flex items-center gap-1.5 border-b px-2 py-1 text-[11px] text-muted-foreground">
|
||||
<Loader2 v-if="serverFilterLoading" class="h-3 w-3 animate-spin" />
|
||||
<span class="min-w-0 truncate">
|
||||
<template v-if="serverFilterLoading">{{ t("grid.loadingValues") }}</template>
|
||||
<template v-else-if="serverFilterError">{{ serverFilterError }}</template>
|
||||
<template v-else>{{ t("grid.serverValuesLimited", { count: SERVER_COLUMN_FILTER_LIMIT }) }}</template>
|
||||
</span>
|
||||
</div>
|
||||
<div class="max-h-72 overflow-auto py-0.5">
|
||||
<button v-for="option in localFilterOptions" :key="option.key" type="button" class="grid w-full grid-cols-[1.75rem_minmax(0,1fr)_3.5rem] items-center px-2 py-1 text-left text-xs hover:bg-accent" @click="toggleLocalFilterValue(option.key)">
|
||||
<span class="flex h-4 w-4 items-center justify-center rounded border" :class="localFilterDraft?.values.has(option.key) ? 'border-blue-600 bg-blue-600 text-white' : 'border-border bg-background text-foreground/70'">
|
||||
|
|
@ -7863,9 +8066,9 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
<span class="truncate font-mono" :class="{ 'italic text-muted-foreground': option.value === null }">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span class="text-right tabular-nums text-muted-foreground text-xs">{{ option.count }}</span>
|
||||
<span class="text-right tabular-nums text-muted-foreground text-xs">{{ option.count ?? "" }}</span>
|
||||
</button>
|
||||
<div v-if="localFilterAllOptions.length > localFilterOptions.length" class="px-2 py-0.5 text-center text-[10px] text-muted-foreground">
|
||||
<div v-if="localFilterDraft?.mode === 'local' && localFilterAllOptions.length > localFilterOptions.length" class="px-2 py-0.5 text-center text-[10px] text-muted-foreground">
|
||||
{{
|
||||
t("grid.moreValues", {
|
||||
count: localFilterAllOptions.length - localFilterOptions.length,
|
||||
|
|
@ -7878,7 +8081,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
{{ t("grid.filterTypedValue", { value: localFilterTypedValue }) }}
|
||||
</span>
|
||||
</button>
|
||||
<div v-if="localFilterOptions.length === 0 && !canApplyTypedLocalFilterValue" class="px-2 py-6 text-center text-xs text-muted-foreground">
|
||||
<div v-if="localFilterOptions.length === 0 && !canApplyTypedLocalFilterValue && !serverFilterLoading" class="px-2 py-6 text-center text-xs text-muted-foreground">
|
||||
{{ t("grid.noSearchResults") }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -7897,6 +8100,16 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<button
|
||||
v-if="!compactColumnHeaderActions && canUseServerColumnFilter"
|
||||
type="button"
|
||||
class="flex h-4 w-4 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-gray-200 dark:hover:bg-gray-800 hover:text-foreground"
|
||||
:class="localFilterOpenColumn === col.actualColIdx && localFilterDraft?.mode === 'server' ? 'text-primary opacity-100' : 'opacity-80'"
|
||||
:title="t('grid.databaseValueFilter')"
|
||||
@click.stop="openLocalFilter(col.actualColIdx, 'server')"
|
||||
>
|
||||
<Database class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
<div data-column-resize-handle class="absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-primary/30" @mousedown.stop="onResizeStart(col.visibleColIdx, $event)" @dblclick.stop="autoFitColumn(col.visibleColIdx)" />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -622,8 +622,10 @@ export default {
|
|||
filterBuilderIsNull: "Is NULL",
|
||||
filterBuilderIsNotNull: "Is not NULL",
|
||||
columnActions: "Column actions",
|
||||
localFilter: "Local column filter",
|
||||
localFilterFor: "Local Filter For '{column}'",
|
||||
localFilter: "Local value filter",
|
||||
localFilterFor: "Local Values For '{column}'",
|
||||
databaseValueFilter: "Database value filter",
|
||||
databaseValueFilterFor: "Database Values For '{column}'",
|
||||
columnFormatter: "Column formatter",
|
||||
columnFormatterFor: "Formatter for '{column}'",
|
||||
columnFormatterHint: "Formats display only; raw values stay unchanged.",
|
||||
|
|
@ -649,13 +651,15 @@ export default {
|
|||
saveFormatter: "Save",
|
||||
clearFormatter: "Clear",
|
||||
searchValues: "Search values...",
|
||||
loadingValues: "Loading values...",
|
||||
serverValuesLimited: "Showing first {count} values",
|
||||
filterTypedValue: "Use '{value}'",
|
||||
value: "Value",
|
||||
count: "Count",
|
||||
applyFilter: "Apply Filter",
|
||||
clearLocalFilters: "Clear local column filters",
|
||||
clearLocalFilters: "Clear local value filters",
|
||||
clearLocalFiltersShort: "Clear local",
|
||||
localFiltersActive: "{count} local column filters",
|
||||
localFiltersActive: "{count} local value filters",
|
||||
localFilterMoreValues: " +{count} more",
|
||||
columnVisibility: "Columns",
|
||||
columnVisibilityHint: "At least one column stays visible.",
|
||||
|
|
|
|||
|
|
@ -624,8 +624,10 @@ export default withEnglishFallback({
|
|||
filterBuilderIsNull: "Es NULL",
|
||||
filterBuilderIsNotNull: "No es NULL",
|
||||
columnActions: "Acciones de columna",
|
||||
localFilter: "Filtro local de columna",
|
||||
localFilterFor: "Filtro local para '{column}'",
|
||||
localFilter: "Filtro local de valores",
|
||||
localFilterFor: "Valores locales para '{column}'",
|
||||
databaseValueFilter: "Filtro de valores de base de datos",
|
||||
databaseValueFilterFor: "Valores de base de datos para '{column}'",
|
||||
columnFormatter: "Formateador de columna",
|
||||
columnFormatterFor: "Formateador para '{column}'",
|
||||
columnFormatterHint: "Solo formatea la visualización; los valores sin procesar no cambian.",
|
||||
|
|
@ -655,9 +657,9 @@ export default withEnglishFallback({
|
|||
value: "Valor",
|
||||
count: "Conteo",
|
||||
applyFilter: "Aplicar filtro",
|
||||
clearLocalFilters: "Limpiar filtros locales de columnas",
|
||||
clearLocalFilters: "Limpiar filtros locales de valores",
|
||||
clearLocalFiltersShort: "Limpiar local",
|
||||
localFiltersActive: "{count} filtros locales de columna",
|
||||
localFiltersActive: "{count} filtros locales de valores",
|
||||
localFilterMoreValues: " +{count} más",
|
||||
columnVisibility: "Columnas",
|
||||
columnVisibilityHint: "Al menos una columna permanece visible.",
|
||||
|
|
|
|||
|
|
@ -631,8 +631,10 @@ export default withEnglishFallback({
|
|||
filterBuilderIsNull: "È NULL",
|
||||
filterBuilderIsNotNull: "Non è NULL",
|
||||
columnActions: "Azioni colonna",
|
||||
localFilter: "Filtro colonna locale",
|
||||
localFilterFor: "Filtro Locale Per '{column}'",
|
||||
localFilter: "Filtro valori locale",
|
||||
localFilterFor: "Valori locali per '{column}'",
|
||||
databaseValueFilter: "Filtro valori database",
|
||||
databaseValueFilterFor: "Valori database per '{column}'",
|
||||
columnFormatter: "Formattatore colonna",
|
||||
columnFormatterFor: "Formattatore per '{column}'",
|
||||
columnFormatterHint: "Formatta solo la visualizzazione; i valori effettivi rimangono invariati.",
|
||||
|
|
@ -662,9 +664,9 @@ export default withEnglishFallback({
|
|||
value: "Valore",
|
||||
count: "Conteggio",
|
||||
applyFilter: "Applica Filtro",
|
||||
clearLocalFilters: "Cancella filtri colonna locali",
|
||||
clearLocalFilters: "Cancella filtri valori locali",
|
||||
clearLocalFiltersShort: "Cancella locali",
|
||||
localFiltersActive: "{count} filtri colonna locali",
|
||||
localFiltersActive: "{count} filtri valori locali",
|
||||
localFilterMoreValues: " +{count} altri",
|
||||
columnVisibility: "Colonne",
|
||||
columnVisibilityHint: "Almeno una colonna deve rimanere visibile.",
|
||||
|
|
|
|||
|
|
@ -620,8 +620,10 @@ export default withEnglishFallback({
|
|||
filterBuilderIsNull: "NULLである",
|
||||
filterBuilderIsNotNull: "NULLでない",
|
||||
columnActions: "列の操作",
|
||||
localFilter: "ローカル列フィルター",
|
||||
localFilterFor: "'{column}'のローカルフィルター",
|
||||
localFilter: "ローカル値フィルター",
|
||||
localFilterFor: "'{column}'のローカル値",
|
||||
databaseValueFilter: "データベース値フィルター",
|
||||
databaseValueFilterFor: "'{column}'のデータベース値",
|
||||
columnFormatter: "列フォーマッター",
|
||||
columnFormatterFor: "'{column}'のフォーマッター",
|
||||
columnFormatterHint: "表示のみをフォーマットし、実際の値は変更されません。",
|
||||
|
|
@ -647,13 +649,15 @@ export default withEnglishFallback({
|
|||
saveFormatter: "保存",
|
||||
clearFormatter: "クリア",
|
||||
searchValues: "値を検索...",
|
||||
loadingValues: "値を読み込み中...",
|
||||
serverValuesLimited: "最初の{count}件の値を表示中",
|
||||
filterTypedValue: "'{value}'を使用",
|
||||
value: "値",
|
||||
count: "件数",
|
||||
applyFilter: "フィルターを適用",
|
||||
clearLocalFilters: "ローカル列フィルターをクリア",
|
||||
clearLocalFilters: "ローカル値フィルターをクリア",
|
||||
clearLocalFiltersShort: "ローカルをクリア",
|
||||
localFiltersActive: "{count}件のローカル列フィルター",
|
||||
localFiltersActive: "{count}件のローカル値フィルター",
|
||||
localFilterMoreValues: " +{count}件以上",
|
||||
columnVisibility: "列",
|
||||
columnVisibilityHint: "少なくとも1列は表示されます。",
|
||||
|
|
|
|||
|
|
@ -636,8 +636,10 @@ export default withEnglishFallback({
|
|||
filterBuilderIsNull: "É NULL",
|
||||
filterBuilderIsNotNull: "Não é NULL",
|
||||
columnActions: "Ações da coluna",
|
||||
localFilter: "Filtro local de coluna",
|
||||
localFilterFor: "Filtro Local Para '{column}'",
|
||||
localFilter: "Filtro local de valores",
|
||||
localFilterFor: "Valores locais para '{column}'",
|
||||
databaseValueFilter: "Filtro de valores do banco de dados",
|
||||
databaseValueFilterFor: "Valores do banco de dados para '{column}'",
|
||||
columnFormatter: "Formatador de coluna",
|
||||
columnFormatterFor: "Formatador para '{column}'",
|
||||
columnFormatterHint: "Formata apenas a exibição; os valores brutos permanecem inalterados.",
|
||||
|
|
@ -667,9 +669,9 @@ export default withEnglishFallback({
|
|||
value: "Valor",
|
||||
count: "Contagem",
|
||||
applyFilter: "Aplicar Filtro",
|
||||
clearLocalFilters: "Limpar filtros locais de coluna",
|
||||
clearLocalFilters: "Limpar filtros locais de valores",
|
||||
clearLocalFiltersShort: "Limpar local",
|
||||
localFiltersActive: "{count} filtros locais de coluna",
|
||||
localFiltersActive: "{count} filtros locais de valores",
|
||||
localFilterMoreValues: " +{count} mais",
|
||||
columnVisibility: "Colunas",
|
||||
columnVisibilityHint: "Pelo menos uma coluna permanece visível.",
|
||||
|
|
|
|||
|
|
@ -625,8 +625,10 @@ export default withEnglishFallback({
|
|||
filterBuilderIsNull: "为空",
|
||||
filterBuilderIsNotNull: "不为空",
|
||||
columnActions: "字段操作",
|
||||
localFilter: "本地列筛选",
|
||||
localFilterFor: "本地筛选“{column}”",
|
||||
localFilter: "本地值筛选",
|
||||
localFilterFor: "本地值筛选“{column}”",
|
||||
databaseValueFilter: "数据库值筛选",
|
||||
databaseValueFilterFor: "数据库值筛选“{column}”",
|
||||
columnFormatter: "列格式化",
|
||||
columnFormatterFor: "格式化“{column}”",
|
||||
columnFormatterHint: "仅改变显示效果,原始值保持不变。",
|
||||
|
|
@ -652,13 +654,15 @@ export default withEnglishFallback({
|
|||
saveFormatter: "保存",
|
||||
clearFormatter: "清除",
|
||||
searchValues: "搜索值...",
|
||||
loadingValues: "正在加载值...",
|
||||
serverValuesLimited: "仅显示前 {count} 个值",
|
||||
filterTypedValue: "使用“{value}”筛选",
|
||||
value: "值",
|
||||
count: "数量",
|
||||
applyFilter: "应用筛选",
|
||||
clearLocalFilters: "清除本地列筛选",
|
||||
clearLocalFilters: "清除本地值筛选",
|
||||
clearLocalFiltersShort: "清除本地",
|
||||
localFiltersActive: "{count} 个本地列筛选",
|
||||
localFiltersActive: "{count} 个本地值筛选",
|
||||
localFilterMoreValues: ",还有 {count} 个",
|
||||
columnVisibility: "字段筛选",
|
||||
columnVisibilityHint: "至少保留一列可见。",
|
||||
|
|
|
|||
|
|
@ -624,8 +624,10 @@ export default withEnglishFallback({
|
|||
filterBuilderIsNull: "為空",
|
||||
filterBuilderIsNotNull: "不為空",
|
||||
columnActions: "欄位操作",
|
||||
localFilter: "本機欄位篩選",
|
||||
localFilterFor: "本機篩選「{column}」",
|
||||
localFilter: "本機值篩選",
|
||||
localFilterFor: "本機值篩選「{column}」",
|
||||
databaseValueFilter: "資料庫值篩選",
|
||||
databaseValueFilterFor: "資料庫值篩選「{column}」",
|
||||
columnFormatter: "欄位格式設定",
|
||||
columnFormatterFor: "格式化「{column}」",
|
||||
columnFormatterHint: "僅改變顯示效果,原始值保持不變。",
|
||||
|
|
@ -655,9 +657,9 @@ export default withEnglishFallback({
|
|||
value: "值",
|
||||
count: "數量",
|
||||
applyFilter: "套用篩選",
|
||||
clearLocalFilters: "清除本機欄位篩選",
|
||||
clearLocalFilters: "清除本機值篩選",
|
||||
clearLocalFiltersShort: "清除本機",
|
||||
localFiltersActive: "{count} 個本機欄位篩選",
|
||||
localFiltersActive: "{count} 個本機值篩選",
|
||||
localFilterMoreValues: ",還有 {count} 個",
|
||||
columnVisibility: "欄位篩選",
|
||||
columnVisibilityHint: "至少保留一欄可見。",
|
||||
|
|
|
|||
|
|
@ -181,6 +181,8 @@ export const buildDataGridCopyUpdateStatements = forward("buildDataGridCopyUpdat
|
|||
export const buildDataGridCopyInsertStatement = forward("buildDataGridCopyInsertStatement");
|
||||
export const buildDataGridContextFilterCondition = forward("buildDataGridContextFilterCondition");
|
||||
export const buildDataGridColumnValueFilterCondition = forward("buildDataGridColumnValueFilterCondition");
|
||||
export const buildDataGridColumnValuesFilterCondition = forward("buildDataGridColumnValuesFilterCondition");
|
||||
export const buildDataGridColumnDistinctValuesSql = forward("buildDataGridColumnDistinctValuesSql");
|
||||
export const buildDataGridCountSql = forward("buildDataGridCountSql");
|
||||
export const buildHiveTablePropertiesSql = forward("buildHiveTablePropertiesSql");
|
||||
export const buildExportInsertStatements = forward("buildExportInsertStatements");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { ColumnInfo, DatabaseType } from "@/types/database";
|
||||
import type { DataGridColumnInfo, DataGridContextFilterMode, GridCellValue } from "@/lib/dataGridSql";
|
||||
import { buildDataGridColumnValueFilterCondition } from "@/lib/dataGridSql";
|
||||
import { buildDataGridColumnValueFilterCondition, buildDataGridColumnValuesFilterCondition } from "@/lib/dataGridSql";
|
||||
import { normalizeWhereInput } from "@/lib/tableSelectSql";
|
||||
|
||||
export function buildColumnValueFilterCondition(options: { databaseType?: DatabaseType; columnName: string; columnInfo?: Pick<ColumnInfo, "data_type">; rawValue: string }): Promise<string | undefined> {
|
||||
|
|
@ -18,6 +18,21 @@ export function buildColumnValueFilterCondition(options: { databaseType?: Databa
|
|||
});
|
||||
}
|
||||
|
||||
export function buildColumnValuesFilterCondition(options: { databaseType?: DatabaseType; columnName: string; columnInfo?: Pick<ColumnInfo, "data_type">; values: GridCellValue[] }): Promise<string | undefined> {
|
||||
return buildDataGridColumnValuesFilterCondition({
|
||||
databaseType: options.databaseType,
|
||||
columnName: options.columnName,
|
||||
columnInfo: options.columnInfo
|
||||
? {
|
||||
name: options.columnName,
|
||||
data_type: options.columnInfo.data_type,
|
||||
is_nullable: true,
|
||||
}
|
||||
: undefined,
|
||||
values: options.values,
|
||||
});
|
||||
}
|
||||
|
||||
export function appendColumnValueFilterCondition(whereInput: string | undefined, condition: string | undefined): string {
|
||||
if (!condition) return normalizeWhereInput(whereInput);
|
||||
const existing = normalizeWhereInput(whereInput);
|
||||
|
|
|
|||
|
|
@ -64,6 +64,25 @@ export interface DataGridColumnValueFilterConditionOptions {
|
|||
rawValue: string;
|
||||
}
|
||||
|
||||
export interface DataGridColumnValuesFilterConditionOptions {
|
||||
databaseType?: DatabaseType;
|
||||
columnName: string;
|
||||
columnInfo?: DataGridColumnInfo;
|
||||
values: GridCellValue[];
|
||||
}
|
||||
|
||||
export interface DataGridColumnDistinctValuesSqlOptions {
|
||||
databaseType?: DatabaseType;
|
||||
schema?: string;
|
||||
tableName: string;
|
||||
columnName: string;
|
||||
columnInfo?: DataGridColumnInfo;
|
||||
whereInput?: string;
|
||||
searchValue?: string;
|
||||
limit?: number;
|
||||
includeCounts?: boolean;
|
||||
}
|
||||
|
||||
export interface DataGridCountSqlOptions {
|
||||
databaseType?: DatabaseType;
|
||||
schema?: string;
|
||||
|
|
@ -93,6 +112,14 @@ export function buildDataGridColumnValueFilterCondition(options: DataGridColumnV
|
|||
return api.buildDataGridColumnValueFilterCondition(options);
|
||||
}
|
||||
|
||||
export function buildDataGridColumnValuesFilterCondition(options: DataGridColumnValuesFilterConditionOptions): Promise<string | undefined> {
|
||||
return api.buildDataGridColumnValuesFilterCondition(options);
|
||||
}
|
||||
|
||||
export function buildDataGridColumnDistinctValuesSql(options: DataGridColumnDistinctValuesSqlOptions): Promise<string> {
|
||||
return api.buildDataGridColumnDistinctValuesSql(options);
|
||||
}
|
||||
|
||||
export function buildDataGridCountSql(options: DataGridCountSqlOptions): Promise<string> {
|
||||
return api.buildDataGridCountSql(options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,17 @@ import type {
|
|||
DroppedFilePreviewSqlOptions,
|
||||
} from "./tauri";
|
||||
import type { QueryEditability } from "@/lib/sqlAnalysis";
|
||||
import type { DataGridColumnValueFilterConditionOptions, DataGridContextFilterConditionOptions, DataGridCountSqlOptions, DataGridCopyInsertStatementOptions, DataGridCopyUpdateStatementOptions, DataGridSaveStatementOptions, HiveTablePropertiesSqlOptions } from "@/lib/dataGridSql";
|
||||
import type {
|
||||
DataGridColumnDistinctValuesSqlOptions,
|
||||
DataGridColumnValueFilterConditionOptions,
|
||||
DataGridColumnValuesFilterConditionOptions,
|
||||
DataGridContextFilterConditionOptions,
|
||||
DataGridCountSqlOptions,
|
||||
DataGridCopyInsertStatementOptions,
|
||||
DataGridCopyUpdateStatementOptions,
|
||||
DataGridSaveStatementOptions,
|
||||
HiveTablePropertiesSqlOptions,
|
||||
} from "@/lib/dataGridSql";
|
||||
import type { BuildTableStructureChangeSqlOptions, BuildSingleColumnAlterSqlOptions, TableStructureChangeSql } from "@/lib/tableStructureEditorSql";
|
||||
import type { BuildTableSelectSqlOptions } from "@/lib/tableSelectSql";
|
||||
import type { DatabaseSearchSql, DatabaseSearchSqlOptions, SearchResultWhereOptions } from "@/lib/databaseSearch";
|
||||
|
|
@ -808,6 +818,15 @@ export async function buildDataGridColumnValueFilterCondition(options: DataGridC
|
|||
return result ?? undefined;
|
||||
}
|
||||
|
||||
export async function buildDataGridColumnValuesFilterCondition(options: DataGridColumnValuesFilterConditionOptions): Promise<string | undefined> {
|
||||
const result = await post<string | null>("/api/query/build-data-grid-column-values-filter-condition", { options });
|
||||
return result ?? undefined;
|
||||
}
|
||||
|
||||
export async function buildDataGridColumnDistinctValuesSql(options: DataGridColumnDistinctValuesSqlOptions): Promise<string> {
|
||||
return post("/api/query/build-data-grid-column-distinct-values-sql", { options });
|
||||
}
|
||||
|
||||
export async function buildDataGridCountSql(options: DataGridCountSqlOptions): Promise<string> {
|
||||
return post("/api/query/build-data-grid-count-sql", { options });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,17 @@ import type { CollectionInfo } from "@/types/database";
|
|||
import type { SidebarObjectKind } from "@/lib/databaseObjectCapabilities";
|
||||
import type { AiConfig, AiTestConnectionResult } from "@/stores/settingsStore";
|
||||
import type { QueryEditability } from "@/lib/sqlAnalysis";
|
||||
import type { DataGridColumnValueFilterConditionOptions, DataGridContextFilterConditionOptions, DataGridCountSqlOptions, DataGridCopyInsertStatementOptions, DataGridCopyUpdateStatementOptions, DataGridSaveStatementOptions, HiveTablePropertiesSqlOptions } from "@/lib/dataGridSql";
|
||||
import type {
|
||||
DataGridColumnDistinctValuesSqlOptions,
|
||||
DataGridColumnValueFilterConditionOptions,
|
||||
DataGridColumnValuesFilterConditionOptions,
|
||||
DataGridContextFilterConditionOptions,
|
||||
DataGridCountSqlOptions,
|
||||
DataGridCopyInsertStatementOptions,
|
||||
DataGridCopyUpdateStatementOptions,
|
||||
DataGridSaveStatementOptions,
|
||||
HiveTablePropertiesSqlOptions,
|
||||
} from "@/lib/dataGridSql";
|
||||
import type { DataCompareFromTablesOptions, DataCompareFromTablesPreparation, DataCompareSyncPlan, DataCompareSyncPlanOptions, DataComparePreparation, DataComparePreparationOptions } from "@/lib/dataCompare";
|
||||
import type { SchemaDiffPreparation, SchemaDiffPreparationOptions, TableDiff, FunctionDiff, SequenceDiff, RuleDiff, OwnerDiff } from "@/lib/schemaDiff";
|
||||
import type { BuildTableStructureChangeSqlOptions, BuildSingleColumnAlterSqlOptions, TableStructureChangeSql } from "@/lib/tableStructureEditorSql";
|
||||
|
|
@ -802,6 +812,15 @@ export async function buildDataGridColumnValueFilterCondition(options: DataGridC
|
|||
return result ?? undefined;
|
||||
}
|
||||
|
||||
export async function buildDataGridColumnValuesFilterCondition(options: DataGridColumnValuesFilterConditionOptions): Promise<string | undefined> {
|
||||
const result = await invoke<string | null>("build_data_grid_column_values_filter_condition", { options });
|
||||
return result ?? undefined;
|
||||
}
|
||||
|
||||
export async function buildDataGridColumnDistinctValuesSql(options: DataGridColumnDistinctValuesSqlOptions): Promise<string> {
|
||||
return invoke("build_data_grid_column_distinct_values_sql", { options });
|
||||
}
|
||||
|
||||
export async function buildDataGridCountSql(options: DataGridCountSqlOptions): Promise<string> {
|
||||
return invoke("build_data_grid_count_sql", { options });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,14 @@ mod data_grid_tdengine_sql;
|
|||
use data_grid_tdengine_sql::build_tdengine_data_grid_save_statements;
|
||||
|
||||
use crate::models::connection::DatabaseType;
|
||||
use crate::sql_dialect::quote_table_identifier;
|
||||
use crate::sql_dialect::{quote_table_identifier, table_pagination_strategy, TablePaginationStrategy};
|
||||
use crate::transfer::{format_ch_array_sql_literal, format_pg_array_sql_literal};
|
||||
|
||||
const DBX_ROWID_COLUMN: &str = "__DBX_ROWID";
|
||||
pub(crate) const DBX_NEO4J_ELEMENT_ID_COLUMN: &str = "__DBX_ELEMENT_ID";
|
||||
pub(crate) const DBX_TDENGINE_TBNAME_COLUMN: &str = "tbname";
|
||||
const DATA_GRID_COLUMN_DISTINCT_VALUES_DEFAULT_LIMIT: usize = 1000;
|
||||
const DATA_GRID_COLUMN_DISTINCT_VALUES_MAX_LIMIT: usize = 1000;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -129,6 +131,39 @@ pub struct DataGridColumnValueFilterConditionOptions {
|
|||
pub raw_value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DataGridColumnValuesFilterConditionOptions {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub database_type: Option<DatabaseType>,
|
||||
pub column_name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub column_info: Option<DataGridColumnInfo>,
|
||||
#[serde(default)]
|
||||
pub values: Vec<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DataGridColumnDistinctValuesSqlOptions {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub database_type: Option<DatabaseType>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub schema: Option<String>,
|
||||
pub table_name: String,
|
||||
pub column_name: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub column_info: Option<DataGridColumnInfo>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub where_input: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub search_value: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub limit: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub include_counts: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DataGridCountSqlOptions {
|
||||
|
|
@ -387,6 +422,96 @@ pub fn build_data_grid_column_value_filter_condition(
|
|||
Some(format!("{column} = {}", format_grid_sql_literal(&value, options.database_type, options.column_info.as_ref())))
|
||||
}
|
||||
|
||||
pub fn build_data_grid_column_values_filter_condition(
|
||||
options: DataGridColumnValuesFilterConditionOptions,
|
||||
) -> Option<String> {
|
||||
if options.values.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let column = column_filter_ref(options.database_type, &options.column_name);
|
||||
let mut has_null = false;
|
||||
let mut literals = Vec::new();
|
||||
for value in &options.values {
|
||||
if value.is_null() {
|
||||
has_null = true;
|
||||
continue;
|
||||
}
|
||||
let literal = format_grid_sql_literal(value, options.database_type, options.column_info.as_ref());
|
||||
if !literals.contains(&literal) {
|
||||
literals.push(literal);
|
||||
}
|
||||
}
|
||||
|
||||
let mut predicates = Vec::new();
|
||||
if has_null {
|
||||
predicates.push(format!("{column} IS NULL"));
|
||||
}
|
||||
if !literals.is_empty() {
|
||||
let list = literals.join(", ");
|
||||
if literals.len() == 1 {
|
||||
predicates.push(format!("{column} = {list}"));
|
||||
} else if options.database_type == Some(DatabaseType::Neo4j) {
|
||||
predicates.push(format!("{column} IN [{list}]"));
|
||||
} else {
|
||||
predicates.push(format!("{column} IN ({list})"));
|
||||
}
|
||||
}
|
||||
|
||||
match predicates.len() {
|
||||
0 => None,
|
||||
1 => predicates.into_iter().next(),
|
||||
_ => Some(format!("({})", predicates.join(" OR "))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_data_grid_column_distinct_values_sql(options: DataGridColumnDistinctValuesSqlOptions) -> String {
|
||||
if options.database_type == Some(DatabaseType::Neo4j) {
|
||||
return build_neo4j_data_grid_column_distinct_values_sql(&options);
|
||||
}
|
||||
|
||||
let limit = data_grid_column_distinct_values_limit(options.limit);
|
||||
let table = qualified_table_name(options.database_type, options.schema.as_deref(), &options.table_name);
|
||||
let column = column_filter_ref(options.database_type, &options.column_name);
|
||||
let mut predicates = Vec::new();
|
||||
let predicate = crate::sql_dialect::normalize_where_input(options.where_input.as_deref());
|
||||
if !predicate.is_empty() {
|
||||
predicates.push(format!("({predicate})"));
|
||||
}
|
||||
if let Some(search_predicate) = data_grid_column_distinct_values_search_predicate(&options) {
|
||||
predicates.push(search_predicate);
|
||||
}
|
||||
let where_clause =
|
||||
if predicates.is_empty() { String::new() } else { format!(" WHERE {}", predicates.join(" AND ")) };
|
||||
let select_list = if options.include_counts {
|
||||
format!("{column} AS dbx_value, COUNT(*) AS dbx_count")
|
||||
} else {
|
||||
format!("{column} AS dbx_value")
|
||||
};
|
||||
let group_by = format!(" GROUP BY {column}");
|
||||
let order_by = if options.include_counts { " ORDER BY dbx_count DESC, dbx_value" } else { " ORDER BY dbx_value" };
|
||||
let from_clause = format!(" FROM {table}{where_clause}{group_by}{order_by}");
|
||||
|
||||
match table_pagination_strategy(options.database_type) {
|
||||
TablePaginationStrategy::SqlServerTop => format!("SELECT TOP ({limit}) {select_list}{from_clause}"),
|
||||
TablePaginationStrategy::IrisTop => format!("SELECT TOP {limit} {select_list}{from_clause}"),
|
||||
TablePaginationStrategy::InformixFirst => format!("SELECT FIRST {limit} {select_list}{from_clause}"),
|
||||
TablePaginationStrategy::Db2FetchFirst | TablePaginationStrategy::FetchFirst => {
|
||||
format!("SELECT {select_list}{from_clause} FETCH FIRST {limit} ROWS ONLY")
|
||||
}
|
||||
TablePaginationStrategy::Rownum => {
|
||||
let inner = format!("SELECT {select_list}{from_clause}");
|
||||
format!("SELECT * FROM ({inner}) WHERE ROWNUM <= {limit}")
|
||||
}
|
||||
TablePaginationStrategy::AgentMaxRows | TablePaginationStrategy::Unbounded => {
|
||||
format!("SELECT {select_list}{from_clause}")
|
||||
}
|
||||
TablePaginationStrategy::QuestDbLimit | TablePaginationStrategy::LimitOffset => {
|
||||
format!("SELECT {select_list}{from_clause} LIMIT {limit}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_data_grid_count_sql(options: DataGridCountSqlOptions) -> String {
|
||||
let table = qualified_table_name(options.database_type, options.schema.as_deref(), &options.table_name);
|
||||
let predicate = crate::sql_dialect::normalize_where_input(options.where_input.as_deref());
|
||||
|
|
@ -400,6 +525,60 @@ pub fn build_hive_table_properties_sql(options: HiveTablePropertiesSqlOptions) -
|
|||
format!("SHOW TBLPROPERTIES {table} ('{property}')")
|
||||
}
|
||||
|
||||
fn data_grid_column_distinct_values_limit(limit: Option<usize>) -> usize {
|
||||
limit.unwrap_or(DATA_GRID_COLUMN_DISTINCT_VALUES_DEFAULT_LIMIT).clamp(1, DATA_GRID_COLUMN_DISTINCT_VALUES_MAX_LIMIT)
|
||||
}
|
||||
|
||||
fn data_grid_column_distinct_values_search_predicate(
|
||||
options: &DataGridColumnDistinctValuesSqlOptions,
|
||||
) -> Option<String> {
|
||||
let search = options.search_value.as_deref()?.trim();
|
||||
if search.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if !options.column_info.as_ref().map(|column| is_textual_column_type(&column.data_type)).unwrap_or(true)
|
||||
&& !is_postgres_like_pattern_database(options.database_type)
|
||||
{
|
||||
let column = column_filter_ref(options.database_type, &options.column_name);
|
||||
let value = parse_typed_filter_value(search, options.column_info.as_ref());
|
||||
return Some(format!(
|
||||
"{column} = {}",
|
||||
format_grid_sql_literal(&value, options.database_type, options.column_info.as_ref())
|
||||
));
|
||||
}
|
||||
let column = column_like_filter_ref(options.database_type, &options.column_name, options.column_info.as_ref());
|
||||
let pattern = Value::String(format!("%{search}%"));
|
||||
Some(format!("{column} LIKE {}", format_grid_sql_literal(&pattern, options.database_type, None)))
|
||||
}
|
||||
|
||||
fn build_neo4j_data_grid_column_distinct_values_sql(options: &DataGridColumnDistinctValuesSqlOptions) -> String {
|
||||
let limit = data_grid_column_distinct_values_limit(options.limit);
|
||||
let label = quote_ident(Some(DatabaseType::Neo4j), &options.table_name);
|
||||
let column = column_filter_ref(Some(DatabaseType::Neo4j), &options.column_name);
|
||||
let mut predicates = Vec::new();
|
||||
let predicate = crate::sql_dialect::normalize_where_input(options.where_input.as_deref());
|
||||
if !predicate.is_empty() {
|
||||
predicates.push(predicate);
|
||||
}
|
||||
if let Some(search) = options.search_value.as_deref().map(str::trim).filter(|value| !value.is_empty()) {
|
||||
predicates.push(format!(
|
||||
"toString({column}) CONTAINS {}",
|
||||
format_grid_sql_literal(&Value::String(search.to_string()), Some(DatabaseType::Neo4j), None)
|
||||
));
|
||||
}
|
||||
let where_clause =
|
||||
if predicates.is_empty() { String::new() } else { format!(" WHERE {}", predicates.join(" AND ")) };
|
||||
if options.include_counts {
|
||||
format!(
|
||||
"MATCH (n:{label}){where_clause} RETURN {column} AS dbx_value, count(*) AS dbx_count ORDER BY dbx_count DESC, dbx_value LIMIT {limit}"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"MATCH (n:{label}){where_clause} RETURN DISTINCT {column} AS dbx_value ORDER BY dbx_value LIMIT {limit}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_data_grid_save(options: &DataGridSaveStatementOptions) -> Option<String> {
|
||||
if let Some(error) = validate_inserted_primary_keys(options) {
|
||||
return Some(error);
|
||||
|
|
@ -1856,6 +2035,90 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_multi_value_filter_conditions() {
|
||||
assert_eq!(
|
||||
build_data_grid_column_values_filter_condition(DataGridColumnValuesFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Postgres),
|
||||
column_name: "status".to_string(),
|
||||
column_info: Some(column("status", "varchar", true, None)),
|
||||
values: vec![json!("active"), json!("pending"), Value::Null, json!("active")],
|
||||
})
|
||||
.as_deref(),
|
||||
Some("(\"status\" IS NULL OR \"status\" IN ('active', 'pending'))")
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_column_values_filter_condition(DataGridColumnValuesFilterConditionOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
column_name: "id".to_string(),
|
||||
column_info: Some(column("id", "int", false, None)),
|
||||
values: vec![json!(42)],
|
||||
})
|
||||
.as_deref(),
|
||||
Some("`id` = 42")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_column_distinct_values_sql() {
|
||||
assert_eq!(
|
||||
build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions {
|
||||
database_type: Some(DatabaseType::Postgres),
|
||||
schema: Some("public".to_string()),
|
||||
table_name: "users".to_string(),
|
||||
column_name: "status".to_string(),
|
||||
column_info: Some(column("status", "varchar", true, None)),
|
||||
where_input: Some("WHERE deleted_at IS NULL;".to_string()),
|
||||
search_value: Some("act".to_string()),
|
||||
limit: None,
|
||||
include_counts: true,
|
||||
}),
|
||||
"SELECT \"status\" AS dbx_value, COUNT(*) AS dbx_count FROM \"public\".\"users\" WHERE (deleted_at IS NULL) AND \"status\" LIKE '%act%' GROUP BY \"status\" ORDER BY dbx_count DESC, dbx_value LIMIT 1000"
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions {
|
||||
database_type: Some(DatabaseType::SqlServer),
|
||||
schema: None,
|
||||
table_name: "users".to_string(),
|
||||
column_name: "status".to_string(),
|
||||
column_info: Some(column("status", "nvarchar", true, None)),
|
||||
where_input: None,
|
||||
search_value: None,
|
||||
limit: Some(25),
|
||||
include_counts: false,
|
||||
}),
|
||||
"SELECT TOP (25) [status] AS dbx_value FROM [users] GROUP BY [status] ORDER BY dbx_value"
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions {
|
||||
database_type: Some(DatabaseType::SqlServer),
|
||||
schema: None,
|
||||
table_name: "users".to_string(),
|
||||
column_name: "id".to_string(),
|
||||
column_info: Some(column("id", "int", false, None)),
|
||||
where_input: None,
|
||||
search_value: Some("42".to_string()),
|
||||
limit: Some(25),
|
||||
include_counts: true,
|
||||
}),
|
||||
"SELECT TOP (25) [id] AS dbx_value, COUNT(*) AS dbx_count FROM [users] WHERE [id] = 42 GROUP BY [id] ORDER BY dbx_count DESC, dbx_value"
|
||||
);
|
||||
assert_eq!(
|
||||
build_data_grid_column_distinct_values_sql(DataGridColumnDistinctValuesSqlOptions {
|
||||
database_type: Some(DatabaseType::Oracle),
|
||||
schema: Some("APP".to_string()),
|
||||
table_name: "EVENTS".to_string(),
|
||||
column_name: "KIND".to_string(),
|
||||
column_info: Some(column("KIND", "VARCHAR2", true, None)),
|
||||
where_input: None,
|
||||
search_value: None,
|
||||
limit: Some(10),
|
||||
include_counts: true,
|
||||
}),
|
||||
"SELECT * FROM (SELECT \"KIND\" AS dbx_value, COUNT(*) AS dbx_count FROM \"APP\".\"EVENTS\" GROUP BY \"KIND\" ORDER BY dbx_count DESC, dbx_value) WHERE ROWNUM <= 10"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_grid_count_sql() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -290,6 +290,14 @@ async fn main() {
|
|||
"/query/build-data-grid-column-value-filter-condition",
|
||||
post(routes::query::build_data_grid_column_value_filter_condition),
|
||||
)
|
||||
.route(
|
||||
"/query/build-data-grid-column-values-filter-condition",
|
||||
post(routes::query::build_data_grid_column_values_filter_condition),
|
||||
)
|
||||
.route(
|
||||
"/query/build-data-grid-column-distinct-values-sql",
|
||||
post(routes::query::build_data_grid_column_distinct_values_sql),
|
||||
)
|
||||
.route("/query/build-data-grid-count-sql", post(routes::query::build_data_grid_count_sql))
|
||||
.route("/query/build-hive-table-properties-sql", post(routes::query::build_hive_table_properties_sql))
|
||||
.route("/query/build-export-insert-statements", post(routes::query::build_export_insert_statements))
|
||||
|
|
|
|||
|
|
@ -234,6 +234,18 @@ pub struct BuildDataGridColumnValueFilterConditionRequest {
|
|||
pub options: dbx_core::data_grid_sql::DataGridColumnValueFilterConditionOptions,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BuildDataGridColumnValuesFilterConditionRequest {
|
||||
pub options: dbx_core::data_grid_sql::DataGridColumnValuesFilterConditionOptions,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BuildDataGridColumnDistinctValuesSqlRequest {
|
||||
pub options: dbx_core::data_grid_sql::DataGridColumnDistinctValuesSqlOptions,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BuildDataGridCountSqlRequest {
|
||||
|
|
@ -690,6 +702,18 @@ pub async fn build_data_grid_column_value_filter_condition(
|
|||
Json(dbx_core::data_grid_sql::build_data_grid_column_value_filter_condition(req.options))
|
||||
}
|
||||
|
||||
pub async fn build_data_grid_column_values_filter_condition(
|
||||
Json(req): Json<BuildDataGridColumnValuesFilterConditionRequest>,
|
||||
) -> Json<Option<String>> {
|
||||
Json(dbx_core::data_grid_sql::build_data_grid_column_values_filter_condition(req.options))
|
||||
}
|
||||
|
||||
pub async fn build_data_grid_column_distinct_values_sql(
|
||||
Json(req): Json<BuildDataGridColumnDistinctValuesSqlRequest>,
|
||||
) -> Json<String> {
|
||||
Json(dbx_core::data_grid_sql::build_data_grid_column_distinct_values_sql(req.options))
|
||||
}
|
||||
|
||||
pub async fn build_data_grid_count_sql(Json(req): Json<BuildDataGridCountSqlRequest>) -> Json<String> {
|
||||
Json(dbx_core::data_grid_sql::build_data_grid_count_sql(req.options))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import { appendColumnValueFilterCondition, buildColumnValueFilterCondition } from "../../apps/desktop/src/lib/dataGridColumnFilter.ts";
|
||||
import { appendColumnValueFilterCondition, buildColumnValueFilterCondition, buildColumnValuesFilterCondition } from "../../apps/desktop/src/lib/dataGridColumnFilter.ts";
|
||||
|
||||
function installFilterFetchMock() {
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
if (String(input) === "/api/query/build-data-grid-column-values-filter-condition") {
|
||||
const body = JSON.parse(String(init?.body ?? "{}"));
|
||||
const options = body.options;
|
||||
const quote = options.databaseType === "mysql" ? (name: string) => `\`${name}\`` : options.databaseType === "sqlserver" ? (name: string) => `[${name}]` : (name: string) => `"${name}"`;
|
||||
const values = options.values ?? [];
|
||||
const nonNull = values.filter((value: unknown) => value !== null);
|
||||
const nullClause = values.some((value: unknown) => value === null) ? `${quote(options.columnName)} IS NULL` : "";
|
||||
const valueClause = nonNull.length ? `${quote(options.columnName)} IN (${nonNull.map((value: unknown) => (typeof value === "number" ? value : `'${value}'`)).join(", ")})` : "";
|
||||
const result = [nullClause, valueClause].filter(Boolean).join(" OR ");
|
||||
return new Response(JSON.stringify(result ? `(${result})` : null), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
if (String(input) !== "/api/query/build-data-grid-column-value-filter-condition") {
|
||||
return new Response("unexpected request", { status: 500 });
|
||||
}
|
||||
|
|
@ -51,3 +62,15 @@ test("builds IS NULL for typed NULL filters", async () => {
|
|||
|
||||
assert.equal(condition, "[archived_at] IS NULL");
|
||||
});
|
||||
|
||||
test("builds multi-value server-side column filters", async () => {
|
||||
installFilterFetchMock();
|
||||
const condition = await buildColumnValuesFilterCondition({
|
||||
databaseType: "postgres",
|
||||
columnName: "status",
|
||||
columnInfo: { name: "status", data_type: "varchar", is_nullable: true, is_primary_key: false },
|
||||
values: ["active", "pending", null],
|
||||
});
|
||||
|
||||
assert.equal(condition, `("status" IS NULL OR "status" IN ('active', 'pending'))`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -438,6 +438,20 @@ pub fn build_data_grid_column_value_filter_condition(
|
|||
Ok(dbx_core::data_grid_sql::build_data_grid_column_value_filter_condition(options))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn build_data_grid_column_values_filter_condition(
|
||||
options: dbx_core::data_grid_sql::DataGridColumnValuesFilterConditionOptions,
|
||||
) -> Result<Option<String>, String> {
|
||||
Ok(dbx_core::data_grid_sql::build_data_grid_column_values_filter_condition(options))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn build_data_grid_column_distinct_values_sql(
|
||||
options: dbx_core::data_grid_sql::DataGridColumnDistinctValuesSqlOptions,
|
||||
) -> Result<String, String> {
|
||||
Ok(dbx_core::data_grid_sql::build_data_grid_column_distinct_values_sql(options))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn build_data_grid_count_sql(options: dbx_core::data_grid_sql::DataGridCountSqlOptions) -> Result<String, String> {
|
||||
Ok(dbx_core::data_grid_sql::build_data_grid_count_sql(options))
|
||||
|
|
|
|||
|
|
@ -755,6 +755,8 @@ pub fn run() {
|
|||
commands::query::build_data_grid_copy_insert_statement,
|
||||
commands::query::build_data_grid_context_filter_condition,
|
||||
commands::query::build_data_grid_column_value_filter_condition,
|
||||
commands::query::build_data_grid_column_values_filter_condition,
|
||||
commands::query::build_data_grid_column_distinct_values_sql,
|
||||
commands::query::build_data_grid_count_sql,
|
||||
commands::query::build_hive_table_properties_sql,
|
||||
commands::query::build_export_insert_statements,
|
||||
|
|
|
|||
Loading…
Reference in New Issue