From 47a15a66749b268d3b9c02969f152ae161374fe2 Mon Sep 17 00:00:00 2001 From: Syngnat <92659908+Syngnat@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:28:10 +0800 Subject: [PATCH 1/8] fix(oceanbase): stop inflating Oracle query timeout by default --- internal/app/db_context.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/internal/app/db_context.go b/internal/app/db_context.go index 98323571..e47f3fb6 100644 --- a/internal/app/db_context.go +++ b/internal/app/db_context.go @@ -9,8 +9,6 @@ import ( "GoNavi-Wails/internal/db" ) -const defaultOceanBaseOracleQueryTimeoutSeconds = 120 - func normalizeRunConfig(config connection.ConnectionConfig, dbName string) connection.ConnectionConfig { runConfig := config name := strings.TrimSpace(dbName) @@ -56,10 +54,6 @@ func applyOceanBaseOracleCurrentSchemaInit(config connection.ConnectionConfig, s if normalizedSchema == "" { return config } - if config.Timeout <= 0 { - // OceanBase Oracle 查询经常需要经过 OBProxy/Oracle driver 的读等待;默认 30s 容易把慢查询误报成 driver: bad connection。 - config.Timeout = defaultOceanBaseOracleQueryTimeoutSeconds - } values, err := url.ParseQuery(strings.TrimSpace(config.ConnectionParams)) if err != nil { return config From bd0752df960add53c4171460b7e1e70418b622ed Mon Sep 17 00:00:00 2001 From: Syngnat <92659908+Syngnat@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:28:57 +0800 Subject: [PATCH 2/8] test(oceanbase): keep Oracle default timeout unset --- internal/app/db_context_oceanbase_oracle_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/app/db_context_oceanbase_oracle_test.go b/internal/app/db_context_oceanbase_oracle_test.go index 3f3cebc8..198af5ad 100644 --- a/internal/app/db_context_oceanbase_oracle_test.go +++ b/internal/app/db_context_oceanbase_oracle_test.go @@ -20,8 +20,8 @@ func TestNormalizeRunConfig_OceanBaseOracleAddsCurrentSchemaInit(t *testing.T) { if runConfig.Database != "OBORCL" { t.Fatalf("expected OceanBase Oracle service name to stay OBORCL, got %q", runConfig.Database) } - if runConfig.Timeout != defaultOceanBaseOracleQueryTimeoutSeconds { - t.Fatalf("expected OceanBase Oracle default query timeout %d, got %d", defaultOceanBaseOracleQueryTimeoutSeconds, runConfig.Timeout) + if runConfig.Timeout != 0 { + t.Fatalf("expected default OceanBase Oracle timeout to stay unset, got %d", runConfig.Timeout) } values, err := url.ParseQuery(runConfig.ConnectionParams) if err != nil { From 4fee58999d87380958f7a9eaf2b519a21a740943 Mon Sep 17 00:00:00 2001 From: Syngnat <92659908+Syngnat@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:29:17 +0800 Subject: [PATCH 3/8] perf(db): keep one idle Oracle-style connection --- internal/db/sql_pool.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/db/sql_pool.go b/internal/db/sql_pool.go index 146ec2a5..13620c3f 100644 --- a/internal/db/sql_pool.go +++ b/internal/db/sql_pool.go @@ -19,6 +19,12 @@ func configureSQLConnectionPool(db *sql.DB, dbType string) { switch strings.ToLower(strings.TrimSpace(dbType)) { case "sqlite", "duckdb": return + case "oracle", "oceanbase": + db.SetMaxOpenConns(defaultSQLMaxOpenConns) + db.SetMaxIdleConns(1) + db.SetConnMaxIdleTime(defaultSQLConnMaxIdleTime) + db.SetConnMaxLifetime(defaultSQLConnMaxLifetime) + return } db.SetMaxOpenConns(defaultSQLMaxOpenConns) db.SetMaxIdleConns(0) From 8c968c1a87712f0310a4afa4a48d2c9dee3a16d1 Mon Sep 17 00:00:00 2001 From: Syngnat <92659908+Syngnat@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:31:53 +0800 Subject: [PATCH 4/8] feat(data-grid): expose total count action in pagination --- .../src/components/DataGridPaginationBar.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/DataGridPaginationBar.tsx b/frontend/src/components/DataGridPaginationBar.tsx index 0edcd786..5db3fc2c 100644 --- a/frontend/src/components/DataGridPaginationBar.tsx +++ b/frontend/src/components/DataGridPaginationBar.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Button, InputNumber, Pagination, Select } from 'antd'; -import { LeftOutlined, RightOutlined } from '@ant-design/icons'; +import { CloseOutlined, LeftOutlined, RightOutlined, VerticalAlignBottomOutlined } from '@ant-design/icons'; import { t as defaultTranslate, type I18nParams } from '../i18n'; interface DataGridPaginationState { @@ -26,9 +26,12 @@ export interface DataGridPaginationBarProps { paginationPageText: string; paginationPageSizeOptions: string[]; showKnownPageCount: boolean; + manualTotalCountAvailable?: boolean; + totalCountLoading?: boolean; onPageChange?: (page: number, size: number) => void; onPageSizeChange: (value: string) => void; onV2PageStep: (direction: 'previous' | 'next') => void; + onToggleTotalCount?: () => void; translate?: DataGridPaginationTranslate; } @@ -42,9 +45,12 @@ const DataGridPaginationBar: React.FC = ({ paginationPageText, paginationPageSizeOptions, showKnownPageCount, + manualTotalCountAvailable = false, + totalCountLoading = false, onPageChange, onPageSizeChange, onV2PageStep, + onToggleTotalCount, translate = defaultTranslate, }) => { const [jumpPage, setJumpPage] = React.useState(pagination?.current ?? null); @@ -58,6 +64,17 @@ const DataGridPaginationBar: React.FC = ({ return null; } + const totalCountButton = manualTotalCountAvailable && onToggleTotalCount ? ( + + ) : null; + const maxJumpPage = showKnownPageCount ? Math.max(1, paginationTotalPages) : null; const normalizedJumpPage = Number.isFinite(Number(jumpPage)) && Number(jumpPage) > 0 ? (maxJumpPage !== null @@ -132,6 +149,7 @@ const DataGridPaginationBar: React.FC = ({
{paginationV2SummaryText}
+ {totalCountButton} ) : null; From 93a3240862dae4966cc86a939dfa71c976dd8351 Mon Sep 17 00:00:00 2001 From: Syngnat <92659908+Syngnat@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:47:45 +0800 Subject: [PATCH 8/8] fix(query-editor): fallback ctrl-click for OB Oracle schema tables --- frontend/src/components/MonacoEditor.tsx | 135 +++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/frontend/src/components/MonacoEditor.tsx b/frontend/src/components/MonacoEditor.tsx index ce17b244..7701d8a3 100644 --- a/frontend/src/components/MonacoEditor.tsx +++ b/frontend/src/components/MonacoEditor.tsx @@ -24,6 +24,140 @@ const sameEditorPosition = (left: any, right: any): boolean => ( && Number(left?.column) === Number(right?.column) ); +const stripSqlIdentifierQuotes = (value: string): string => { + const text = String(value || '').trim(); + if (!text) return ''; + if ((text.startsWith('`') && text.endsWith('`')) + || (text.startsWith('"') && text.endsWith('"')) + || (text.startsWith('[') && text.endsWith(']'))) { + return text.slice(1, -1).trim(); + } + return text; +}; + +const splitSqlIdentifierPath = (raw: string): string[] => ( + String(raw || '') + .split('.') + .map(stripSqlIdentifierQuotes) + .map((part) => part.trim()) + .filter(Boolean) +); + +const resolveIdentifierWindowAtColumn = ( + lineContent: string, + column: number, +): { start: number; end: number; text: string } | null => { + const text = String(lineContent || ''); + if (!text) return null; + const isIdentChar = (ch: string) => /[A-Za-z0-9_$`"\[\].]/.test(ch || ''); + let offset = Math.max(0, Math.min(text.length - 1, Number(column || 1) - 2)); + if (!isIdentChar(text[offset] || '')) { + if (offset > 0 && isIdentChar(text[offset - 1] || '')) { + offset -= 1; + } else if (offset + 1 < text.length && isIdentChar(text[offset + 1] || '')) { + offset += 1; + } else { + return null; + } + } + let start = offset; + while (start > 0 && isIdentChar(text[start - 1] || '')) start -= 1; + let end = offset + 1; + while (end < text.length && isIdentChar(text[end] || '')) end += 1; + return start < end ? { start, end, text: text.slice(start, end).trim() } : null; +}; + +const isLikelyTableReferenceIdentifier = ( + lineContent: string, + identifierStart: number, +): boolean => { + const beforeIdentifier = String(lineContent || '').slice(0, Math.max(0, identifierStart)); + return /\b(?:from|join|update|into|delete\s+from|alter\s+table|drop\s+table|truncate\s+table)\s*$/i.test(beforeIdentifier); +}; + +const isOceanBaseOracleConnection = (connection: any): boolean => { + const config = connection?.config || {}; + return String(config.type || '').trim().toLowerCase() === 'oceanbase' + && String(config.oceanBaseProtocol || '').trim().toLowerCase() === 'oracle'; +}; + +const installOceanBaseOracleNavigationFallback = (editor: any) => { + const editorDomNode = editor?.getDomNode?.(); + if (!editorDomNode || editor.__gonaviObOracleNavigationFallbackInstalled) { + return; + } + Object.defineProperty(editor, '__gonaviObOracleNavigationFallbackInstalled', { + value: true, + configurable: true, + }); + + const handleMouseDownCapture = (event: MouseEvent) => { + if (event.button !== 0 || !(event.ctrlKey || event.metaKey) || event.altKey) { + return; + } + + const store = useStore.getState(); + const activeTab = (store.tabs || []).find((tab: any) => tab.id === store.activeTabId); + if (!activeTab || activeTab.type !== 'query') { + return; + } + const connectionId = String(activeTab.connectionId || store.activeContext?.connectionId || '').trim(); + if (!connectionId) { + return; + } + const connection = (store.connections || []).find((item: any) => item.id === connectionId); + if (!isOceanBaseOracleConnection(connection)) { + return; + } + + const target = editor.getTargetAtClientPoint?.(event.clientX, event.clientY); + const position = target?.position; + if (!position) { + return; + } + const model = editor.getModel?.(); + const lineContent = String(model?.getLineContent?.(position.lineNumber) || ''); + const identifier = resolveIdentifierWindowAtColumn(lineContent, position.column); + if (!identifier || !identifier.text.includes('.')) { + return; + } + if (!isLikelyTableReferenceIdentifier(lineContent, identifier.start)) { + return; + } + + const parts = splitSqlIdentifierPath(identifier.text); + if (parts.length !== 2) { + return; + } + const [schemaName, tableName] = parts; + if (!schemaName || !tableName) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation?.(); + store.setActiveContext?.({ connectionId, dbName: schemaName }); + store.addTab?.({ + id: `${connectionId}-${schemaName}-table-${tableName}`, + title: tableName, + type: 'table', + connectionId, + dbName: schemaName, + tableName, + initialViewMode: 'fields', + initialViewModeRequestId: String(Date.now()), + objectType: 'table', + returnToTabId: activeTab.id || undefined, + }); + }; + + editorDomNode.addEventListener('mousedown', handleMouseDownCapture, true); + editor.onDidDispose?.(() => { + editorDomNode.removeEventListener('mousedown', handleMouseDownCapture, true); + }); +}; + const patchQueryEditorAiInlineRightArrowFallback = (editor: any, monaco: any) => { const originalAddCommand = editor?.addCommand?.bind?.(editor); if (!originalAddCommand || !monaco?.KeyCode?.RightArrow) { @@ -154,6 +288,7 @@ const MonacoEditor: React.FC = ({ }, [beforeMount]); const handleMount: OnMount = useCallback((editor, monaco) => { + installOceanBaseOracleNavigationFallback(editor); patchQueryEditorAiInlineRightArrowFallback(editor, monaco); onMount?.(editor, monaco); }, [onMount]);