|
6 | 6 |
|
7 | 7 | namespace GitHub.Copilot.SDK; |
8 | 8 |
|
| 9 | +/// <summary> |
| 10 | +/// Result of a SQLite query execution via <see cref="ISessionFsSqliteProvider"/>. |
| 11 | +/// Same shape as <see cref="SessionFsSqliteQueryResult"/> but without the <c>Error</c> field, |
| 12 | +/// since providers signal errors by throwing. |
| 13 | +/// </summary> |
| 14 | +public class SessionFsSqliteResult |
| 15 | +{ |
| 16 | + /// <summary>Column names from the result set.</summary> |
| 17 | + public IList<string> Columns { get; set; } = []; |
| 18 | + |
| 19 | + /// <summary>For SELECT: rows as column-keyed dictionaries. For others: empty.</summary> |
| 20 | + public IList<IDictionary<string, object>> Rows { get; set; } = []; |
| 21 | + |
| 22 | + /// <summary>Number of rows affected (for INSERT/UPDATE/DELETE).</summary> |
| 23 | + public long RowsAffected { get; set; } |
| 24 | + |
| 25 | + /// <summary>Last inserted row ID (for INSERT).</summary> |
| 26 | + public long? LastInsertRowid { get; set; } |
| 27 | +} |
| 28 | + |
| 29 | +/// <summary> |
| 30 | +/// Optional interface for <see cref="SessionFsProvider"/> subclasses that support |
| 31 | +/// per-session SQLite databases. Implement this interface on your provider to enable |
| 32 | +/// the runtime's SQL tool to route queries through your SessionFs implementation. |
| 33 | +/// </summary> |
| 34 | +public interface ISessionFsSqliteProvider |
| 35 | +{ |
| 36 | + /// <summary> |
| 37 | + /// Executes a SQLite query against the per-session database. |
| 38 | + /// </summary> |
| 39 | + /// <param name="queryType">How to execute: <c>"exec"</c> for DDL/multi-statement, <c>"query"</c> for SELECT, <c>"run"</c> for INSERT/UPDATE/DELETE.</param> |
| 40 | + /// <param name="query">SQL query to execute.</param> |
| 41 | + /// <param name="bindParams">Optional named bind parameters.</param> |
| 42 | + /// <param name="cancellationToken">Cancellation token.</param> |
| 43 | + /// <returns>The query result, or <c>null</c> for exec-type queries.</returns> |
| 44 | + Task<SessionFsSqliteResult?> QueryAsync( |
| 45 | + SessionFsSqliteQueryType queryType, |
| 46 | + string query, |
| 47 | + IDictionary<string, object>? bindParams, |
| 48 | + CancellationToken cancellationToken); |
| 49 | + |
| 50 | + /// <summary> |
| 51 | + /// Checks whether the per-session SQLite database already exists, without creating it. |
| 52 | + /// </summary> |
| 53 | + /// <param name="cancellationToken">Cancellation token.</param> |
| 54 | + Task<bool> ExistsAsync(CancellationToken cancellationToken); |
| 55 | +} |
| 56 | + |
9 | 57 | /// <summary> |
10 | 58 | /// Base class for session filesystem providers. Subclasses override the |
11 | 59 | /// virtual methods and use normal C# patterns (return values, throw exceptions). |
12 | 60 | /// The base class catches exceptions and converts them to <see cref="SessionFsError"/> |
13 | 61 | /// results expected by the runtime. |
| 62 | +/// To add SQLite support, also implement <see cref="ISessionFsSqliteProvider"/>. |
14 | 63 | /// </summary> |
15 | 64 | public abstract class SessionFsProvider : ISessionFsHandler |
16 | 65 | { |
@@ -75,24 +124,6 @@ public abstract class SessionFsProvider : ISessionFsHandler |
75 | 124 | /// <param name="cancellationToken">Cancellation token.</param> |
76 | 125 | protected abstract Task RenameAsync(string src, string dest, CancellationToken cancellationToken); |
77 | 126 |
|
78 | | - /// <summary>Executes a SQLite query against the per-session database.</summary> |
79 | | - /// <param name="sessionId">Target session identifier.</param> |
80 | | - /// <param name="query">SQL query to execute.</param> |
81 | | - /// <param name="queryType">How to execute the query.</param> |
82 | | - /// <param name="parameters">Optional named bind parameters.</param> |
83 | | - /// <param name="cancellationToken">Cancellation token.</param> |
84 | | - protected abstract Task<SessionFsSqliteQueryResult> SqliteQueryAsync( |
85 | | - string sessionId, |
86 | | - string query, |
87 | | - SessionFsSqliteQueryType queryType, |
88 | | - IDictionary<string, object>? parameters, |
89 | | - CancellationToken cancellationToken); |
90 | | - |
91 | | - /// <summary>Checks whether the per-session SQLite database already exists.</summary> |
92 | | - /// <param name="sessionId">Target session identifier.</param> |
93 | | - /// <param name="cancellationToken">Cancellation token.</param> |
94 | | - protected abstract Task<bool> SqliteExistsAsync(string sessionId, CancellationToken cancellationToken); |
95 | | - |
96 | 127 | // ---- ISessionFsHandler implementation (private, handles error mapping) ---- |
97 | 128 |
|
98 | 129 | async Task<SessionFsReadFileResult> ISessionFsHandler.ReadFileAsync(SessionFsReadFileRequest request, CancellationToken cancellationToken) |
@@ -246,42 +277,50 @@ async Task<SessionFsReaddirWithTypesResult> ISessionFsHandler.ReaddirWithTypesAs |
246 | 277 |
|
247 | 278 | async Task<SessionFsSqliteQueryResult> ISessionFsHandler.SqliteQueryAsync(SessionFsSqliteQueryRequest request, CancellationToken cancellationToken) |
248 | 279 | { |
249 | | - ArgumentNullException.ThrowIfNull(request); |
| 280 | + if (this is not ISessionFsSqliteProvider sqliteProvider) |
| 281 | + { |
| 282 | + return new SessionFsSqliteQueryResult |
| 283 | + { |
| 284 | + Error = new SessionFsError { Code = SessionFsErrorCode.UNKNOWN, Message = "SQLite is not supported by this provider." }, |
| 285 | + }; |
| 286 | + } |
250 | 287 |
|
251 | 288 | try |
252 | 289 | { |
253 | | - return await SqliteQueryAsync(request.SessionId, request.Query, request.QueryType, request.Params, cancellationToken).ConfigureAwait(false); |
| 290 | + var result = await sqliteProvider.QueryAsync(request.QueryType, request.Query, request.Params, cancellationToken).ConfigureAwait(false); |
| 291 | + |
| 292 | + return new SessionFsSqliteQueryResult |
| 293 | + { |
| 294 | + Rows = result?.Rows ?? [], |
| 295 | + Columns = result?.Columns ?? [], |
| 296 | + RowsAffected = result?.RowsAffected ?? 0, |
| 297 | + LastInsertRowid = result?.LastInsertRowid, |
| 298 | + }; |
254 | 299 | } |
255 | | - catch (Exception ex) when (!IsCriticalException(ex)) |
| 300 | + catch (Exception ex) |
256 | 301 | { |
257 | 302 | return new SessionFsSqliteQueryResult { Error = ToSessionFsError(ex) }; |
258 | 303 | } |
259 | 304 | } |
260 | 305 |
|
261 | 306 | async Task<SessionFsSqliteExistsResult> ISessionFsHandler.SqliteExistsAsync(SessionFsSqliteExistsRequest request, CancellationToken cancellationToken) |
262 | 307 | { |
263 | | - ArgumentNullException.ThrowIfNull(request); |
| 308 | + if (this is not ISessionFsSqliteProvider sqliteProvider) |
| 309 | + { |
| 310 | + return new SessionFsSqliteExistsResult { Exists = false }; |
| 311 | + } |
264 | 312 |
|
265 | 313 | try |
266 | 314 | { |
267 | | - var exists = await SqliteExistsAsync(request.SessionId, cancellationToken).ConfigureAwait(false); |
| 315 | + var exists = await sqliteProvider.ExistsAsync(cancellationToken).ConfigureAwait(false); |
268 | 316 | return new SessionFsSqliteExistsResult { Exists = exists }; |
269 | 317 | } |
270 | | - catch (Exception ex) when (!IsCriticalException(ex)) |
| 318 | + catch |
271 | 319 | { |
272 | 320 | return new SessionFsSqliteExistsResult { Exists = false }; |
273 | 321 | } |
274 | 322 | } |
275 | 323 |
|
276 | | - private static bool IsCriticalException(Exception ex) => |
277 | | - ex is OperationCanceledException |
278 | | - or OutOfMemoryException |
279 | | - or StackOverflowException |
280 | | - or AccessViolationException |
281 | | - or AppDomainUnloadedException |
282 | | - or BadImageFormatException |
283 | | - or CannotUnloadAppDomainException |
284 | | - or InvalidProgramException; |
285 | 324 |
|
286 | 325 | private static SessionFsError ToSessionFsError(Exception ex) |
287 | 326 | { |
|
0 commit comments