diff --git a/.github/workflows/apply-ob-oracle-performance-navigation-count.yml b/.github/workflows/apply-ob-oracle-performance-navigation-count.yml new file mode 100644 index 0000000..2f11f84 --- /dev/null +++ b/.github/workflows/apply-ob-oracle-performance-navigation-count.yml @@ -0,0 +1,459 @@ +name: Apply OB Oracle performance/navigation/count fixes + +on: + push: + branches: + - fix/ob-oracle-performance-navigation-count + paths: + - .github/workflows/apply-ob-oracle-performance-navigation-count.yml + +permissions: + contents: write + +jobs: + apply-fix: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: fix/ob-oracle-performance-navigation-count + persist-credentials: true + - name: Apply focused patches + run: | + python3 - <<'PY' + from pathlib import Path + + def replace_once(text: str, old: str, new: str, label: str) -> str: + if old not in text: + if new in text: + return text + raise SystemExit(f'{label} not found') + return text.replace(old, new, 1) + + # 1) Avoid retrying the same query after its execution context is already done. + methods_db = Path('internal/app/methods_db.go') + text = methods_db.read_text() + helper_anchor = """func newQueryExecutionContext(config connection.ConnectionConfig) (context.Context, context.CancelFunc) { + if strings.EqualFold(strings.TrimSpace(config.Type), "duckdb") { + return context.WithCancel(context.Background()) + } + timeoutSeconds := config.Timeout + if timeoutSeconds <= 0 { + timeoutSeconds = 30 + } + return utils.ContextWithTimeout(time.Duration(timeoutSeconds) * time.Second) +} +""" + helper_block = helper_anchor + """ +func shouldRetryConnectionAfterQueryError(ctx context.Context, err error) bool { + if err == nil { + return false + } + if ctx != nil && ctx.Err() != nil { + return false + } + return shouldRefreshCachedConnection(err) +} +""" + if 'func shouldRetryConnectionAfterQueryError(ctx context.Context, err error) bool' not in text: + text = replace_once(text, helper_anchor, helper_block, 'query retry helper anchor') + text = text.replace('if err != nil && shouldRefreshCachedConnection(err) {', 'if err != nil && shouldRetryConnectionAfterQueryError(ctx, err) {') + text = text.replace('if batchErr != nil && shouldRefreshCachedConnection(batchErr) {', 'if batchErr != nil && shouldRetryConnectionAfterQueryError(ctx, batchErr) {') + methods_db.write_text(text) + + # 2) Lazy-load schema table metadata for Ctrl/Cmd navigation in the SQL editor. + query_editor = Path('frontend/src/components/QueryEditor.tsx') + text = query_editor.read_text() + if ' findIdentifierWindowAtOffset,\n' not in text: + text = replace_once( + text, + ' collectQueryEditorReferencedDatabaseNames,\n', + ' collectQueryEditorReferencedDatabaseNames,\n findIdentifierWindowAtOffset,\n', + 'QueryEditor import findIdentifierWindowAtOffset', + ) + if ' normalizeNavigationIdentifierParts,\n' not in text: + text = replace_once( + text, + ' normalizeMetadataDialect,\n', + ' normalizeMetadataDialect,\n normalizeNavigationIdentifierParts,\n', + 'QueryEditor import normalizeNavigationIdentifierParts', + ) + if 'const navigationMetadataWarmupRef = useRef | undefined>>({});' not in text: + text = replace_once( + text, + ' const aiContextMetadataWarmupRef = useRef | undefined>>({});\n', + ' const aiContextMetadataWarmupRef = useRef | undefined>>({});\n const navigationMetadataWarmupRef = useRef | undefined>>({});\n', + 'QueryEditor navigation metadata ref', + ) + + navigation_helpers = """ const getNavigationIdentifierPartsAtPosition = ( + lineContent: string, + column: number, + ): string[] => { + const offset = Math.max(0, Number(column || 1) - 2); + const windowRange = findIdentifierWindowAtOffset(lineContent, offset); + if (!windowRange) { + return []; + } + return normalizeNavigationIdentifierParts(lineContent.slice(windowRange.start, windowRange.end).trim()); + }; + + const isVisibleNavigationSchema = (schemaName: string): boolean => { + const normalized = String(schemaName || '').trim().toLowerCase(); + return !!normalized && visibleDbsRef.current.some((db) => String(db || '').trim().toLowerCase() === normalized); + }; + + const ensureQueryEditorNavigationSchemaTables = async (schemaName: string): Promise => { + const requestedSchema = String(schemaName || '').trim(); + if (!requestedSchema) { + return false; + } + const connectionId = String(currentConnectionIdRef.current || currentConnectionId || tab.connectionId || '').trim(); + if (!connectionId || !isVisibleNavigationSchema(requestedSchema)) { + return false; + } + const normalizedSchema = requestedSchema.toLowerCase(); + if (tablesRef.current.some((table) => String(table.dbName || '').trim().toLowerCase() === normalizedSchema)) { + return true; + } + + const warmupKey = `${connectionId}\u0000${normalizedSchema}\u0000navigation-tables`; + const existingWarmup = navigationMetadataWarmupRef.current[warmupKey]; + if (existingWarmup) { + return existingWarmup; + } + + const warmupPromise = (async (): Promise => { + const conn = connectionsRef.current.find((item) => item.id === connectionId); + if (!conn) { + return false; + } + const schemaFromTree = visibleDbsRef.current.find((db) => String(db || '').trim().toLowerCase() === normalizedSchema) || requestedSchema; + const config = buildQueryEditorObjectDefinitionConnectionConfig(conn); + try { + const resTables = await DBGetTables(buildRpcConnectionConfig(config) as any, schemaFromTree); + if (!resTables?.success || !Array.isArray(resTables.data)) { + return false; + } + const fetchedTables = resTables.data + .map((row: any) => buildCompletionTableMeta(schemaFromTree, row, new Map())) + .filter((table): table is CompletionTableMeta => !!table); + if (fetchedTables.length === 0) { + return false; + } + const nextTableByKey = new Map( + tablesRef.current.map((table) => [ + `${String(table.dbName || '').trim().toLowerCase()}\u0000${String(table.tableName || '').trim().toLowerCase()}`, + table, + ]), + ); + fetchedTables.forEach((table) => { + nextTableByKey.set( + `${String(table.dbName || '').trim().toLowerCase()}\u0000${String(table.tableName || '').trim().toLowerCase()}`, + table, + ); + }); + tablesRef.current = [...nextTableByKey.values()]; + if (!visibleDbsRef.current.some((db) => String(db || '').trim().toLowerCase() === normalizedSchema)) { + visibleDbsRef.current = [...visibleDbsRef.current, schemaFromTree]; + } + sharedTablesData = tablesRef.current; + sharedVisibleDbs = visibleDbsRef.current; + sharedLazyTablesCache[`${connectionId}|${schemaFromTree}`] = fetchedTables; + refreshObjectDecorations(QUERY_EDITOR_LIVE_DECORATION_MAX_TEXT_LENGTH); + return true; + } catch (error) { + console.warn('GoNavi query editor navigation metadata warmup failed', error); + return false; + } + })(); + + navigationMetadataWarmupRef.current[warmupKey] = warmupPromise; + const succeeded = await warmupPromise; + if (!succeeded) { + delete navigationMetadataWarmupRef.current[warmupKey]; + } + return succeeded; + }; + +""" + if 'const ensureQueryEditorNavigationSchemaTables = async (schemaName: string): Promise' not in text: + text = replace_once( + text, + ' editor.onMouseDown?.((event: any) => {\n', + navigation_helpers + ' editor.onMouseDown?.((event: any) => {\n', + 'QueryEditor navigation helper insertion', + ) + + old_mouse = """ editor.onMouseDown?.((event: any) => { + const browserEvent = event?.event; + const targetPosition = normalizeEditorPosition(event?.target?.position); + if (!browserEvent || !targetPosition) { + return; + } + if (!isQueryEditorPrimaryMouseButton(browserEvent)) { + return; + } + if (!hasQueryEditorCtrlMetaModifier(browserEvent) && !ctrlMetaPressedRef.current) { + return; + } + + const model = editor.getModel?.(); + const lineContent = String(model?.getLineContent?.(targetPosition.lineNumber) || ''); + const navigationTarget = resolveQueryEditorNavigationTarget( + lineContent, + targetPosition.column, + currentDbRef.current, + visibleDbsRef.current, + tablesRef.current, + viewsRef.current, + materializedViewsRef.current, + triggersRef.current, + routinesRef.current, + sequencesRef.current, + packagesRef.current, + ); + if (!navigationTarget) { + return; + } + + browserEvent.preventDefault?.(); + browserEvent.stopPropagation?.(); + + const connectionId = String(currentConnectionIdRef.current || '').trim(); + if (!connectionId) { + return; + } + + if (navigationTarget.type === 'database') { + const nextDbName = String(navigationTarget.dbName || '').trim(); + if (!nextDbName) { + return; + } + setCurrentDb(nextDbName); + currentDbRef.current = nextDbName; + setActiveContext({ connectionId, dbName: nextDbName }); + return; + } + + const targetDbName = String(navigationTarget.dbName || '').trim(); + if (!targetDbName) { + return; + } + + if (navigationTarget.type === 'table') { + const targetTableName = String(navigationTarget.tableName || '').trim(); + if (!targetTableName) return; + addTab({ + id: `${connectionId}-${targetDbName}-table-${targetTableName}`, + title: targetTableName, + type: 'table', + connectionId, + dbName: targetDbName, + tableName: targetTableName, + initialViewMode: 'fields', + initialViewModeRequestId: String(Date.now()), + objectType: 'table', + returnToTabId: activeTabId || undefined, + }); + return; + } + + if (navigationTarget.type === 'view' || navigationTarget.type === 'materialized-view') { + void openDefinitionObjectEditTab(navigationTarget, connectionId, targetDbName); + return; + } + + if (navigationTarget.type === 'trigger') { + void openTriggerObjectEditTab(navigationTarget, connectionId, targetDbName); + return; + } + + if (navigationTarget.type === 'sequence') { + void openDefinitionObjectEditTab(navigationTarget, connectionId, targetDbName); + return; + } + + if (navigationTarget.type === 'package') { + void openDefinitionObjectEditTab(navigationTarget, connectionId, targetDbName); + return; + } + + void openRoutineObjectEditTab(navigationTarget, connectionId, targetDbName); + }); +""" + new_mouse = """ editor.onMouseDown?.((event: any) => { + const browserEvent = event?.event; + const targetPosition = normalizeEditorPosition(event?.target?.position); + if (!browserEvent || !targetPosition) { + return; + } + if (!isQueryEditorPrimaryMouseButton(browserEvent)) { + return; + } + if (!hasQueryEditorCtrlMetaModifier(browserEvent) && !ctrlMetaPressedRef.current) { + return; + } + + const model = editor.getModel?.(); + const lineContent = String(model?.getLineContent?.(targetPosition.lineNumber) || ''); + const resolveNavigationTargetAtPosition = () => resolveQueryEditorNavigationTarget( + lineContent, + targetPosition.column, + currentDbRef.current, + visibleDbsRef.current, + tablesRef.current, + viewsRef.current, + materializedViewsRef.current, + triggersRef.current, + routinesRef.current, + sequencesRef.current, + packagesRef.current, + ); + const initialNavigationTarget = resolveNavigationTargetAtPosition(); + const identifierParts = getNavigationIdentifierPartsAtPosition(lineContent, targetPosition.column); + const lazySchemaName = !initialNavigationTarget && identifierParts.length >= 2 && isVisibleNavigationSchema(identifierParts[0]) + ? identifierParts[0] + : ''; + if (!initialNavigationTarget && !lazySchemaName) { + return; + } + + browserEvent.preventDefault?.(); + browserEvent.stopPropagation?.(); + + void (async () => { + let navigationTarget = initialNavigationTarget; + if (!navigationTarget && lazySchemaName) { + const loaded = await ensureQueryEditorNavigationSchemaTables(lazySchemaName); + if (loaded) { + navigationTarget = resolveNavigationTargetAtPosition(); + if (navigationTarget) { + applyNavigationHoverStateAtPosition(targetPosition); + } + } + } + if (!navigationTarget) { + return; + } + + const connectionId = String(currentConnectionIdRef.current || '').trim(); + if (!connectionId) { + return; + } + + if (navigationTarget.type === 'database') { + const nextDbName = String(navigationTarget.dbName || '').trim(); + if (!nextDbName) { + return; + } + setCurrentDb(nextDbName); + currentDbRef.current = nextDbName; + setActiveContext({ connectionId, dbName: nextDbName }); + return; + } + + const targetDbName = String(navigationTarget.dbName || '').trim(); + if (!targetDbName) { + return; + } + + if (navigationTarget.type === 'table') { + const targetTableName = String(navigationTarget.tableName || '').trim(); + if (!targetTableName) return; + addTab({ + id: `${connectionId}-${targetDbName}-table-${targetTableName}`, + title: targetTableName, + type: 'table', + connectionId, + dbName: targetDbName, + tableName: targetTableName, + initialViewMode: 'fields', + initialViewModeRequestId: String(Date.now()), + objectType: 'table', + returnToTabId: activeTabId || undefined, + }); + return; + } + + if (navigationTarget.type === 'view' || navigationTarget.type === 'materialized-view') { + void openDefinitionObjectEditTab(navigationTarget, connectionId, targetDbName); + return; + } + + if (navigationTarget.type === 'trigger') { + void openTriggerObjectEditTab(navigationTarget, connectionId, targetDbName); + return; + } + + if (navigationTarget.type === 'sequence') { + void openDefinitionObjectEditTab(navigationTarget, connectionId, targetDbName); + return; + } + + if (navigationTarget.type === 'package') { + void openDefinitionObjectEditTab(navigationTarget, connectionId, targetDbName); + return; + } + + void openRoutineObjectEditTab(navigationTarget, connectionId, targetDbName); + })(); + }); +""" + text = replace_once(text, old_mouse, new_mouse, 'QueryEditor mouse navigation block') + query_editor.write_text(text) + + # 3) Put manual total-count action in the pagination bar where the not-counted state is shown. + shell = Path('frontend/src/components/DataGridShell.tsx') + text = shell.read_text() + old = """ +""" + new = """ +""" + text = replace_once(text, old, new, 'DataGridShell pagination props') + shell.write_text(text) + + workflow = Path('.github/workflows/apply-ob-oracle-performance-navigation-count.yml') + if workflow.exists(): + workflow.unlink() + PY + + git diff --check + - name: Commit patches + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add internal/app/methods_db.go frontend/src/components/QueryEditor.tsx frontend/src/components/DataGridShell.tsx .github/workflows/apply-ob-oracle-performance-navigation-count.yml + git commit -m "fix(ob-oracle): avoid timeout retries and lazy-load navigation metadata" + git push origin HEAD:fix/ob-oracle-performance-navigation-count