diff --git a/packages/api/src/controllers/serverConnections.js b/packages/api/src/controllers/serverConnections.js index 86e461feb..db0bea5b6 100644 --- a/packages/api/src/controllers/serverConnections.js +++ b/packages/api/src/controllers/serverConnections.js @@ -270,10 +270,40 @@ module.exports = { serverSummary_meta: true, async serverSummary({ conid }, req) { + logger.info({ conid }, 'DBGM-00260 Processing server summary'); testConnectionPermission(conid, req); return this.loadDataCore('serverSummary', { conid }); }, + listDatabaseProcesses_meta: true, + async listDatabaseProcesses(ctx, req) { + const { conid } = ctx; + // logger.info({ conid }, 'DBGM-00261 Listing processes of database server'); + testConnectionPermission(conid, req); + + const opened = await this.ensureOpened(conid); + if (!opened) { + return null; + } + if (opened.connection.isReadOnly) return false; + + return this.sendRequest(opened, { msgtype: 'listDatabaseProcesses' }); + }, + + killDatabaseProcess_meta: true, + async killDatabaseProcess(ctx, req) { + const { conid, pid } = ctx; + testConnectionPermission(conid, req); + + const opened = await this.ensureOpened(conid); + if (!opened) { + return null; + } + if (opened.connection.isReadOnly) return false; + + return this.sendRequest(opened, { msgtype: 'killDatabaseProcess', pid }); + }, + summaryCommand_meta: true, async summaryCommand({ conid, command, row }, req) { testConnectionPermission(conid, req); diff --git a/packages/api/src/proc/serverConnectionProcess.js b/packages/api/src/proc/serverConnectionProcess.js index d58aa18ed..f9a2325c1 100644 --- a/packages/api/src/proc/serverConnectionProcess.js +++ b/packages/api/src/proc/serverConnectionProcess.js @@ -146,6 +146,30 @@ async function handleServerSummary({ msgid }) { return handleDriverDataCore(msgid, driver => driver.serverSummary(dbhan)); } +async function handleKillDatabaseProcess({ msgid, pid }) { + await waitConnected(); + const driver = requireEngineDriver(storedConnection); + + try { + const result = await driver.killProcess(dbhan, Number(pid)); + process.send({ msgtype: 'response', msgid, result }); + } catch (err) { + process.send({ msgtype: 'response', msgid, errorMessage: err.message }); + } +} + +async function handleListDatabaseProcesses({ msgid }) { + await waitConnected(); + const driver = requireEngineDriver(storedConnection); + + try { + const result = await driver.listProcesses(dbhan); + process.send({ msgtype: 'response', msgid, result }); + } catch (err) { + process.send({ msgtype: 'response', msgid, errorMessage: err.message }); + } +} + async function handleSummaryCommand({ msgid, command, row }) { return handleDriverDataCore(msgid, driver => driver.summaryCommand(dbhan, command, row)); } @@ -154,6 +178,8 @@ const messageHandlers = { connect: handleConnect, ping: handlePing, serverSummary: handleServerSummary, + killDatabaseProcess: handleKillDatabaseProcess, + listDatabaseProcesses: handleListDatabaseProcesses, summaryCommand: handleSummaryCommand, createDatabase: props => handleDatabaseOp('createDatabase', props), dropDatabase: props => handleDatabaseOp('dropDatabase', props), diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index 4d8fd53b5..0daf4c172 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -99,19 +99,46 @@ export interface SupportedDbKeyType { showItemList?: boolean; } +export type DatabaseProcess = { + processId: number; + connectionId: number; + client: string; + operation?: string; + namespace?: string; + command?: any; + runningTime: number; + state?: any; + waitingFor?: boolean; + locks?: any; + progress?: any; +}; + +export type DatabaseVariable = { + variable: string; + value: any; +}; + export interface SqlBackupDumper { run(); } -export interface SummaryColumn { - fieldName: string; - header: string; - dataType: 'string' | 'number' | 'bytes'; +export interface ServerSummaryDatabases { + rows: any[]; + columns: SummaryDatabaseColumn[]; } -export interface ServerSummaryDatabase {} + +export type SummaryDatabaseColumn = { + header: string; + fieldName: string; + type: 'data' | 'fileSize'; + filterable?: boolean; + sortable?: boolean; +}; + export interface ServerSummary { - columns: SummaryColumn[]; - databases: ServerSummaryDatabase[]; + processes: DatabaseProcess[]; + variables: DatabaseVariable[]; + databases: ServerSummaryDatabases; } export type CollectionAggregateFunction = 'count' | 'sum' | 'avg' | 'min' | 'max'; @@ -161,12 +188,12 @@ export interface FilterBehaviourProvider { getFilterBehaviour(dataType: string, standardFilterBehaviours: { [id: string]: FilterBehaviour }): FilterBehaviour; } -export interface DatabaseHandle { +export interface DatabaseHandle { client: TClient; database?: string; conid?: string; feedback?: (message: any) => void; - getDatabase?: () => any; + getDatabase?: () => TDataBase; connectionType?: string; treeKeySeparator?: string; } @@ -196,7 +223,7 @@ export interface RestoreDatabaseSettings extends BackupRestoreSettingsBase { inputFile: string; } -export interface EngineDriver extends FilterBehaviourProvider { +export interface EngineDriver extends FilterBehaviourProvider { engine: string; title: string; defaultPort?: number; @@ -242,61 +269,88 @@ export interface EngineDriver extends FilterBehaviourProvider { defaultSocketPath?: string; authTypeLabel?: string; importExportArgs?: any[]; - connect({ server, port, user, password, database, connectionDefinition }): Promise>; - close(dbhan: DatabaseHandle): Promise; - query(dbhan: DatabaseHandle, sql: string, options?: QueryOptions): Promise; - stream(dbhan: DatabaseHandle, sql: string, options: StreamOptions); - readQuery(dbhan: DatabaseHandle, sql: string, structure?: TableInfo): Promise; - readJsonQuery(dbhan: DatabaseHandle, query: any, structure?: TableInfo): Promise; + connect({ + server, + port, + user, + password, + database, + connectionDefinition, + }): Promise>; + close(dbhan: DatabaseHandle): Promise; + query(dbhan: DatabaseHandle, sql: string, options?: QueryOptions): Promise; + stream(dbhan: DatabaseHandle, sql: string, options: StreamOptions); + readQuery(dbhan: DatabaseHandle, sql: string, structure?: TableInfo): Promise; + readJsonQuery(dbhan: DatabaseHandle, query: any, structure?: TableInfo): Promise; // eg. PostgreSQL COPY FROM stdin - writeQueryFromStream(dbhan: DatabaseHandle, sql: string): Promise; - writeTable(dbhan: DatabaseHandle, name: NamedObjectInfo, options: WriteTableOptions): Promise; + writeQueryFromStream(dbhan: DatabaseHandle, sql: string): Promise; + writeTable( + dbhan: DatabaseHandle, + name: NamedObjectInfo, + options: WriteTableOptions + ): Promise; analyseSingleObject( - dbhan: DatabaseHandle, + dbhan: DatabaseHandle, name: NamedObjectInfo, objectTypeField: keyof DatabaseInfo ): Promise; - analyseSingleTable(dbhan: DatabaseHandle, name: NamedObjectInfo): Promise; - getVersion(dbhan: DatabaseHandle): Promise<{ version: string; versionText?: string }>; - listDatabases(dbhan: DatabaseHandle): Promise< + analyseSingleTable(dbhan: DatabaseHandle, name: NamedObjectInfo): Promise; + getVersion(dbhan: DatabaseHandle): Promise<{ version: string; versionText?: string }>; + listDatabases(dbhan: DatabaseHandle): Promise< { name: string; + sizeOnDisk?: number; + empty?: boolean; }[] >; - loadKeys(dbhan: DatabaseHandle, root: string, filter?: string): Promise; - scanKeys(dbhan: DatabaseHandle, root: string, pattern: string, cursor: string, count: number): Promise; - exportKeys(dbhan: DatabaseHandle, options: {}): Promise; - loadKeyInfo(dbhan: DatabaseHandle, key): Promise; - loadKeyTableRange(dbhan: DatabaseHandle, key, cursor, count): Promise; + loadKeys(dbhan: DatabaseHandle, root: string, filter?: string): Promise; + scanKeys( + dbhan: DatabaseHandle, + root: string, + pattern: string, + cursor: string, + count: number + ): Promise; + exportKeys(dbhan: DatabaseHandle, options: {}): Promise; + loadKeyInfo(dbhan: DatabaseHandle, key): Promise; + loadKeyTableRange(dbhan: DatabaseHandle, key, cursor, count): Promise; loadFieldValues( - dbhan: DatabaseHandle, + dbhan: DatabaseHandle, name: NamedObjectInfo, field: string, search: string, dataType: string ): Promise; - analyseFull(dbhan: DatabaseHandle, serverVersion): Promise; - analyseIncremental(dbhan: DatabaseHandle, structure: DatabaseInfo, serverVersion): Promise; + analyseFull(dbhan: DatabaseHandle, serverVersion): Promise; + analyseIncremental( + dbhan: DatabaseHandle, + structure: DatabaseInfo, + serverVersion + ): Promise; dialect: SqlDialect; dialectByVersion(version): SqlDialect; createDumper(options = null): SqlDumper; - createBackupDumper(dbhan: DatabaseHandle, options): Promise; + createBackupDumper(dbhan: DatabaseHandle, options): Promise; getAuthTypes(): EngineAuthType[]; - readCollection(dbhan: DatabaseHandle, options: ReadCollectionOptions): Promise; - updateCollection(dbhan: DatabaseHandle, changeSet: any): Promise; + readCollection(dbhan: DatabaseHandle, options: ReadCollectionOptions): Promise; + updateCollection(dbhan: DatabaseHandle, changeSet: any): Promise; getCollectionUpdateScript(changeSet: any, collectionInfo: CollectionInfo): string; - createDatabase(dbhan: DatabaseHandle, name: string): Promise; - dropDatabase(dbhan: DatabaseHandle, name: string): Promise; + createDatabase(dbhan: DatabaseHandle, name: string): Promise; + dropDatabase(dbhan: DatabaseHandle, name: string): Promise; getQuerySplitterOptions(usage: 'stream' | 'script' | 'editor' | 'import'): any; - script(dbhan: DatabaseHandle, sql: string, options?: RunScriptOptions): Promise; - operation(dbhan: DatabaseHandle, operation: CollectionOperationInfo, options?: RunScriptOptions): Promise; + script(dbhan: DatabaseHandle, sql: string, options?: RunScriptOptions): Promise; + operation( + dbhan: DatabaseHandle, + operation: CollectionOperationInfo, + options?: RunScriptOptions + ): Promise; getNewObjectTemplates(): NewObjectTemplate[]; // direct call of dbhan.client method, only some methods could be supported, on only some drivers - callMethod(dbhan: DatabaseHandle, method, args); - serverSummary(dbhan: DatabaseHandle): Promise; - summaryCommand(dbhan: DatabaseHandle, command, row): Promise; - startProfiler(dbhan: DatabaseHandle, options): Promise; - stopProfiler(dbhan: DatabaseHandle, profiler): Promise; + callMethod(dbhan: DatabaseHandle, method, args); + serverSummary(dbhan: DatabaseHandle): Promise; + summaryCommand(dbhan: DatabaseHandle, command, row): Promise; + startProfiler(dbhan: DatabaseHandle, options): Promise; + stopProfiler(dbhan: DatabaseHandle, profiler): Promise; getRedirectAuthUrl(connection, options): Promise<{ url: string; sid: string }>; getAuthTokenFromCode(connection, options): Promise; getAccessTokenFromAuth(connection, req): Promise; @@ -313,7 +367,10 @@ export interface EngineDriver extends FilterBehaviourProvider { adaptTableInfo(table: TableInfo): TableInfo; // simple data type adapter adaptDataType(dataType: string): string; - listSchemas(dbhan: DatabaseHandle): Promise; + listSchemas(dbhan: DatabaseHandle): Promise; + listProcesses(dbhan: DatabaseHandle): Promise; + listVariables(dbhan: DatabaseHandle): Promise; + killProcess(dbhan: DatabaseHandle, pid: number): Promise; backupDatabaseCommand( connection: any, settings: BackupDatabaseSettings, @@ -337,7 +394,7 @@ export interface EngineDriver extends FilterBehaviourProvider { analyserClass?: any; dumperClass?: any; singleConnectionOnly?: boolean; - getLogDbInfo(dbhan: DatabaseHandle): { + getLogDbInfo(dbhan: DatabaseHandle): { database?: string; engine: string; conid?: string; diff --git a/packages/web/src/buttons/CtaButton.svelte b/packages/web/src/buttons/CtaButton.svelte new file mode 100644 index 000000000..2b77f7ca8 --- /dev/null +++ b/packages/web/src/buttons/CtaButton.svelte @@ -0,0 +1,46 @@ + + + + + diff --git a/packages/web/src/elements/TabControl.svelte b/packages/web/src/elements/TabControl.svelte index 4c7ee15aa..87cc6dff6 100644 --- a/packages/web/src/elements/TabControl.svelte +++ b/packages/web/src/elements/TabControl.svelte @@ -15,7 +15,10 @@ export let menu = null; export let isInline = false; export let containerMaxWidth = undefined; + export let containerMaxHeight = undefined; export let flex1 = true; + export let flexColContainer = true; + export let maxHeight100 = false; export let contentTestId = undefined; export let inlineTabs = false; export let onUserChange = null; @@ -28,7 +31,7 @@ } -
+
{#each _.compact(tabs) as tab, index}
-
+
{#each _.compact(tabs) as tab, index} -
- +
+ {#if tab.slot != null} {#if tab.slot == 0} {:else if tab.slot == 1} @@ -83,6 +98,10 @@ max-width: 100%; } + .main.maxHeight100 { + max-height: 100%; + } + .tabs { display: flex; height: var(--dim-tabs-height); @@ -132,6 +151,15 @@ position: relative; } + .container.maxHeight100 { + max-height: 100%; + } + + .container.flexColContainer { + display: flex; + flex-direction: column; + } + .container:not(.isInline) { position: absolute; display: flex; diff --git a/packages/web/src/elements/TableControl.svelte b/packages/web/src/elements/TableControl.svelte index 6da2e8914..f760c30b9 100644 --- a/packages/web/src/elements/TableControl.svelte +++ b/packages/web/src/elements/TableControl.svelte @@ -350,7 +350,7 @@ {#if col.component} {:else if col.formatter} - {col.formatter(row)} + {col.formatter(row, col)} {:else if col.slot != null} {#key row[col.slotKey] || 'key'} {#if col.slot == -1} diff --git a/packages/web/src/jsontree/JSONArrayNode.svelte b/packages/web/src/jsontree/JSONArrayNode.svelte index 1f57ac377..205ff32f8 100644 --- a/packages/web/src/jsontree/JSONArrayNode.svelte +++ b/packages/web/src/jsontree/JSONArrayNode.svelte @@ -3,6 +3,8 @@ export let key, value, isParentExpanded, isParentArray; export let expanded = false; + export let labelOverride = null; + export let hideKey = false; const filteredKey = new Set(['length']); $: keys = Object.getOwnPropertyNames(value); @@ -22,8 +24,10 @@ {keys} {previewKeys} {getValue} - label="Array({value.length})" + label={labelOverride || `Array(${value.length})`} bracketOpen="[" bracketClose="]" elementValue={value} -/> \ No newline at end of file + {labelOverride} + {hideKey} +/> diff --git a/packages/web/src/jsontree/JSONIterableArrayNode.svelte b/packages/web/src/jsontree/JSONIterableArrayNode.svelte index 927455bce..75bc356ea 100644 --- a/packages/web/src/jsontree/JSONIterableArrayNode.svelte +++ b/packages/web/src/jsontree/JSONIterableArrayNode.svelte @@ -2,6 +2,8 @@ import JSONNested from './JSONNested.svelte'; export let key, value, isParentExpanded, isParentArray, nodeType; + export let labelOverride = null; + export let hideKey = false; let keys = []; @@ -29,7 +31,9 @@ {getKey} {getValue} isArray={true} - label="{nodeType}({keys.length})" + label={labelOverride || `${nodeType}(${keys.length})`} bracketOpen={'{'} bracketClose={'}'} -/> \ No newline at end of file + {labelOverride} + {hideKey} +/> diff --git a/packages/web/src/jsontree/JSONIterableMapNode.svelte b/packages/web/src/jsontree/JSONIterableMapNode.svelte index 907ee0326..8c0aad697 100644 --- a/packages/web/src/jsontree/JSONIterableMapNode.svelte +++ b/packages/web/src/jsontree/JSONIterableMapNode.svelte @@ -3,6 +3,8 @@ import MapEntry from './utils/MapEntry' export let key, value, isParentExpanded, isParentArray, nodeType; + export let labelOverride = null; + export let hideKey = false; let keys = []; @@ -28,8 +30,10 @@ {keys} {getKey} {getValue} - label="{nodeType}({keys.length})" + label={labelOverride || `${nodeType}(${keys.length})`} colon="" bracketOpen={'{'} bracketClose={'}'} + {labelOverride} + {hideKey} /> diff --git a/packages/web/src/jsontree/JSONMapEntryNode.svelte b/packages/web/src/jsontree/JSONMapEntryNode.svelte index cccd283fa..1f012e10b 100644 --- a/packages/web/src/jsontree/JSONMapEntryNode.svelte +++ b/packages/web/src/jsontree/JSONMapEntryNode.svelte @@ -3,6 +3,8 @@ export let key, value, isParentExpanded, isParentArray; export let expanded = false; + export let hideKey = false; + export let labelOverride = null; const keys = ['key', 'value']; @@ -17,7 +19,9 @@ key={isParentExpanded ? String(key) : value.key} {keys} {getValue} - label={isParentExpanded ? 'Entry ' : '=> '} + label={labelOverride || (isParentExpanded ? 'Entry ' : '=> ')} bracketOpen={'{'} bracketClose={'}'} -/> \ No newline at end of file + {labelOverride} + {hideKey} +/> diff --git a/packages/web/src/jsontree/JSONNested.svelte b/packages/web/src/jsontree/JSONNested.svelte index 2c0a71bec..0efa828ac 100644 --- a/packages/web/src/jsontree/JSONNested.svelte +++ b/packages/web/src/jsontree/JSONNested.svelte @@ -21,11 +21,14 @@ expandable = true; export let elementValue = null; export let onRootExpandedChanged = null; + export let labelOverride = null; + export let hideKey = false; const context = getContext('json-tree-context-key'); setContext('json-tree-context-key', { ...context, colon }); const elementData = getContext('json-tree-element-data'); const slicedKeyCount = getContext('json-tree-sliced-key-count'); + const keyLabel = labelOverride ?? key; $: slicedKeys = expanded ? keys : previewKeys.slice(0, slicedKeyCount || 5); @@ -56,7 +59,16 @@ {#if expandable && isParentExpanded} {/if} - + {#if !hideKey} + + {/if} {label}{bracketOpen} {#if isParentExpanded} diff --git a/packages/web/src/jsontree/JSONNode.svelte b/packages/web/src/jsontree/JSONNode.svelte index bf526e1ca..a7b179c16 100644 --- a/packages/web/src/jsontree/JSONNode.svelte +++ b/packages/web/src/jsontree/JSONNode.svelte @@ -16,6 +16,7 @@ export let expanded = !!getContext('json-tree-default-expanded'); export let labelOverride = null; export let onRootExpandedChanged = null; + export let hideKey = false; $: nodeType = objType(value); $: componentType = getComponent(nodeType); @@ -85,4 +86,5 @@ {expanded} {labelOverride} {onRootExpandedChanged} + {hideKey} /> diff --git a/packages/web/src/jsontree/JSONObjectNode.svelte b/packages/web/src/jsontree/JSONObjectNode.svelte index 71f0dd536..33320a8ff 100644 --- a/packages/web/src/jsontree/JSONObjectNode.svelte +++ b/packages/web/src/jsontree/JSONObjectNode.svelte @@ -5,6 +5,7 @@ export let expanded = false; export let labelOverride = null; export let onRootExpandedChanged = null; + export let hideKey = false; $: keys = Object.getOwnPropertyNames(value); @@ -26,4 +27,5 @@ bracketClose={'}'} elementValue={value} {onRootExpandedChanged} + {hideKey} /> diff --git a/packages/web/src/jsontree/JSONTree.svelte b/packages/web/src/jsontree/JSONTree.svelte index fddfda32e..311e54c6b 100644 --- a/packages/web/src/jsontree/JSONTree.svelte +++ b/packages/web/src/jsontree/JSONTree.svelte @@ -2,7 +2,6 @@ import JSONNode from './JSONNode.svelte'; import { setContext } from 'svelte'; import contextMenu, { getContextMenu } from '../utility/contextMenu'; - import openNewTab from '../utility/openNewTab'; import _ from 'lodash'; import { copyTextToClipboard } from '../utility/clipboard'; import { openJsonLinesData } from '../utility/openJsonLinesData'; @@ -23,6 +22,7 @@ export let isDeleted = false; export let isInserted = false; export let isModified = false; + export let hideKey = false; const settings = useSettings(); $: wrap = $settings?.['behaviour.jsonPreviewWrap']; @@ -73,6 +73,7 @@ class:wrap > + +
  • + + + {valueGetter ? valueGetter(value) : value} + +
  • + -
  • - - - {valueGetter ? valueGetter(value) : value} - -
  • \ No newline at end of file + diff --git a/packages/web/src/stores.ts b/packages/web/src/stores.ts index badfa1ce7..34188e2cc 100644 --- a/packages/web/src/stores.ts +++ b/packages/web/src/stores.ts @@ -215,6 +215,8 @@ export const connectionAppObjectSearchSettings = writableWithStorage( 'connectionAppObjectSearchSettings2' ); +export const serverSummarySelectedTab = writableWithStorage(0, 'serverSummary.selectedTab'); + let currentThemeValue = null; currentTheme.subscribe(value => { currentThemeValue = value; diff --git a/packages/web/src/tabs/ServerSummaryTab.svelte b/packages/web/src/tabs/ServerSummaryTab.svelte index 0686a5edc..b43d2dec6 100644 --- a/packages/web/src/tabs/ServerSummaryTab.svelte +++ b/packages/web/src/tabs/ServerSummaryTab.svelte @@ -18,17 +18,22 @@ import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte'; import ToolStripContainer from '../buttons/ToolStripContainer.svelte'; import registerCommand from '../commands/registerCommand'; - import Link from '../elements/Link.svelte'; import LoadingInfo from '../elements/LoadingInfo.svelte'; + import TabControl from '../elements/TabControl.svelte'; - import ObjectListControl from '../elements/ObjectListControl.svelte'; import { _t } from '../translations'; import { apiCall } from '../utility/api'; import createActivator, { getActiveComponent } from '../utility/createActivator'; - import formatFileSize from '../utility/formatFileSize'; import openNewTab from '../utility/openNewTab'; + import SummaryVariables from '../widgets/SummaryVariables.svelte'; + import SummaryProcesses from '../widgets/SummaryProcesses.svelte'; + import SummaryDatabases from '../widgets/SummaryDatabases.svelte'; + import { activeTabId, serverSummarySelectedTab } from '../stores'; + import { getContext } from 'svelte'; export let conid; + const tabid = getContext('tabid'); + $: isActiveTab = tabid === $activeTabId; let refreshToken = 0; @@ -75,30 +80,54 @@ {#await apiCall('server-connections/server-summary', { conid, refreshToken })} - + {:then summary} -
    - ({ - ...col, - slot: col.columnType == 'bytes' ? 1 : col.columnType == 'actions' ? 2 : null, - }))} - > - {formatFileSize(row?.[col.fieldName])} - - {#each col.actions as action, index} - {#if index > 0} - | - {/if} - runAction(action, row)}>{action.header} - {/each} - - -
    + {#if 'errorMessage' in summary} +
    +
    +

    {_t('serverSummaryTab.errorTitle', { defaultMessage: 'Error loading server summary' })}

    +

    {summary.errorMessage}

    +
    +
    + {:else} +
    + serverSummarySelectedTab.set(index)} + tabs={[ + { + label: _t('serverSummaryTab.variables', { defaultMessage: 'Variables' }), + component: SummaryVariables, + props: { variables: summary.variables || [] }, + }, + { + label: _t('serverSummaryTab.processes', { defaultMessage: 'Processes' }), + component: SummaryProcesses, + props: { + processes: summary.processes || [], + isSummaryOpened: isActiveTab, + conid, + }, + }, + { + label: _t('serverSummaryTab.databases', { defaultMessage: 'Databases' }), + component: SummaryDatabases, + props: { rows: summary.databases?.rows ?? [], columns: summary.databases?.columns ?? [] }, + }, + ]} + /> +
    + {/if} {/await} @@ -114,10 +143,33 @@ right: 0; bottom: 0; background-color: var(--theme-bg-0); - overflow: auto; + height: 100%; + display: flex; + flex-direction: column; } .action-separator { margin: 0 5px; } + + .error-wrapper { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + } + + .error-message { + background: var(--theme-bg-1); + border: 1px solid var(--theme-border); + border-radius: 4px; + padding: 20px; + max-width: 500px; + text-align: center; + } + + .error-message h3 { + color: var(--theme-font-error); + margin-top: 0; + } diff --git a/packages/web/src/widgets/SummaryDatabases.svelte b/packages/web/src/widgets/SummaryDatabases.svelte new file mode 100644 index 000000000..ccd0b27c5 --- /dev/null +++ b/packages/web/src/widgets/SummaryDatabases.svelte @@ -0,0 +1,36 @@ + + +
    + +
    + + diff --git a/packages/web/src/widgets/SummaryProcesses.svelte b/packages/web/src/widgets/SummaryProcesses.svelte new file mode 100644 index 000000000..29c48230d --- /dev/null +++ b/packages/web/src/widgets/SummaryProcesses.svelte @@ -0,0 +1,206 @@ + + +
    + + +
    + { + selectedProcess = e.detail; + }} + {filters} + stickyHeader + rows={internalProcesses} + columns={[ + { + sortable: true, + filterable: true, + header: _t('summaryProcesses.processId', { defaultMessage: 'Process ID' }), + fieldName: 'processId', + slot: 1, + }, + { + sortable: true, + filterable: true, + header: _t('summaryProcesses.connectionId', { defaultMessage: 'Connection ID' }), + fieldName: 'connectionId', + }, + { + sortable: true, + filterable: true, + header: _t('summaryProcesses.client', { defaultMessage: 'Client' }), + fieldName: 'client', + }, + { + filterable: true, + header: _t('summaryProcesses.operation', { defaultMessage: 'Operation' }), + fieldName: 'operation', + }, + { + sortable: true, + filterable: true, + header: _t('summaryProcesses.namespace', { defaultMessage: 'Namespace' }), + fieldName: 'namespace', + }, + { + sortable: true, + header: _t('summaryProcesses.runningTime', { defaultMessage: 'Running Time' }), + fieldName: 'runningTime', + slot: 2, + }, + { + sortable: true, + filterable: true, + header: _t('summaryProcesses.state', { defaultMessage: 'State' }), + fieldName: 'state', + }, + { + sortable: true, + header: _t('summaryProcesses.waitingFor', { defaultMessage: 'Waiting For' }), + fieldName: 'waitingFor', + slot: 3, + }, + { + header: _t('summaryProcesses.actions', { defaultMessage: 'Actions' }), + fieldName: 'processId', + slot: 0, + }, + ]} + > + + killProcessWithConfirm(row.processId)}> + {_t('common.kill', { defaultMessage: 'Kill' })} + + + + + {row.processId} + + + + {formatRunningTime(row.runningTime)} + + + + {row.waitingFor ? 'Yes' : 'No'} + + +
    +
    + + {#if !!selectedProcess} + + {/if} + +
    +
    + + diff --git a/packages/web/src/widgets/SummaryVariables.svelte b/packages/web/src/widgets/SummaryVariables.svelte new file mode 100644 index 000000000..8f6bdfb25 --- /dev/null +++ b/packages/web/src/widgets/SummaryVariables.svelte @@ -0,0 +1,44 @@ + + +
    + + + + + +
    + + diff --git a/plugins/dbgate-plugin-mongo/src/backend/driver.js b/plugins/dbgate-plugin-mongo/src/backend/driver.js index d275f5611..b48783818 100644 --- a/plugins/dbgate-plugin-mongo/src/backend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/backend/driver.js @@ -6,7 +6,7 @@ const Analyser = require('./Analyser'); const isPromise = require('is-promise'); const { MongoClient, ObjectId, AbstractCursor, Long } = require('mongodb'); const { EJSON } = require('bson'); -const { serializeJsTypesForJsonStringify, deserializeJsTypesFromJsonParse } = require('dbgate-tools'); +const { serializeJsTypesForJsonStringify, deserializeJsTypesFromJsonParse, getLogger } = require('dbgate-tools'); const createBulkInsertStream = require('./createBulkInsertStream'); const { convertToMongoCondition, @@ -16,6 +16,8 @@ const { let isProApp; +const logger = getLogger('mongoDriver'); + function serializeMongoData(row) { return EJSON.serialize( serializeJsTypesForJsonStringify(row, (value) => { @@ -80,7 +82,7 @@ async function getScriptableDb(dbhan) { // } // } -/** @type {import('dbgate-types').EngineDriver} */ +/** @type {import('dbgate-types').EngineDriver} */ const driver = { ...driverBase, analyserClass: Analyser, @@ -193,7 +195,10 @@ const driver = { let exprValue; try { - const serviceProvider = new NodeDriverServiceProvider(dbhan.client, new EventEmitter(), { productDocsLink: '', productName: 'DbGate' }); + const serviceProvider = new NodeDriverServiceProvider(dbhan.client, new EventEmitter(), { + productDocsLink: '', + productName: 'DbGate', + }); const runtime = new ElectronRuntime(serviceProvider); await runtime.evaluate(`use ${dbhan.database}`); exprValue = await runtime.evaluate(sql); @@ -629,86 +634,107 @@ const driver = { }, async serverSummary(dbhan) { - const res = await dbhan.getDatabase().admin().listDatabases(); - const profiling = await Promise.all(res.databases.map((x) => dbhan.client.db(x.name).command({ profile: -1 }))); + const [processes, variables, databases] = await Promise.all([ + this.listProcesses(dbhan), + this.listVariables(dbhan), + this.listDatabases(dbhan), + ]); - function formatProfiling(info) { - switch (info.was) { - case 0: - return 'No profiling'; - case 1: - return `Filtered (>${info.slowms} ms)`; - case 2: - return 'Profile all'; - default: - return '???'; - } - } - - return { - columns: [ - { - fieldName: 'name', - columnType: 'string', - header: 'Name', - }, - { - fieldName: 'sizeOnDisk', - columnType: 'bytes', - header: 'Size', - }, - { - fieldName: 'profiling', - columnType: 'string', - header: 'Profiling', - }, - { - fieldName: 'setProfile', - columnType: 'actions', - header: 'Profiling actions', - actions: [ - { - header: 'Off', - command: 'profileOff', - }, - { - header: 'Filtered', - command: 'profileFiltered', - }, - { - header: 'All', - command: 'profileAll', - }, - // { - // header: 'View', - // openQuery: "db['system.profile'].find()", - // tabTitle: 'Profile data', - // }, - { - header: 'View', - openTab: { - title: 'system.profile', - icon: 'img collection', - tabComponent: 'CollectionDataTab', - props: { - pureName: 'system.profile', - }, - }, - addDbProps: true, - }, - ], - }, - ], - databases: res.databases.map((db, i) => ({ - ...db, - profiling: formatProfiling(profiling[i]), - })), + /** @type {import('dbgate-types').ServerSummary} */ + const data = { + processes, + variables, + databases: { + rows: databases, + columns: [ + { + filterable: true, + sortable: true, + header: 'Name', + fieldName: 'name', + type: 'data', + }, + { + sortable: true, + header: 'Size on disk', + fieldName: 'sizeOnDisk', + type: 'fileSize', + }, + { + filterable: true, + sortable: true, + header: 'Empty', + fieldName: 'empty', + type: 'data', + }, + ], + }, }; + + return data; }, async close(dbhan) { return dbhan.client.close(); }, + + async listProcesses(dbhan) { + const db = dbhan.getDatabase(); + const adminDb = db.admin(); + + const currentOp = await adminDb.command({ + currentOp: { + $all: true, + active: true, + idle: true, + system: true, + killPending: true, + }, + }); + + const processes = currentOp.inprog.map((op) => ({ + processId: op.opid, + connectionId: op.connectionId, + client: op.client, + operation: op.op, + namespace: op.ns, + command: op.command, + runningTime: op.secs_running, + state: op.state, + waitingFor: op.waitingForLock, + locks: op.locks, + progress: op.progress, + })); + + return processes; + }, + + async listVariables(dbhan) { + const db = dbhan.getDatabase(); + const adminDb = db.admin(); + + const variables = await adminDb + .command({ getParameter: '*' }) + .then((params) => + Object.entries(params).map(([key, value]) => ({ variable: key, value: value?.value || value })) + ); + + return variables; + }, + + async killProcess(dbhan, processId) { + const db = dbhan.getDatabase(); + const adminDb = db.admin(); + + const result = await adminDb.command({ + killOp: 1, + op: processId, + }); + + logger.info(`Killed process with ID ${processId}`, result); + + return result; + }, }; driver.initialize = (dbgateEnv) => { diff --git a/plugins/dbgate-plugin-mssql/src/backend/driver.js b/plugins/dbgate-plugin-mssql/src/backend/driver.js index 8d16e35b8..6eeccb4f5 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/driver.js +++ b/plugins/dbgate-plugin-mssql/src/backend/driver.js @@ -9,6 +9,7 @@ const lock = new AsyncLock(); const { tediousConnect, tediousQueryCore, tediousReadQuery, tediousStream } = require('./tediousDriver'); const { nativeConnect, nativeQueryCore, nativeReadQuery, nativeStream } = require('./nativeDriver'); const { getLogger } = global.DBGATE_PACKAGES['dbgate-tools']; +const sql = require('./sql'); const logger = getLogger('mssqlDriver'); @@ -148,9 +149,89 @@ const driver = { return res; }, async listDatabases(dbhan) { - const { rows } = await this.query(dbhan, 'SELECT name FROM sys.databases order by name'); + const { rows } = await this.query(dbhan, sql.listDatabases); return rows; }, + + async listProcesses(dbhan) { + const { rows } = await this.query(dbhan, sql.listProcesses); + return rows; + }, + + async listVariables(dbhan) { + const { rows } = await this.query(dbhan, sql.listVariables); + return rows; + }, + + async killProcess(dbhan, processId) { + await this.query(dbhan, `KILL ${processId}`); + }, + + async serverSummary(dbhan) { + const [variables, processes, databases] = await Promise.all([ + this.listVariables(dbhan), + this.listProcesses(dbhan), + this.listDatabases(dbhan), + ]); + + return { + variables: variables, + processes: processes, + databases: { + rows: databases, + columns: [ + { + filterable: true, + sortable: true, + header: 'Database', + fieldName: 'name', + type: 'data', + }, + { + filterable: true, + sortable: true, + header: 'Status', + fieldName: 'status', + type: 'data', + }, + { + filterable: true, + sortable: true, + header: 'Recovery Model', + fieldName: 'recoveryModel', + type: 'data', + }, + { + filterable: true, + sortable: true, + header: 'Compatibility Level', + fieldName: 'compatibilityLevel', + type: 'data', + }, + { + filterable: true, + sortable: true, + header: 'Read Only', + fieldName: 'isReadOnly', + type: 'data', + }, + { + sortable: true, + header: 'Data Size', + fieldName: 'sizeOnDisk', + type: 'fileSize', + }, + { + sortable: true, + header: 'Log Size', + fieldName: 'logSizeOnDisk', + type: 'fileSize', + }, + ], + }, + }; + }, + getRedirectAuthUrl(connection, options) { if (connection.authType != 'msentra') return null; return authProxy.authProxyGetRedirectUrl({ diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/index.js b/plugins/dbgate-plugin-mssql/src/backend/sql/index.js index 1676b4758..a281b48e0 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/index.js @@ -13,6 +13,9 @@ const viewColumns = require('./viewColumns'); const indexes = require('./indexes'); const indexcols = require('./indexcols'); const triggers = require('./triggers'); +const listVariables = require('./listVariables'); +const listDatabases = require('./listDatabases'); +const listProcesses = require('./listProcesses'); const baseColumns = require('./baseColumns'); module.exports = { @@ -31,5 +34,8 @@ module.exports = { indexcols, tableSizes, triggers, + listVariables, + listDatabases, + listProcesses, baseColumns, }; diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/listDatabases.js b/plugins/dbgate-plugin-mssql/src/backend/sql/listDatabases.js new file mode 100644 index 000000000..751c54b39 --- /dev/null +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/listDatabases.js @@ -0,0 +1,17 @@ +module.exports = ` + SELECT + d.name, + d.database_id, + d.state_desc as status, + d.recovery_model_desc as recoveryModel, + d.collation_name as collation, + d.compatibility_level as compatibilityLevel, + d.is_read_only as isReadOnly, + CAST(SUM(CASE WHEN mf.type = 0 THEN mf.size * 8192.0 ELSE 0 END) AS BIGINT) AS sizeOnDisk, + CAST(SUM(CASE WHEN mf.type = 1 THEN mf.size * 8192.0 ELSE 0 END) AS BIGINT) AS logSizeOnDisk + FROM sys.databases d + LEFT JOIN sys.master_files mf ON d.database_id = mf.database_id + GROUP BY d.name, d.database_id, d.state_desc, d.recovery_model_desc, d.collation_name, + d.compatibility_level, d.is_read_only + ORDER BY d.name +`; diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/listProcesses.js b/plugins/dbgate-plugin-mssql/src/backend/sql/listProcesses.js new file mode 100644 index 000000000..30ffe5768 --- /dev/null +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/listProcesses.js @@ -0,0 +1,11 @@ +module.exports = ` +SELECT + session_id as processId, + ISNULL(host_name, 'Unknown') + ':' + ISNULL(CAST(host_process_id AS VARCHAR(10)), '?') as client, + ISNULL(DB_NAME(database_id), 'master') as namespace, + ISNULL(DATEDIFF(SECOND, last_request_start_time, GETDATE()), 0) as runningTime, + status as state +FROM sys.dm_exec_sessions +WHERE is_user_process = 1 +ORDER BY session_id +`; diff --git a/plugins/dbgate-plugin-mssql/src/backend/sql/listVariables.js b/plugins/dbgate-plugin-mssql/src/backend/sql/listVariables.js new file mode 100644 index 000000000..a6543c759 --- /dev/null +++ b/plugins/dbgate-plugin-mssql/src/backend/sql/listVariables.js @@ -0,0 +1,3 @@ +module.exports = ` + SELECT name as variable, value FROM sys.configurations ORDER BY name +`; diff --git a/plugins/dbgate-plugin-mssql/src/frontend/driver.js b/plugins/dbgate-plugin-mssql/src/frontend/driver.js index 137a63fbc..13520b105 100644 --- a/plugins/dbgate-plugin-mssql/src/frontend/driver.js +++ b/plugins/dbgate-plugin-mssql/src/frontend/driver.js @@ -131,6 +131,7 @@ const dialect = { /** @type {import('dbgate-types').EngineDriver} */ const driver = { ...driverBase, + supportsServerSummary: true, dumperClass: MsSqlDumper, dialect, readOnlySessions: false, diff --git a/plugins/dbgate-plugin-mysql/src/backend/drivers.js b/plugins/dbgate-plugin-mysql/src/backend/drivers.js index bd6bb5a02..f684e0b84 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/drivers.js +++ b/plugins/dbgate-plugin-mysql/src/backend/drivers.js @@ -200,6 +200,69 @@ const drivers = driverBases.map(driverBase => ({ const { rows } = await this.query(dbhan, 'show databases'); return rows.map(x => ({ name: x.Database })); }, + + async listVariables(dbhan) { + const { rows } = await this.query(dbhan, 'SHOW VARIABLES'); + return rows.map(row => ({ + variable: row.Variable_name, + value: row.Value, + })); + }, + + async listProcesses(dbhan) { + const { rows } = await this.query(dbhan, 'SHOW FULL PROCESSLIST'); + return rows.map(row => ({ + processId: row.Id, + connectionId: null, + client: row.Host, + operation: row.Info, + namespace: row.Database, + runningTime: row.Time, + state: row.State, + waitingFor: row.State && row.State.includes('Waiting'), + })); + }, + + async killProcess(dbhan, processId) { + await this.query(dbhan, `KILL ${processId}`); + }, + + async serverSummary(dbhan) { + const [variables, processes, databases] = await Promise.all([ + this.listVariables(dbhan), + this.listProcesses(dbhan), + this.listDatabases(dbhan), + ]); + + return { + variables, + processes: processes.map(p => ({ + processId: p.processId, + connectionId: p.connectionId, + client: p.client, + operation: p.operation, + namespace: p.namespace, + runningTime: p.runningTime, + state: p.state, + waitingFor: p.waitingFor, + })), + databases: { + rows: databases.map(db => ({ + name: db.name, + })), + columns: [ + { + filterable: true, + sortable: true, + header: 'Database', + fieldName: 'name', + type: 'data', + }, + ], + }, + }; + }, + async writeTable(dbhan, name, options) { // @ts-ignore return createBulkInsertStreamBase(this, stream, dbhan, name, options); diff --git a/plugins/dbgate-plugin-mysql/src/frontend/drivers.js b/plugins/dbgate-plugin-mysql/src/frontend/drivers.js index 486942207..8e605068c 100644 --- a/plugins/dbgate-plugin-mysql/src/frontend/drivers.js +++ b/plugins/dbgate-plugin-mysql/src/frontend/drivers.js @@ -385,6 +385,7 @@ const mysqlDriverBase = { /** @type {import('dbgate-types').EngineDriver} */ const mysqlDriver = { ...mysqlDriverBase, + supportsServerSummary: true, dialect: mysqlDialect, engine: 'mysql@dbgate-plugin-mysql', title: 'MySQL', @@ -425,6 +426,7 @@ const mariaDbDialect = { /** @type {import('dbgate-types').EngineDriver} */ const mariaDriver = { ...mysqlDriverBase, + supportsServerSummary: true, dialect: mariaDbDialect, engine: 'mariadb@dbgate-plugin-mysql', title: 'MariaDB', diff --git a/plugins/dbgate-plugin-postgres/src/backend/drivers.js b/plugins/dbgate-plugin-postgres/src/backend/drivers.js index 595b56b97..de81e3d19 100644 --- a/plugins/dbgate-plugin-postgres/src/backend/drivers.js +++ b/plugins/dbgate-plugin-postgres/src/backend/drivers.js @@ -6,6 +6,7 @@ const Analyser = require('./Analyser'); const wkx = require('wkx'); const pg = require('pg'); const pgCopyStreams = require('pg-copy-streams'); +const sql = require('./sql'); const { getLogger, createBulkInsertStreamBase, @@ -351,11 +352,65 @@ const drivers = driverBases.map(driverBase => ({ // @ts-ignore return createBulkInsertStreamBase(this, stream, dbhan, name, options); }, + + async serverSummary(dbhan) { + const [processes, variables, databases] = await Promise.all([ + this.listProcesses(dbhan), + this.listVariables(dbhan), + this.listDatabases(dbhan), + ]); + + /** @type {import('dbgate-types').ServerSummary} */ + const data = { + processes, + variables, + databases: { + rows: databases, + columns: [ + { header: 'Name', fieldName: 'name', type: 'data' }, + { header: 'Size on disk', fieldName: 'sizeOnDisk', type: 'fileSize' }, + ], + }, + }; + + return data; + }, + + async killProcess(dbhan, pid) { + const result = await this.query(dbhan, `SELECT pg_terminate_backend(${parseInt(pid)})`); + return result; + }, + async listDatabases(dbhan) { - const { rows } = await this.query(dbhan, 'SELECT datname AS name FROM pg_database WHERE datistemplate = false'); + const { rows } = await this.query(dbhan, sql.listDatabases); return rows; }, + async listVariables(dbhan) { + const result = await this.query(dbhan, sql.listVariables); + return result.rows.map(row => ({ + variable: row.variable, + value: row.value, + })); + }, + + async listProcesses(dbhan) { + const result = await this.query(dbhan, sql.listProcesses); + return result.rows.map(row => ({ + processId: row.processId, + connectionId: row.connectionId, + client: row.client, + operation: row.operation, + namespace: null, + command: row.operation, + runningTime: row.runningTime ? Math.max(Number(row.runningTime), 0) : null, + state: row.state, + waitingFor: row.waitingFor, + locks: null, + progress: null, + })); + }, + getAuthTypes() { const res = [ { diff --git a/plugins/dbgate-plugin-postgres/src/backend/sql/index.js b/plugins/dbgate-plugin-postgres/src/backend/sql/index.js index 8f648c64f..833153196 100644 --- a/plugins/dbgate-plugin-postgres/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-postgres/src/backend/sql/index.js @@ -17,6 +17,9 @@ const geographyColumns = require('./geographyColumns'); const proceduresParameters = require('./proceduresParameters'); const foreignKeys = require('./foreignKeys'); const triggers = require('./triggers'); +const listDatabases = require('./listDatabases'); +const listVariables = require('./listVariables'); +const listProcesses = require('./listProcesses'); const fk_keyColumnUsage = require('./fk_key_column_usage'); @@ -41,4 +44,7 @@ module.exports = { geographyColumns, proceduresParameters, triggers, + listDatabases, + listVariables, + listProcesses, }; diff --git a/plugins/dbgate-plugin-postgres/src/backend/sql/listDatabases.js b/plugins/dbgate-plugin-postgres/src/backend/sql/listDatabases.js new file mode 100644 index 000000000..a033b9297 --- /dev/null +++ b/plugins/dbgate-plugin-postgres/src/backend/sql/listDatabases.js @@ -0,0 +1,11 @@ +module.exports = ` +SELECT + "datname" AS "name", + pg_database_size("datname") AS "sizeOnDisk", + 0 AS "tableCount", + 0 AS "viewCount", + 0 AS "matviewCount" +FROM "pg_database" +WHERE "datistemplate" = false +ORDER BY pg_database_size("datname") DESC +`; diff --git a/plugins/dbgate-plugin-postgres/src/backend/sql/listProcesses.js b/plugins/dbgate-plugin-postgres/src/backend/sql/listProcesses.js new file mode 100644 index 000000000..807dcad38 --- /dev/null +++ b/plugins/dbgate-plugin-postgres/src/backend/sql/listProcesses.js @@ -0,0 +1,13 @@ +module.exports = ` +SELECT + "pid" AS "processId", + "application_name" AS "client", + "client_addr" AS "connectionId", + "state" AS "state", + "query" AS "operation", + EXTRACT(EPOCH FROM (NOW() - "state_change")) AS "runningTime", + "wait_event" IS NOT NULL AS "waitingFor" +FROM "pg_stat_activity" +WHERE "state" IS NOT NULL +ORDER BY "pid" +`; diff --git a/plugins/dbgate-plugin-postgres/src/backend/sql/listVariables.js b/plugins/dbgate-plugin-postgres/src/backend/sql/listVariables.js new file mode 100644 index 000000000..0973f035b --- /dev/null +++ b/plugins/dbgate-plugin-postgres/src/backend/sql/listVariables.js @@ -0,0 +1,5 @@ +module.exports = ` +SELECT "name" AS "variable", "setting" AS "value" +FROM "pg_settings" +ORDER BY "name" +`; diff --git a/plugins/dbgate-plugin-postgres/src/frontend/drivers.js b/plugins/dbgate-plugin-postgres/src/frontend/drivers.js index e44bf8b6f..c67f1577e 100644 --- a/plugins/dbgate-plugin-postgres/src/frontend/drivers.js +++ b/plugins/dbgate-plugin-postgres/src/frontend/drivers.js @@ -361,6 +361,7 @@ EXECUTE FUNCTION function_name();`, /** @type {import('dbgate-types').EngineDriver} */ const postgresDriver = { ...postgresDriverBase, + supportsServerSummary: true, engine: 'postgres@dbgate-plugin-postgres', title: 'PostgreSQL', defaultPort: 5432, @@ -388,6 +389,7 @@ const postgresDriver = { /** @type {import('dbgate-types').EngineDriver} */ const cockroachDriver = { ...postgresDriverBase, + supportsServerSummary: true, engine: 'cockroach@dbgate-plugin-postgres', title: 'CockroachDB', defaultPort: 26257, @@ -403,6 +405,7 @@ const cockroachDriver = { /** @type {import('dbgate-types').EngineDriver} */ const redshiftDriver = { ...postgresDriverBase, + supportsServerSummary: true, dialect: { ...dialect, stringAgg: false,