diff --git a/src/Client/extension.ts b/src/Client/extension.ts index 274e603..34d2ff1 100644 --- a/src/Client/extension.ts +++ b/src/Client/extension.ts @@ -213,7 +213,10 @@ export async function activate(context: ExtensionContext) ); const queryEditor = new QueryEditor(context, server, clipboard, historyManager, connectionManager, resultsViewer, historyPanel); context.subscriptions.push( + vscode.commands.registerCommand('msKustoExplorer.noop', () => {}), vscode.commands.registerCommand('msKustoExplorer.runQuery', (startLine?: number, startChar?: number, endLine?: number, endChar?: number) => queryEditor.runQuery(startLine, startChar, endLine, endChar)), + vscode.commands.registerCommand('msKustoExplorer.runQuery.running', () => {}), + vscode.commands.registerCommand('msKustoExplorer.copyClientRequestId', (clientRequestId?: string) => queryEditor.copyClientRequestId(clientRequestId)), vscode.commands.registerCommand('msKustoExplorer.copyQuery', (startLine?: number, startChar?: number, endLine?: number, endChar?: number) => queryEditor.copyQuery(startLine, startChar, endLine, endChar)), vscode.commands.registerCommand('msKustoExplorer.copyQueryTransparent', (startLine?: number, startChar?: number, endLine?: number, endChar?: number) => queryEditor.copyQuery(startLine, startChar, endLine, endChar, true)), vscode.commands.registerCommand('msKustoExplorer.formatQuery', (startLine?: number, startChar?: number, endLine?: number, endChar?: number) => queryEditor.formatQuery(startLine, startChar, endLine, endChar)), diff --git a/src/Client/features/historyManager.ts b/src/Client/features/historyManager.ts index 3efac91..9c8e6c7 100644 --- a/src/Client/features/historyManager.ts +++ b/src/Client/features/historyManager.ts @@ -43,6 +43,12 @@ export interface HistoryEntry { database?: string; /** Number of result rows. */ rowCount?: number; + /** ISO 8601 timestamp of when query execution started. */ + executionStartedAt?: string; + /** Query execution duration measured by the client, in milliseconds. */ + executionDurationMs?: number; + /** Client request id used for the Kusto query. */ + clientRequestId?: string; /** FNV-1a hash of the server-minified query text, for CodeLens and result lookup. */ queryHash?: number; } @@ -251,6 +257,9 @@ export class HistoryManager { ...(resultData.cluster !== undefined && { cluster: resultData.cluster }), ...(resultData.database !== undefined && { database: resultData.database }), rowCount, + ...(resultData.executionStartedAt !== undefined && { executionStartedAt: resultData.executionStartedAt }), + ...(resultData.executionDurationMs !== undefined && { executionDurationMs: resultData.executionDurationMs }), + ...(resultData.clientRequestId !== undefined && { clientRequestId: resultData.clientRequestId }), ...(queryHash !== undefined && { queryHash }), }; diff --git a/src/Client/features/queryEditor.ts b/src/Client/features/queryEditor.ts index 96365a7..565a9e4 100644 --- a/src/Client/features/queryEditor.ts +++ b/src/Client/features/queryEditor.ts @@ -6,16 +6,20 @@ */ import * as vscode from 'vscode'; +import * as crypto from 'crypto'; import type { IServer, SelectionRange, Range } from './server'; import type { ConnectionManager } from './connectionManager'; import { ResultsViewer } from './resultsViewer'; import { HistoryManager } from './historyManager'; +import type { HistoryEntry } from './historyManager'; import type { HistoryPanel } from './historyPanel'; import type { IClipboard } from './clipboard'; import type { ClipboardItem } from './clipboard'; import { ENTITY_DEFINITION_SCHEME } from './entityDefinitionProvider'; const PASTE_KIND = vscode.DocumentDropOrPasteEditKind.Text.append('kusto'); +const QUERY_RUNNING_CONTEXT_KEY = 'msKustoExplorer.queryRunning'; +const MIN_QUERY_RUNNING_INDICATOR_MS = 500; /** * Builds a SelectionRange from optional CodeLens arguments. @@ -28,6 +32,66 @@ function rangeFromArgs(startLine?: number, startChar?: number, endLine?: number, return undefined; } +function createClientRequestId(): string { + return `KustoExplorerVsCode;${crypto.randomUUID()}`; +} + +function formatRunTimestamp(timestamp: string): string { + const date = new Date(timestamp); + if (Number.isNaN(date.valueOf())) { + return timestamp; + } + + return date.toLocaleString(undefined, { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + second: '2-digit' + }); +} + +function formatDuration(durationMs: number): string { + if (durationMs < 1000) { + return `${Math.max(0, Math.round(durationMs))}ms`; + } + + const seconds = durationMs / 1000; + if (seconds < 60) { + return `${seconds >= 10 ? Math.round(seconds).toString() : seconds.toFixed(1)}s`; + } + + const roundedSeconds = Math.round(seconds); + const minutes = Math.floor(roundedSeconds / 60); + const remainingSeconds = roundedSeconds % 60; + return `${minutes}m ${remainingSeconds}s`; +} + +function getLastRunTitle(entry: HistoryEntry): string | undefined { + const timestamp = entry.executionStartedAt ?? entry.timestamp; + const parts = [`Last run: ${formatRunTimestamp(timestamp)}`]; + if (entry.executionDurationMs !== undefined) { + parts.push(`took: ${formatDuration(entry.executionDurationMs)}`); + } + + return parts.join(', '); +} + +function getQueryRangeKey(uri: string, range: Range): string { + return `${uri}:${range.start.line}:${range.start.character}:${range.end.line}:${range.end.character}`; +} + +interface QueryRunIndicator { + key: string; + generation: number; +} + +interface RunningQueryRangeState { + count: number; + startedAt: number; + generation: number; +} + // ============================================================================= // Query Editor // ============================================================================= @@ -48,6 +112,13 @@ export class QueryEditor { private readonly resultsViewer: ResultsViewer; private readonly historyPanel: HistoryPanel; private readonly errorRangeDecoration: vscode.TextEditorDecorationType; + private readonly queryRunningStatusBarItem: vscode.StatusBarItem; + private runningQueryCount = 0; + private isQueryRunning = false; + private queryRunningStartedAt = 0; + private queryRunningGeneration = 0; + private queryRangeRunningGeneration = 0; + private readonly runningQueryRanges = new Map(); constructor( context: vscode.ExtensionContext, @@ -72,6 +143,15 @@ export class QueryEditor { } }); + this.queryRunningStatusBarItem = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 1 + ); + this.queryRunningStatusBarItem.text = '$(sync~spin) Running Kusto query'; + this.queryRunningStatusBarItem.tooltip = 'A Kusto query is running'; + context.subscriptions.push(this.queryRunningStatusBarItem); + void vscode.commands.executeCommand('setContext', QUERY_RUNNING_CONTEXT_KEY, false); + // Register CodeLens provider for queries this.codeLensProvider = new KustoCodeLensProvider(this.server, this.history); context.subscriptions.push( @@ -115,6 +195,7 @@ export class QueryEditor { return; } + let queryRunIndicator: QueryRunIndicator | undefined; try { const uri = editor.document.uri.toString(); const selection = queryRange ?? { @@ -131,6 +212,8 @@ export class QueryEditor { return; } + queryRunIndicator = await this.beginQueryRun(uri, resolvedRange); + // Extract the query text from the document const queryText = editor.document.getText(new vscode.Range( resolvedRange.start.line, resolvedRange.start.character, @@ -141,7 +224,17 @@ export class QueryEditor { const connection = await this.connections.getDocumentConnection(uri); // Run the query via server.runQuery (text-based, returns ResultData) - const runResult = await this.server.runQuery(queryText, connection?.cluster, connection?.database, true); + const executionStartedAt = new Date().toISOString(); + const startedAtMs = Date.now(); + const clientRequestId = createClientRequestId(); + const runResult = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Window, + title: 'Running Kusto query...' + }, + () => this.server.runQuery(queryText, connection?.cluster, connection?.database, true, undefined, clientRequestId) + ); + const executionDurationMs = Date.now() - startedAtMs; // If the result includes a connection string for an unknown cluster, add it as a server if (runResult?.connection || runResult?.cluster) { @@ -169,6 +262,10 @@ export class QueryEditor { } else if (runResult?.data) { + runResult.data.executionStartedAt = executionStartedAt; + runResult.data.executionDurationMs = executionDurationMs; + runResult.data.clientRequestId = clientRequestId; + // Add to history and use the history file as backing for the singleton view const historyUri = await this.history.addHistoryEntry(runResult.data); this.resultsViewer.setSingletonViewBackingUri(historyUri); @@ -183,7 +280,120 @@ export class QueryEditor { catch (error) { vscode.window.showErrorMessage(`Failed to execute query: ${error}`); + } + finally + { + if (queryRunIndicator) { + await this.endQueryRun(queryRunIndicator); + } + } + } + + private async beginQueryRun(uri: string, range: Range): Promise { + const startedAt = Date.now(); + const key = getQueryRangeKey(uri, range); + let rangeState = this.runningQueryRanges.get(key); + if (!rangeState || rangeState.count === 0) { + rangeState = { + count: 0, + startedAt, + generation: ++this.queryRangeRunningGeneration + }; + this.runningQueryRanges.set(key, rangeState); + this.codeLensProvider.setQueryRangeRunning(key, true); + } + + rangeState.count++; + + if (this.runningQueryCount === 0) { + this.queryRunningStartedAt = startedAt; + this.queryRunningGeneration++; + } + + this.runningQueryCount++; + await this.setQueryRunning(this.runningQueryCount > 0); + + return { + key, + generation: rangeState.generation + }; + } + + private async endQueryRun(indicator: QueryRunIndicator): Promise { + this.endQueryRangeRun(indicator); + + this.runningQueryCount = Math.max(0, this.runningQueryCount - 1); + if (this.runningQueryCount > 0) { + await this.setQueryRunning(true); + return; + } + + const generation = this.queryRunningGeneration; + const elapsedMs = Date.now() - this.queryRunningStartedAt; + const delayMs = Math.max(0, MIN_QUERY_RUNNING_INDICATOR_MS - elapsedMs); + if (delayMs === 0) { + await this.setQueryRunning(false); + return; } + + setTimeout(() => { + if (this.runningQueryCount === 0 && this.queryRunningGeneration === generation) { + void this.setQueryRunning(false); + } + }, delayMs); + } + + private endQueryRangeRun(indicator: QueryRunIndicator): void { + const rangeState = this.runningQueryRanges.get(indicator.key); + if (!rangeState || rangeState.generation !== indicator.generation) { + return; + } + + rangeState.count = Math.max(0, rangeState.count - 1); + if (rangeState.count > 0) { + return; + } + + const elapsedMs = Date.now() - rangeState.startedAt; + const delayMs = Math.max(0, MIN_QUERY_RUNNING_INDICATOR_MS - elapsedMs); + const clearRunningRange = () => { + const currentState = this.runningQueryRanges.get(indicator.key); + if (currentState?.count === 0 && currentState.generation === indicator.generation) { + this.runningQueryRanges.delete(indicator.key); + this.codeLensProvider.setQueryRangeRunning(indicator.key, false); + } + }; + + if (delayMs === 0) { + clearRunningRange(); + return; + } + + setTimeout(clearRunningRange, delayMs); + } + + private async setQueryRunning(isRunning: boolean): Promise { + if (this.isQueryRunning === isRunning) { + return; + } + + this.isQueryRunning = isRunning; + if (isRunning) { + this.queryRunningStatusBarItem.show(); + } else { + this.queryRunningStatusBarItem.hide(); + } + + await vscode.commands.executeCommand('setContext', QUERY_RUNNING_CONTEXT_KEY, isRunning); + } + + async copyClientRequestId(clientRequestId?: string): Promise { + if (!clientRequestId) { + vscode.window.showWarningMessage('No client request id available to copy.'); + return; + } + + await this.clipboard.copyText(clientRequestId); } /** @@ -407,6 +617,7 @@ export class QueryEditor { class KustoCodeLensProvider implements vscode.CodeLensProvider { private _onDidChangeCodeLenses = new vscode.EventEmitter(); readonly onDidChangeCodeLenses = this._onDidChangeCodeLenses.event; + private readonly runningQueryRangeKeys = new Set(); constructor(private readonly server: IServer, private readonly history: HistoryManager) { } @@ -415,6 +626,20 @@ class KustoCodeLensProvider implements vscode.CodeLensProvider { this._onDidChangeCodeLenses.fire(); } + setQueryRangeRunning(key: string, isRunning: boolean): void { + if (this.runningQueryRangeKeys.has(key) === isRunning) { + return; + } + + if (isRunning) { + this.runningQueryRangeKeys.add(key); + } else { + this.runningQueryRangeKeys.delete(key); + } + + this.refresh(); + } + async provideCodeLenses(document: vscode.TextDocument): Promise { const isEntityDefinition = document.uri.scheme === ENTITY_DEFINITION_SCHEME; @@ -447,10 +672,11 @@ class KustoCodeLensProvider implements vscode.CodeLensProvider { // Hide Run, Format, and Results lenses in entity definition documents if (!isEntityDefinition) { + const isQueryRangeRunning = this.runningQueryRangeKeys.has(getQueryRangeKey(document.uri.toString(), range)); lenses.push(new vscode.CodeLens(vsRange, { - title: '▶ Run', - command: 'msKustoExplorer.runQuery', - tooltip: 'Run this query', + title: isQueryRangeRunning ? '$(sync~spin) Running' : '▶ Run', + command: isQueryRangeRunning ? 'msKustoExplorer.runQuery.running' : 'msKustoExplorer.runQuery', + tooltip: isQueryRangeRunning ? 'This Kusto query is running' : 'Run this query', arguments: [range.start.line, range.start.character, range.end.line, range.end.character] })); } @@ -470,14 +696,33 @@ class KustoCodeLensProvider implements vscode.CodeLensProvider { arguments: [range.start.line, range.start.character, range.end.line, range.end.character] })); + const lastRun = await this.history.getMatchingEntry(queryText); + // Only show Results lens if there is a history entry for this query - if (await this.history.hasEntryForQuery(queryText)) { + if (lastRun) { lenses.push(new vscode.CodeLens(vsRange, { title: '📊 Results', command: 'msKustoExplorer.showResults', tooltip: 'Show results from history for this query', arguments: [range.start.line, range.start.character] })); + + lenses.push(new vscode.CodeLens(vsRange, { + title: getLastRunTitle(lastRun) ?? 'Last run', + command: 'msKustoExplorer.noop', + tooltip: lastRun.clientRequestId + ? `Client request id: ${lastRun.clientRequestId}` + : 'Last query execution details' + })); + + if (lastRun.clientRequestId) { + lenses.push(new vscode.CodeLens(vsRange, { + title: '$(copy) Copy CID', + command: 'msKustoExplorer.copyClientRequestId', + tooltip: 'Copy the last run client request id', + arguments: [lastRun.clientRequestId] + })); + } } } } diff --git a/src/Client/features/server.ts b/src/Client/features/server.ts index 85b8462..6c46791 100644 --- a/src/Client/features/server.ts +++ b/src/Client/features/server.ts @@ -11,7 +11,7 @@ import { CancellationToken, Disposable, ExtensionContext } from 'vscode'; */ export interface IServer { // LSP Requests - runQuery(query: string, cluster?: string, database?: string, isReadOnly?: boolean, maxRows?: number): Promise; + runQuery(query: string, cluster?: string, database?: string, isReadOnly?: boolean, maxRows?: number, clientRequestId?: string): Promise; getQueryResultType(query: string, cluster: string, database?: string): Promise; getFunctionResultType(cluster: string, database: string, functionName: string): Promise; getQueryRanges(uri: string): Promise; @@ -75,11 +75,12 @@ export class Server implements IServer { cluster?: string, database?: string, isReadOnly?: boolean, - maxRows?: number + maxRows?: number, + clientRequestId?: string ): Promise { return this.client.sendRequest( 'kusto/runQuery', - { query, cluster, database, isReadOnly, maxRows } + { query, cluster, database, isReadOnly, maxRows, clientRequestId } ); } @@ -540,6 +541,9 @@ export interface ResultData { query?: string; cluster?: string; database?: string; + executionStartedAt?: string; + executionDurationMs?: number; + clientRequestId?: string; tables: ResultTable[]; charts?: ResultChart[]; } diff --git a/src/Client/package.json b/src/Client/package.json index de51dc0..30336d5 100644 --- a/src/Client/package.json +++ b/src/Client/package.json @@ -112,6 +112,11 @@ "title": "Run Query", "icon": "$(play)" }, + { + "command": "msKustoExplorer.runQuery.running", + "title": "Running Query", + "icon": "$(sync~spin)" + }, { "command": "msKustoExplorer.addServer", "title": "Add Connection", @@ -307,6 +312,12 @@ } ], "menus": { + "commandPalette": [ + { + "command": "msKustoExplorer.runQuery.running", + "when": "false" + } + ], "view/title": [ { "command": "msKustoExplorer.addServer", @@ -504,7 +515,12 @@ "editor/title": [ { "command": "msKustoExplorer.runQuery", - "when": "editorLangId == kusto && !msKustoExplorer.isEntityDefinition", + "when": "editorLangId == kusto && !msKustoExplorer.isEntityDefinition && !msKustoExplorer.queryRunning", + "group": "navigation@1" + }, + { + "command": "msKustoExplorer.runQuery.running", + "when": "editorLangId == kusto && !msKustoExplorer.isEntityDefinition && msKustoExplorer.queryRunning", "group": "navigation@1" }, { diff --git a/src/Client/tests/unit/historyManager.test.ts b/src/Client/tests/unit/historyManager.test.ts index 8b15167..4f39878 100644 --- a/src/Client/tests/unit/historyManager.test.ts +++ b/src/Client/tests/unit/historyManager.test.ts @@ -48,6 +48,22 @@ describe('HistoryManager', () => { return new HistoryManager(createMockContext(tmpDir), server ?? new NullServer()); } + function seedHistoryEntries(count: number): HistoryEntry[] { + const entries: HistoryEntry[] = Array.from({ length: count }, (_, i) => ({ + fileName: `old-query-${i}.kqr`, + timestamp: new Date(Date.UTC(2026, 0, 1, 0, 0, i)).toISOString(), + queryPreview: `old query ${i}`, + rowCount: 1, + })); + + for (const entry of entries) { + fs.writeFileSync(path.join(historyDir, entry.fileName), '', 'utf-8'); + } + + fs.writeFileSync(path.join(historyDir, 'history-index.json'), JSON.stringify(entries, null, 2), 'utf-8'); + return entries; + } + describe('constructor', () => { it('creates the history directory', () => { createManager(); @@ -138,19 +154,20 @@ describe('HistoryManager', () => { it('enforces max history entries and deletes old files', async () => { const mgr = createManager(); + const seededEntries = seedHistoryEntries(200); + const oldestEntry = seededEntries[seededEntries.length - 1]!; - // Add 202 entries to exceed the 200 limit - for (let i = 0; i < 202; i++) { - await mgr.addHistoryEntry(makeResultData(`query${i}`, 1)); - } + await mgr.addHistoryEntry(makeResultData('new query', 1)); const entries = mgr.getEntries(); expect(entries).toHaveLength(200); - // Most recent should be last added - expect(entries[0]!.queryPreview).toBe('query201'); + // Most recent should be the newly added entry + expect(entries[0]!.queryPreview).toBe('new query'); + expect(entries.some(e => e.fileName === oldestEntry.fileName)).toBe(false); - // Oldest entries' files should be deleted + // Oldest entry's file should be deleted + expect(fs.existsSync(path.join(historyDir, oldestEntry.fileName))).toBe(false); const kqrFiles = fs.readdirSync(historyDir).filter(f => f.endsWith('.kqr')); expect(kqrFiles).toHaveLength(200); }); diff --git a/src/Server/Connections/ConnectionManager.cs b/src/Server/Connections/ConnectionManager.cs index 65f6275..39277d5 100644 --- a/src/Server/Connections/ConnectionManager.cs +++ b/src/Server/Connections/ConnectionManager.cs @@ -173,12 +173,13 @@ public async Task ExecuteAsync( EditString query, ImmutableDictionary? options = null, ImmutableDictionary? parameters = null, + string? clientRequestId = null, CancellationToken cancellationToken = default ) { try { - var properties = CreateClientRequestProperties(options ?? ImmutableDictionary.Empty, parameters ?? ImmutableDictionary.Empty); + var properties = CreateClientRequestProperties(options ?? ImmutableDictionary.Empty, parameters ?? ImmutableDictionary.Empty, clientRequestId); var resultReader = (Kusto.Language.KustoCode.GetKind(query) == CodeKinds.Command) ? await this.AdminProvider.ExecuteControlCommandAsync(this.Database, query, properties).ConfigureAwait(false) : await this.QueryProvider.ExecuteQueryAsync(this.Database, query, properties, cancellationToken).ConfigureAwait(false); @@ -220,7 +221,7 @@ public async Task> ExecuteAsync( { try { - var results = await ExecuteAsync(query, options, parameters, cancellationToken).ConfigureAwait(false); + var results = await ExecuteAsync(query, options, parameters, cancellationToken: cancellationToken).ConfigureAwait(false); if (results.Tables != null && results.Tables.Count > 0) { @@ -244,9 +245,14 @@ public async Task> ExecuteAsync( private ClientRequestProperties CreateClientRequestProperties( ImmutableDictionary options, - ImmutableDictionary parameters) + ImmutableDictionary parameters, + string? clientRequestId) { var crp = new ClientRequestProperties(); + if (!string.IsNullOrWhiteSpace(clientRequestId)) + { + crp.ClientRequestId = clientRequestId; + } foreach (var kvp in options) { diff --git a/src/Server/Connections/IConnection.cs b/src/Server/Connections/IConnection.cs index 1a0022f..026455e 100644 --- a/src/Server/Connections/IConnection.cs +++ b/src/Server/Connections/IConnection.cs @@ -49,6 +49,7 @@ public Task ExecuteAsync( EditString query, ImmutableDictionary? options = null, ImmutableDictionary? parameters = null, + string? clientRequestId = null, CancellationToken cancellationToken = default ); @@ -104,4 +105,4 @@ public record ExecuteResult /// Any diagnostics produced during query execution, such as errors or warnings. /// public ImmutableList? Diagnostics { get; init; } -} \ No newline at end of file +} diff --git a/src/Server/Querying/IQueryManager.cs b/src/Server/Querying/IQueryManager.cs index 3b5bb1d..76f32b6 100644 --- a/src/Server/Querying/IQueryManager.cs +++ b/src/Server/Querying/IQueryManager.cs @@ -37,6 +37,7 @@ Task RunQueryAsync( string? databaseName, ImmutableDictionary queryOptions, ImmutableDictionary queryParameters, + string? clientRequestId, CancellationToken cancellationToken ); } diff --git a/src/Server/Querying/QueryManager.cs b/src/Server/Querying/QueryManager.cs index 86aefaf..3190374 100644 --- a/src/Server/Querying/QueryManager.cs +++ b/src/Server/Querying/QueryManager.cs @@ -125,13 +125,15 @@ public Task RunQueryAsync( string? databaseName, ImmutableDictionary queryOptions, ImmutableDictionary queryParameters, + string? clientRequestId, CancellationToken cancellationToken) { var context = new ExecutionContext { Query = query, Options = queryOptions, - Parameters = queryParameters + Parameters = queryParameters, + ClientRequestId = clientRequestId }; // handle any directives @@ -197,6 +199,13 @@ private Diagnostic CreateDiagnostic(EditString query, string message) return new Diagnostic("KLS100", message).WithLocation(start, end - start); } + private static string? AddClientRequestIdSuffix(string? clientRequestId, string suffix) + { + return string.IsNullOrWhiteSpace(clientRequestId) + ? clientRequestId + : $"{clientRequestId};{suffix}"; + } + private record ExecutionContext { /// @@ -224,6 +233,11 @@ private record ExecutionContext /// public required ImmutableDictionary Parameters { get; init; } + /// + /// The client request id to use when executing the query. + /// + public string? ClientRequestId { get; init; } + /// /// the name of the stored query result to store the result of this execution into. /// @@ -244,6 +258,8 @@ private async Task ExecuteQueryAsync(IConnection connection, Executio }; } + var executeClientRequestId = context.ClientRequestId; + // handle stored query results if (context.StoredQueryResultName != null) { @@ -255,9 +271,15 @@ private async Task ExecuteQueryAsync(IConnection connection, Executio // change query to retrieve the stored result query = query.ReplaceAt(0, query.Length, $"stored_query_result({KustoFacts.GetStringLiteral(context.StoredQueryResultName)})"); + executeClientRequestId = AddClientRequestIdSuffix(context.ClientRequestId, "fetchStoredQueryResult"); } - var executeResult = await connection.ExecuteAsync(query, context.Options, context.Parameters, cancellationToken).ConfigureAwait(false); + var executeResult = await connection.ExecuteAsync( + query, + context.Options, + context.Parameters, + clientRequestId: executeClientRequestId, + cancellationToken: cancellationToken).ConfigureAwait(false); return new RunResult { Query = query, diff --git a/src/Server/Server.cs b/src/Server/Server.cs index 4f1bc79..d13d1d2 100644 --- a/src/Server/Server.cs +++ b/src/Server/Server.cs @@ -2048,7 +2048,8 @@ public class RunQueryDiagnostic @params.Database, queryOptions, queryParameters, - cancellationToken) + clientRequestId: @params.ClientRequestId, + cancellationToken: cancellationToken) .ConfigureAwait(false); if (runResult.Error != null) @@ -2108,6 +2109,9 @@ public class RunQueryParams [DataMember(Name = "maxRows")] public long? MaxRows { get; init; } + + [DataMember(Name = "clientRequestId")] + public string? ClientRequestId { get; init; } } [DataContract] diff --git a/src/ServerTests/Features/ServerSchemaSourceTests.cs b/src/ServerTests/Features/ServerSchemaSourceTests.cs index 2d08450..049a920 100644 --- a/src/ServerTests/Features/ServerSchemaSourceTests.cs +++ b/src/ServerTests/Features/ServerSchemaSourceTests.cs @@ -243,6 +243,7 @@ public Task ExecuteAsync( EditString query, ImmutableDictionary? options = null, ImmutableDictionary? parameters = null, + string? clientRequestId = null, CancellationToken cancellationToken = default) { return Task.FromResult(new ExecuteResult());