dbeaver/pro#7562 [CB] keep data editor filter state during session (#4261)

* dbeaver/pro#7562 feat: persist data editor filter state after reconnect

* dbeaver/pro#7562 feat: update data viewer to persist filter state

* dbeaver/pro#7562 feat: enhance constraint resolution in WebSQLDataFilter

allow using attrName without attrPosition

* dbeaver/pro#7562 refactor: don't use options

replaced private restorePendingState()  with public restoreViewState(pinnedColumnNames, columnOrderNames?) that takes data directly as arguments

* dbeaver/pro#7562 fix: stale constraints in UI

bug with skipping constraints if any without a position.
if (!prevColumn) {
        return;
      }

* dbeaver/pro#7562 refactor: add method to retrieve pinned column names in GridViewAction

* dbeaver/pro#7562 refactor: simplify state validation using schema

* dbeaver/pro#7562 refactor: simplify state management in useDataViewerPanel

* dbeaver/pro#7562 refactor: move persisted state to IDataViewerPageState

* dbeaver/pro#7562 fix: initial pageState

* dbeaver/pro#7562 refactor: simplify getPinnedColumnNames method in GridViewAction

* dbeaver/pro#7562 refactor: shadowed column renamed to c for clarity

* dbeaver/pro#7562 refactor: change type of value in IPersistedConstraint to unknown

* dbeaver/pro#7562 refactor: remove manual state operations

* dbeaver/pro#7562 refactor: introduce DatabasePersistedStateAction for centralized state persistence

* dbeaver/pro#7562 fix

* dbeaver/pro#7562 refactor: replace reaction with when

* Revert "dbeaver/pro#7562 feat: enhance constraint resolution in WebSQLDataFilter"

This reverts commit 3e0e86f4b2.

Last test showed that we don't need any additional logic on BE. It handles all constraints properly.

* dbeaver/pro#7562 fix: make pinColumns optional

* dbeaver/pro#7562 refactor: simplify pinned column handling

* dbeaver/pro#7562 refactor: initialize store in DatabasePersistedStateAction constructor as observable

* dbeaver/pro#7562 refactor: rename getColumnNameByPosition to getColumnNameAt for clarity

* dbeaver/pro#7562 refactor: move DatabasePersistedStateAction to source level, simplify persisted state sync

- Make DatabasePersistedStateAction a standalone class on DatabaseDataSource instead of per-result action
- Convert options to getter
- Auto-sync constraints to persisted state via autorun + action batching
- Restore view state (pinned columns, column order) in GridViewAction constructor

* dbeaver/pro#7562 refactor: replace DatabasePersistedStateAction with DatabasePersistedStateStore and update related references

* dbeaver/pro#7562 refactor: simplify view-state persistence

- DatabasePersistedStateStore: pure KV (observable.ref + actions)
- DatabaseDataSource: add loadPersistedState() + onPersistedStateLoaded() hook
- DatabaseDataConstraintAction: own the constraint persist
- ResultSetDataSource: thin dispatcher
- GridViewAction: columnsOrder/pinnedColumns become computed getters

* dbeaver/pro#7562 refactor: create universal ColumnReference

* dbeaver/pro#7562 refactor: simplify persistence flow, remove extra guards, simplify persisted data filter state handling

* dbeaver/pro#7562 refactor: simplify GridViewAction column-ref handling and layout resolution

* dbeaver/pro#7562 refactor: per-key reactive persisted state store, drop constraint persistence hacks

* dbeaver/pro#7562 refactor: simplify setOptions method to use superclass implementation

* dbeaver/pro#7562 add missing fields

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
Co-authored-by: naumov <iamemptyhuh@gmail.com>
Co-authored-by: alex <48489896+devnaumov@users.noreply.github.com>
This commit is contained in:
Sychev Andrey
2026-04-22 14:52:59 +02:00
committed by GitHub
co-authored by Daria Marutkina naumov alex
parent a2ccd0b0f7
commit 9c6f301fa9
17 changed files with 430 additions and 81 deletions
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@ export class DVResultTraceDetailsBootstrap extends Bootstrap {
super();
}
override register() {
override register(): void {
this.dataPresentationService.add({
id: 'result-trace-details-presentation',
type: DataPresentationType.toolsPanel,
@@ -32,7 +32,7 @@ export class DVResultTraceDetailsBootstrap extends Bootstrap {
if (!isResultSetDataSource(source)) {
return true;
}
const result = (source as ResultSetDataSource<unknown>).getResult(resultIndex);
const result = (source as ResultSetDataSource).getResult(resultIndex);
return !result?.data?.hasDynamicTrace;
},
getPresentationComponent: () => DVResultTraceDetailsPresentation,
@@ -30,6 +30,7 @@ export const DataViewerPanel: ObjectPagePanelComponent<IDataViewerPageState> = o
presentationId,
resultIndex: 0,
valuePresentationId: null,
persistedState: {},
});
} else {
pageState.presentationId = presentationId;
@@ -47,6 +48,7 @@ export const DataViewerPanel: ObjectPagePanelComponent<IDataViewerPageState> = o
presentationId: '',
resultIndex: 0,
valuePresentationId,
persistedState: {},
});
} else {
pageState.valuePresentationId = valuePresentationId;
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -57,18 +57,29 @@ export function useDataViewerPanel(tab: ITab<IObjectViewerTabState>) {
model = dataViewerTableService.create(connectionInfo, node);
tab.handlerState.tableId = model.id;
let pageState = dataViewerTabService.page.getState(tab);
if (!pageState) {
dataViewerTabService.page.setState(tab, {
resultIndex: 0,
presentationId: '',
valuePresentationId: null,
persistedState: {},
});
pageState = dataViewerTabService.page.getState(tab)!;
}
model.source.loadPersistedState(pageState.persistedState);
const presentation = dataPresentationService.get(pageState.presentationId);
if (presentation?.dataFormat !== undefined) {
model.setDataFormat(presentation.dataFormat);
}
model.source.setOutdated();
dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id);
const pageState = dataViewerTabService.page.getState(tab);
if (pageState) {
const presentation = dataPresentationService.get(pageState.presentationId);
if (presentation?.dataFormat !== undefined) {
model.setDataFormat(presentation.dataFormat);
}
}
}
if (node?.name) {
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -111,7 +111,12 @@ export class DataViewerTabService {
await initTab();
if (tabInfo.isNewlyCreated) {
trySwitchPage(this.page);
trySwitchPage(this.page, {
resultIndex: 0,
presentationId: '',
valuePresentationId: null,
persistedState: {},
});
}
} catch (exception: any) {
this.notificationService.logException(exception, 'Data Editor Error', 'Error in Data Editor while processing action with database node');
@@ -5,9 +5,9 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { computed, makeObservable } from 'mobx';
import { action, computed, makeObservable, runInAction } from 'mobx';
import { type DataTypeLogicalOperation, ResultDataFormat, type SqlDataFilterConstraint } from '@cloudbeaver/core-sdk';
import { type DataTypeLogicalOperation, ResultDataFormat, type SqlDataFilterConstraint, type SqlResultColumn } from '@cloudbeaver/core-sdk';
import { DatabaseDataAction } from '../DatabaseDataAction.js';
import type { IDatabaseDataOptions } from '../IDatabaseDataOptions.js';
@@ -21,10 +21,47 @@ import { IDatabaseDataResult } from '../IDatabaseDataResult.js';
export const IS_NULL_ID = 'IS_NULL';
export const IS_NOT_NULL_ID = 'IS_NOT_NULL';
const CONSTRAINTS_KEY = 'constraints';
const WHERE_FILTER_KEY = 'whereFilter';
export function persistDataFilterConstraints<TOptions extends IDatabaseDataOptions>(
source: IDatabaseDataSource<TOptions, IDatabaseResultSet>,
): void {
const options = source.options;
if (!options) {
return;
}
source.persistedState.set(CONSTRAINTS_KEY, options.constraints.filter(hasConstraintIdentity));
source.persistedState.set(WHERE_FILTER_KEY, options.whereFilter || '');
}
export function applyPersistedDataFilterConstraints<TOptions extends IDatabaseDataOptions>(
source: IDatabaseDataSource<TOptions, IDatabaseResultSet>,
): void {
const options = source.options;
if (!options) {
return;
}
const constraints = source.persistedState.get<SqlDataFilterConstraint[]>(CONSTRAINTS_KEY);
const whereFilter = source.persistedState.get<string>(WHERE_FILTER_KEY);
if (!Array.isArray(constraints) || typeof whereFilter !== 'string') {
return;
}
runInAction(() => {
options.constraints = constraints.map(constraint => ({ ...constraint }));
options.whereFilter = whereFilter;
});
}
@injectable(() => [IDatabaseDataSource, IDatabaseDataResult])
export class DatabaseDataConstraintAction
extends DatabaseDataAction<IDatabaseDataOptions, IDatabaseResultSet>
implements IDatabaseDataConstraintAction<IDatabaseResultSet> {
implements IDatabaseDataConstraintAction<IDatabaseResultSet>
{
static dataFormat = [ResultDataFormat.Resultset, ResultDataFormat.Document];
get supported(): boolean {
@@ -54,6 +91,16 @@ export class DatabaseDataConstraintAction
makeObservable(this, {
orderConstraints: computed,
filterConstraints: computed,
deleteAll: action,
deleteFilter: action,
deleteFilters: action,
deleteOrders: action,
deleteOrder: action,
deleteDataFilters: action,
deleteData: action,
setWhereFilter: action,
setFilter: action,
setOrder: action,
});
}
@@ -161,7 +208,7 @@ export class DatabaseDataConstraintAction
this.resetWhereFilter();
}
setWhereFilter(value: string) {
setWhereFilter(value: string): void {
if (!this.source.options) {
throw new Error('Options must be provided');
}
@@ -169,11 +216,11 @@ export class DatabaseDataConstraintAction
this.source.options.whereFilter = value;
}
resetWhereFilter() {
resetWhereFilter(): void {
this.setWhereFilter('');
}
setFilter(attributePosition: number, operator: string, value?: any): void {
setFilter(attributePosition: number, operator: string, value?: unknown): void {
if (!this.source.options) {
throw new Error('Options must be provided');
}
@@ -182,6 +229,7 @@ export class DatabaseDataConstraintAction
if (currentConstraint) {
currentConstraint.operator = operator;
currentConstraint.attributeName = this.getColumnNameAt(attributePosition);
if (value !== undefined) {
currentConstraint.value = value;
} else if (currentConstraint.value !== undefined) {
@@ -192,6 +240,7 @@ export class DatabaseDataConstraintAction
const constraint: SqlDataFilterConstraint = {
attributePosition,
attributeName: this.getColumnNameAt(attributePosition),
operator,
};
@@ -219,6 +268,7 @@ export class DatabaseDataConstraintAction
if (!resetOrder) {
this.source.options.constraints.push({
attributePosition,
attributeName: this.getColumnNameAt(attributePosition),
orderPosition: this.getMaxOrderPosition(),
orderAsc: order === EOrder.asc,
});
@@ -257,6 +307,10 @@ export class DatabaseDataConstraintAction
override updateResult(result: IDatabaseResultSet): void {
updateConstraintsForResult(this.source, result);
}
private getColumnNameAt(colIdx: number): string | undefined {
return this.result.data?.columns?.find(c => c.position === colIdx)?.name;
}
}
function updateConstraintsForResult(source: IDatabaseDataSource<IDatabaseDataOptions, IDatabaseResultSet>, result: IDatabaseResultSet) {
@@ -264,31 +318,66 @@ function updateConstraintsForResult(source: IDatabaseDataSource<IDatabaseDataOpt
return;
}
for (const constraint of source.options.constraints) {
const prevColumn = result.data?.columns?.find(column => column.position === constraint.attributePosition);
const columns = result.data?.columns ?? [];
if (!prevColumn) {
return;
}
if (columns.length === 0) {
return;
}
let column = result.data?.columns?.find(column => column.position === prevColumn.position);
runInAction(() => {
for (const constraint of source.options!.constraints) {
if (!hasConstraintIdentity(constraint)) {
resetDataFilterState(source);
return;
}
if (!column || column.label !== prevColumn.label) {
column = result.data?.columns?.find(column => column.label === prevColumn.label);
}
const initialPosition = constraint.attributePosition;
const initialName = constraint.attributeName;
const resolvedColumn = resolveConstraintColumn(columns, initialName, initialPosition);
if (!resolvedColumn) {
resetDataFilterState(source);
return;
}
constraint.attributeName = resolvedColumn.name;
constraint.attributePosition = resolvedColumn.position;
if (column && prevColumn.position !== column.position) {
const prevConstraint = source.prevOptions?.constraints.find(
prevConstraint => prevConstraint.attributePosition === constraint.attributePosition,
prevConstraint => prevConstraint.attributePosition === initialPosition && prevConstraint.attributeName === initialName,
);
constraint.attributePosition = column.position;
if (prevConstraint) {
prevConstraint.attributeName = constraint.attributeName;
prevConstraint.attributePosition = constraint.attributePosition;
}
}
}
});
}
function resetDataFilterState(source: IDatabaseDataSource<IDatabaseDataOptions, IDatabaseResultSet>): void {
source.options!.constraints = [];
source.options!.whereFilter = '';
source.persistedState.delete(CONSTRAINTS_KEY);
source.persistedState.delete(WHERE_FILTER_KEY);
}
function resolveConstraintColumn(
columns: SqlResultColumn[],
attributeName: string,
attributePosition: number,
): (SqlResultColumn & { name: string; position: number }) | undefined {
return columns.find(
(column): column is SqlResultColumn & { name: string; position: number } =>
typeof column.name === 'string' && typeof column.position === 'number' && column.position === attributePosition && column.name === attributeName,
);
}
function hasConstraintIdentity(
constraint: SqlDataFilterConstraint,
): constraint is SqlDataFilterConstraint & { attributeName: string; attributePosition: number } {
return typeof constraint.attributeName === 'string' && constraint.attributeName.length > 0 && typeof constraint.attributePosition === 'number';
}
export function nullOperationsFilter(operation: DataTypeLogicalOperation): boolean {
@@ -306,12 +395,12 @@ export function getNextOrder(order: Order): Order {
}
}
export function wrapOperationArgument(operationId: string, argument: any): string {
export function wrapOperationArgument(operationId: string, argument: unknown): string {
if (operationId === 'LIKE') {
return `%${argument}%`;
}
return argument;
return String(argument);
}
export function isFilterConstraint(constraint: SqlDataFilterConstraint): boolean {
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -5,8 +5,10 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { action, computed, makeObservable, observable, ObservableSet } from 'mobx';
import { action, computed, makeObservable } from 'mobx';
import { isArraysEqual } from '@cloudbeaver/core-utils';
import { isDefined } from '@dbeaver/js-helpers';
import { DatabaseDataAction } from '../../DatabaseDataAction.js';
import { IDatabaseDataSource } from '../../IDatabaseDataSource.js';
import type { IGridColumnKey, IGridDataKey, IGridRowKey } from './IGridDataKey.js';
@@ -22,6 +24,34 @@ import { IDatabaseDataResultAction } from '../IDatabaseDataResultAction.js';
import { IDatabaseDataEditAction } from '../IDatabaseDataEditAction.js';
import type { IDatabaseValueHolder } from '../IDatabaseValueHolder.js';
const PINNED_COLUMNS_KEY = 'pinnedColumns';
const COLUMN_ORDER_KEY = 'columnOrder';
interface IColumnRef {
name: string;
position: number;
}
function isColumnRef(value: unknown): value is IColumnRef {
return (
typeof value === 'object' &&
value !== null &&
'name' in value &&
'position' in value &&
typeof value.name === 'string' &&
value.name.length > 0 &&
typeof value.position === 'number'
);
}
function isSameRef(left: IColumnRef, right: IColumnRef): boolean {
return left.name === right.name && left.position === right.position;
}
function refKey(ref: IColumnRef): string {
return `${ref.name}\0${ref.position}`;
}
@injectable(() => [IDatabaseDataSource, IDatabaseDataResult, IDatabaseDataResultAction, IDatabaseDataEditAction])
export class GridViewAction<
TColumn = unknown,
@@ -51,8 +81,25 @@ export class GridViewAction<
return this.columnKeys.map(this.mapColumn, this);
}
private columnsOrder: number[];
readonly pinnedColumns: ObservableSet<string>;
get columnsOrder(): number[] {
const stored = this.readRefs(COLUMN_ORDER_KEY);
if (stored.length === 0) {
return this.defaultOrder;
}
return this.resolveRefs(stored) ?? this.defaultOrder;
}
get pinnedColumns(): ReadonlySet<string> {
const resolved = this.resolveRefs(this.readRefs(PINNED_COLUMNS_KEY));
const pinned = new Set<string>();
if (resolved) {
for (const index of resolved) {
pinned.add(GridDataKeysUtils.serialize({ index }));
}
}
return pinned;
}
protected readonly data: GridDataResultAction<TColumn, TRow, TKey, TCell, TResult>;
protected readonly editor?: GridEditAction<TColumn, TRow, TKey, TCell, TResult>;
@@ -65,12 +112,10 @@ export class GridViewAction<
super(source, result);
this.data = data as GridDataResultAction<TColumn, TRow, TKey, TCell, TResult>;
this.editor = editor as GridEditAction<TColumn, TRow, TKey, TCell, TResult> | undefined;
this.columnsOrder = this.data.columns.map((key, index) => index);
this.pinnedColumns = observable.set<string>();
makeObservable<this, 'columnsOrder' | 'pinnedColumns'>(this, {
columnsOrder: observable,
pinnedColumns: observable,
makeObservable(this, {
columnsOrder: computed,
pinnedColumns: computed,
setColumnOrder: action,
pinColumns: action,
unpinColumns: action,
@@ -103,14 +148,24 @@ export class GridViewAction<
}
setColumnOrder(key: IGridColumnKey, index: number): void {
const columnIndex = this.columnDataIndex(key);
if (columnIndex === -1) {
const ref = this.getColumnRef(key);
if (!ref) {
return;
}
this.columnsOrder.splice(this.columnsOrder.indexOf(columnIndex), 1);
this.columnsOrder.splice(index, 0, columnIndex);
const current = this.columnKeys.map(k => this.getColumnRef(k)).filter(isDefined);
const from = current.findIndex(r => isSameRef(r, ref));
if (from === -1) {
return;
}
current.splice(from, 1);
current.splice(index, 0, ref);
const defaults = this.data.columns.map((_, i) => this.getColumnRef({ index: i })).filter(isDefined);
const isDefault = isArraysEqual(current, defaults, isSameRef, true);
this.source.persistedState.set(COLUMN_ORDER_KEY, isDefault ? [] : current);
}
columnIndex(key: IGridColumnKey): number {
@@ -179,32 +234,50 @@ export class GridViewAction<
}
pinColumns(keys: IGridColumnKey[]): void {
for (const key of keys) {
const serializedKey = GridDataKeysUtils.serialize(key);
this.pinnedColumns.add(serializedKey);
}
this.mutatePinned(columns => {
for (const key of keys) {
const ref = this.getColumnRef(key);
if (ref && !columns.some(r => isSameRef(r, ref))) {
columns.push(ref);
}
}
});
}
unpinColumns(keys: IGridColumnKey[]): void {
for (const key of keys) {
const serializedKey = GridDataKeysUtils.serialize(key);
this.pinnedColumns.delete(serializedKey);
}
this.mutatePinned(columns => {
for (const key of keys) {
const ref = this.getColumnRef(key);
if (!ref) {
continue;
}
const idx = columns.findIndex(r => isSameRef(r, ref));
if (idx !== -1) {
columns.splice(idx, 1);
}
}
});
}
unpinAllColumns(): void {
this.pinnedColumns.clear();
this.source.persistedState.set(PINNED_COLUMNS_KEY, []);
}
isColumnPinned(key: IGridColumnKey): boolean {
const serializedKey = GridDataKeysUtils.serialize(key);
return this.pinnedColumns.has(serializedKey);
return this.pinnedColumns.has(GridDataKeysUtils.serialize(key));
}
hasPinnedColumns(): boolean {
return this.pinnedColumns.size > 0;
}
getPinnedColumnNames(): string[] {
return this.columnKeys
.filter(key => this.isColumnPinned(key))
.map(key => this.getColumnName(key))
.filter(isDefined);
}
protected mapRow(row: IGridRowKey): TCell[] {
const edited = this.editor?.getRow(row);
@@ -228,8 +301,83 @@ export class GridViewAction<
override updateResult(result: TResult, index: number): void {
super.updateResult(result, index);
if (this.columnsOrder.length !== this.data.columns.length) {
this.columnsOrder = this.data.columns.map((key, index) => index);
for (const key of [COLUMN_ORDER_KEY, PINNED_COLUMNS_KEY]) {
const stored = this.readRefs(key);
if (stored.length > 0 && this.resolveRefs(stored) === null) {
this.source.persistedState.set(key, []);
}
}
}
private get defaultOrder(): number[] {
return this.data.columns.map((_, i) => i);
}
private getColumnRef(key: IGridColumnKey): IColumnRef | undefined {
const name = this.getColumnName(key);
const column = this.getColumn(key);
if (!name || typeof column !== 'object' || column === null || !('position' in column) || typeof column.position !== 'number') {
return undefined;
}
return { name, position: column.position };
}
private readRefs(key: string): IColumnRef[] {
const stored = this.source.persistedState.get<unknown>(key);
if (!Array.isArray(stored)) {
return [];
}
return stored.filter(isColumnRef);
}
private mutatePinned(mutate: (columns: IColumnRef[]) => void): void {
const columns = this.columnKeys
.filter(key => this.isColumnPinned(key))
.map(key => this.getColumnRef(key))
.filter(isDefined);
mutate(columns);
this.source.persistedState.set(PINNED_COLUMNS_KEY, columns);
}
private resolveRefs(refs: IColumnRef[]): number[] | null {
if (refs.length === 0) {
return [];
}
const byKey = new Map<string, number[]>();
for (let i = 0; i < this.data.columns.length; i++) {
const current = this.getColumnRef({ index: i });
if (!current) {
continue;
}
const k = refKey(current);
const list = byKey.get(k);
if (list) {
list.push(i);
} else {
byKey.set(k, [i]);
}
}
const resolved: number[] = [];
const used = new Set<number>();
for (const ref of refs) {
const candidates = byKey.get(refKey(ref));
if (!candidates) {
return null;
}
const available = candidates.filter(i => !used.has(i));
if (available.length !== 1) {
return null;
}
resolved.push(available[0]!);
used.add(available[0]!);
}
return resolved;
}
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -11,6 +11,7 @@ import { withExternal, type IServiceProvider, type IServiceScope, type SingleSer
import { Executor, ExecutorInterrupter, type IExecutor, type ISyncExecutor, type ITask, SyncExecutor, Task } from '@cloudbeaver/core-executor';
import { ResultDataFormat } from '@cloudbeaver/core-sdk';
import { DatabasePersistedStateStore } from './DatabasePersistedStateStore.js';
import { IDatabaseDataActions } from './IDatabaseDataActions.js';
import type { IDatabaseDataResult } from './IDatabaseDataResult.js';
import {
@@ -32,7 +33,8 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
offset: number;
count: number;
prevOptions: Readonly<TOptions> | null;
options: TOptions | null;
protected _options: TOptions | null;
readonly persistedState: DatabasePersistedStateStore;
requestInfo: IRequestInfo;
error: Error | null;
private readonly features: Set<DatabaseDataFeature | string>;
@@ -53,6 +55,10 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
return false;
}
get options(): TOptions | null {
return this._options;
}
readonly serviceProvider: IServiceProvider;
protected disabled: boolean;
private lastAction: () => Promise<void>;
@@ -78,7 +84,8 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
this.offset = 0;
this.count = 0;
this.prevOptions = null;
this.options = null;
this._options = null;
this.persistedState = new DatabasePersistedStateStore();
this.disabled = false;
this.outdated = true;
this.constraintsAvailable = true;
@@ -98,7 +105,7 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
this.error = null;
this.lastAction = this.requestData.bind(this);
makeObservable<DatabaseDataSource<TOptions, TResult>, 'disabled' | 'features' | 'activeOperationStack' | 'outdated'>(this, {
makeObservable<DatabaseDataSource<TOptions, TResult>, 'disabled' | 'features' | 'activeOperationStack' | 'outdated' | '_options'>(this, {
access: observable,
dataFormat: observable,
features: observable,
@@ -107,7 +114,7 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
offset: observable,
count: observable,
prevOptions: observable,
options: observable,
_options: observable,
requestInfo: observable,
error: observable.ref,
disabled: observable,
@@ -229,10 +236,18 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
}
setOptions(options: TOptions): this {
this.options = options;
this._options = options;
return this;
}
loadPersistedState(state: Record<string, unknown>): this {
this.persistedState.setStore(state);
this.onPersistedStateLoaded();
return this;
}
protected onPersistedStateLoaded(): void {}
setDataFormat(dataFormat: ResultDataFormat): this {
this.dataFormat = dataFormat;
return this;
@@ -300,7 +315,7 @@ export abstract class DatabaseDataSource<TOptions, TResult extends IDatabaseData
this.lastAction = this.refreshData.bind(this);
if (this.prevOptions) {
this.options = toJS(this.prevOptions);
this._options = toJS(this.prevOptions);
}
return this.requestDataAction();
@@ -0,0 +1,43 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observable } from 'mobx';
import type { IDatabasePersistedStateStore } from './IDatabasePersistedStateStore.js';
export class DatabasePersistedStateStore implements IDatabasePersistedStateStore {
private readonly store = observable.map<string, unknown>();
private external: Record<string, unknown> | null = null;
setStore(external: Record<string, unknown>): void {
this.external = external;
this.store.replace(Object.entries(external));
}
has(key: string): boolean {
return this.store.has(key);
}
get<T>(key: string): T | undefined {
return this.store.get(key) as T | undefined;
}
set(key: string, value: unknown): void {
this.store.set(key, value);
// Mirror into the external tab-state object so it persists on serialization.
if (this.external) {
this.external[key] = value;
}
}
delete(key: string): void {
this.store.delete(key);
if (this.external) {
delete this.external[key];
}
}
}
@@ -10,6 +10,7 @@ import type { IExecutor, ISyncExecutor } from '@cloudbeaver/core-executor';
import { type TLocalizationToken } from '@cloudbeaver/core-localization';
import type { ResultDataFormat } from '@cloudbeaver/core-sdk';
import type { IDatabasePersistedStateStore } from './IDatabasePersistedStateStore.js';
import type { IDatabaseDataActions } from './IDatabaseDataActions.js';
import type { IDatabaseDataResult } from './IDatabaseDataResult.js';
@@ -80,6 +81,7 @@ export interface IDatabaseDataSource<TOptions = unknown, TResult extends IDataba
/** Options of the previous request */
readonly prevOptions: Readonly<TOptions> | null;
readonly options: TOptions | null;
readonly persistedState: IDatabasePersistedStateStore;
readonly requestInfo: IRequestInfo;
readonly error: Error | null;
readonly canCancel: boolean;
@@ -116,6 +118,7 @@ export interface IDatabaseDataSource<TOptions = unknown, TResult extends IDataba
setAccess: (access: DatabaseDataAccessMode) => this;
setSlice: (offset: number, count: number) => this;
setOptions: (options: TOptions) => this;
loadPersistedState: (state: Record<string, unknown>) => this;
setDataFormat: (dataFormat: ResultDataFormat) => this;
setSupportedDataFormats: (dataFormats: ResultDataFormat[]) => this;
setFeature: (feature: DatabaseDataFeature) => this;
@@ -0,0 +1,15 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export interface IDatabasePersistedStateStore {
get<T>(key: string): T | undefined;
set(key: string, value: unknown): void;
delete(key: string): void;
has(key: string): boolean;
setStore(store: Record<string, unknown>): void;
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -10,4 +10,5 @@ export interface IDataViewerPageState {
resultIndex: number;
presentationId: string;
valuePresentationId: string | null;
persistedState: Record<string, unknown>;
}
@@ -5,7 +5,7 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { makeObservable, observable } from 'mobx';
import { autorun, type IReactionDisposer, makeObservable, observable } from 'mobx';
import type { IConnectionExecutionContext, IConnectionExecutionContextInfo } from '@cloudbeaver/core-connections';
import type { IServiceProvider } from '@cloudbeaver/core-di';
@@ -16,7 +16,11 @@ import type { GraphQLService, SqlRowIdentifier, SqlRowIdentifierState } from '@c
import { DatabaseDataSource } from '../DatabaseDataModel/DatabaseDataSource.js';
import { type IDatabaseDataOptions } from '../DatabaseDataModel/IDatabaseDataOptions.js';
import type { IDatabaseResultSet } from '../DatabaseDataModel/IDatabaseResultSet.js';
import { DatabaseDataConstraintAction } from '../DatabaseDataModel/Actions/DatabaseDataConstraintAction.js';
import {
applyPersistedDataFilterConstraints,
DatabaseDataConstraintAction,
persistDataFilterConstraints,
} from '../DatabaseDataModel/Actions/DatabaseDataConstraintAction.js';
import { DocumentDataAction } from '../DatabaseDataModel/Actions/Document/DocumentDataAction.js';
import { DocumentEditAction } from '../DatabaseDataModel/Actions/Document/DocumentEditAction.js';
import { IDatabaseDataCacheAction } from '../DatabaseDataModel/Actions/IDatabaseDataCacheAction.js';
@@ -39,10 +43,13 @@ export interface IRowIdentifierInfo {
identifier: SqlRowIdentifier | null;
}
export abstract class ResultSetDataSource<TOptions = IDatabaseDataOptions> extends DatabaseDataSource<TOptions, IDatabaseResultSet> {
export abstract class ResultSetDataSource<TOptions extends IDatabaseDataOptions = IDatabaseDataOptions>
extends DatabaseDataSource<TOptions, IDatabaseResultSet>
{
executionContext: IConnectionExecutionContext | null;
totalCountRequestTask: ITask<number> | null;
private keepExecutionContextOnDispose: boolean;
private readonly persistConstraintsDisposer: IReactionDisposer;
constructor(
override readonly serviceProvider: IServiceProvider,
@@ -71,6 +78,12 @@ export abstract class ResultSetDataSource<TOptions = IDatabaseDataOptions> exten
totalCountRequestTask: observable.ref,
executionContext: observable,
});
this.persistConstraintsDisposer = autorun(() => persistDataFilterConstraints(this));
}
protected override onPersistedStateLoaded(): void {
applyPersistedDataFilterConstraints(this);
}
override isReadonly(resultIndex: number): boolean {
@@ -147,6 +160,7 @@ export abstract class ResultSetDataSource<TOptions = IDatabaseDataOptions> exten
}
override async dispose(): Promise<void> {
this.persistConstraintsDisposer();
await super.dispose();
if (this.keepExecutionContextOnDispose) {
await this.closeResults(this.results);
@@ -226,6 +240,8 @@ export abstract class ResultSetDataSource<TOptions = IDatabaseDataOptions> exten
}
}
export function isResultSetDataSource<T = IDatabaseDataOptions>(dataSource: any): dataSource is ResultSetDataSource<T> {
export function isResultSetDataSource<T extends IDatabaseDataOptions = IDatabaseDataOptions>(
dataSource: unknown,
): dataSource is ResultSetDataSource<T> {
return dataSource instanceof ResultSetDataSource;
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2026 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -10,7 +10,7 @@ import { type IDatabaseDataModel } from '../DatabaseDataModel/IDatabaseDataModel
import { type IDatabaseDataOptions } from '../DatabaseDataModel/IDatabaseDataOptions.js';
import { ResultSetDataSource } from './ResultSetDataSource.js';
export function isResultSetDataModel<T = IDatabaseDataOptions>(
export function isResultSetDataModel<T extends IDatabaseDataOptions = IDatabaseDataOptions>(
dataModel: IDatabaseDataModel<any> | undefined | null,
): dataModel is IDatabaseDataModel<ResultSetDataSource<T>> {
return dataModel instanceof DatabaseDataModel && dataModel.source instanceof ResultSetDataSource;
@@ -49,6 +49,8 @@ export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataContentAction.
export * from './DatabaseDataModel/Actions/DatabaseDataResultAction.js';
export * from './DatabaseDataModel/Actions/DatabaseEditAction.js';
export * from './DatabaseDataModel/Actions/General/DatabaseMetadataAction.js';
export * from './DatabaseDataModel/DatabasePersistedStateStore.js';
export * from './DatabaseDataModel/IDatabasePersistedStateStore.js';
export * from './DatabaseDataModel/Actions/DatabaseSelectAction.js';
export * from './DatabaseDataModel/Actions/IDatabaseDataCacheAction.js';
export * from './DatabaseDataModel/Actions/IDatabaseDataViewAction.js';
@@ -210,8 +210,7 @@ export class QueryDataSource<TOptions extends IDataQueryOptions = IDataQueryOpti
if (this.options?.query !== options.query) {
this.resetQueryParameters();
}
this.options = options;
return this;
return super.setOptions(options);
}
async request(prevResults: IDatabaseResultSet[]): Promise<IDatabaseResultSet[]> {