Skip to content

Commit 971ef11

Browse files
Add SessionFs sqlite support for runtime sqlite routing (#1299)
1 parent 1993292 commit 971ef11

43 files changed

Lines changed: 2755 additions & 448 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dotnet/Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<ItemGroup>
88
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
99
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.2" />
10+
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.6" />
1011
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="10.2.0" />
1112
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
1213
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />

dotnet/src/Client.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,7 @@ await Rpc.SessionFs.SetProviderAsync(
13291329
_options.SessionFs.InitialCwd,
13301330
_options.SessionFs.SessionStatePath,
13311331
_options.SessionFs.Conventions,
1332+
_options.SessionFs.Capabilities,
13321333
cancellationToken: cancellationToken);
13331334
}
13341335

@@ -1345,8 +1346,16 @@ private void ConfigureSessionFsHandlers(CopilotSession session, Func<CopilotSess
13451346
"CreateSessionFsHandler is required in the session config when CopilotClientOptions.SessionFs is configured.");
13461347
}
13471348

1348-
session.ClientSessionApis.SessionFs = createSessionFsHandler(session)
1349+
var provider = createSessionFsHandler(session)
13491350
?? throw new InvalidOperationException("CreateSessionFsHandler returned null.");
1351+
1352+
if (_options.SessionFs.Capabilities?.Sqlite == true && provider is not ISessionFsSqliteProvider)
1353+
{
1354+
throw new InvalidOperationException(
1355+
"SessionFsConfig declares capabilities.sqlite but the provider does not implement ISessionFsSqliteProvider.");
1356+
}
1357+
1358+
session.ClientSessionApis.SessionFs = provider;
13501359
}
13511360

13521361
private async Task VerifyProtocolVersionAsync(Connection connection, CancellationToken cancellationToken)

dotnet/src/SessionFsProvider.cs

Lines changed: 72 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,60 @@
66

77
namespace GitHub.Copilot.SDK;
88

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+
957
/// <summary>
1058
/// Base class for session filesystem providers. Subclasses override the
1159
/// virtual methods and use normal C# patterns (return values, throw exceptions).
1260
/// The base class catches exceptions and converts them to <see cref="SessionFsError"/>
1361
/// results expected by the runtime.
62+
/// To add SQLite support, also implement <see cref="ISessionFsSqliteProvider"/>.
1463
/// </summary>
1564
public abstract class SessionFsProvider : ISessionFsHandler
1665
{
@@ -75,24 +124,6 @@ public abstract class SessionFsProvider : ISessionFsHandler
75124
/// <param name="cancellationToken">Cancellation token.</param>
76125
protected abstract Task RenameAsync(string src, string dest, CancellationToken cancellationToken);
77126

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-
96127
// ---- ISessionFsHandler implementation (private, handles error mapping) ----
97128

98129
async Task<SessionFsReadFileResult> ISessionFsHandler.ReadFileAsync(SessionFsReadFileRequest request, CancellationToken cancellationToken)
@@ -246,42 +277,50 @@ async Task<SessionFsReaddirWithTypesResult> ISessionFsHandler.ReaddirWithTypesAs
246277

247278
async Task<SessionFsSqliteQueryResult> ISessionFsHandler.SqliteQueryAsync(SessionFsSqliteQueryRequest request, CancellationToken cancellationToken)
248279
{
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+
}
250287

251288
try
252289
{
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+
};
254299
}
255-
catch (Exception ex) when (!IsCriticalException(ex))
300+
catch (Exception ex)
256301
{
257302
return new SessionFsSqliteQueryResult { Error = ToSessionFsError(ex) };
258303
}
259304
}
260305

261306
async Task<SessionFsSqliteExistsResult> ISessionFsHandler.SqliteExistsAsync(SessionFsSqliteExistsRequest request, CancellationToken cancellationToken)
262307
{
263-
ArgumentNullException.ThrowIfNull(request);
308+
if (this is not ISessionFsSqliteProvider sqliteProvider)
309+
{
310+
return new SessionFsSqliteExistsResult { Exists = false };
311+
}
264312

265313
try
266314
{
267-
var exists = await SqliteExistsAsync(request.SessionId, cancellationToken).ConfigureAwait(false);
315+
var exists = await sqliteProvider.ExistsAsync(cancellationToken).ConfigureAwait(false);
268316
return new SessionFsSqliteExistsResult { Exists = exists };
269317
}
270-
catch (Exception ex) when (!IsCriticalException(ex))
318+
catch
271319
{
272320
return new SessionFsSqliteExistsResult { Exists = false };
273321
}
274322
}
275323

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;
285324

286325
private static SessionFsError ToSessionFsError(Exception ex)
287326
{

dotnet/src/Types.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,13 @@ public sealed class SessionFsConfig
323323
/// Path conventions used by this filesystem provider.
324324
/// </summary>
325325
public required SessionFsSetProviderConventions Conventions { get; init; }
326+
327+
/// <summary>
328+
/// Optional capabilities that this filesystem provider supports.
329+
/// When <see cref="SessionFsSetProviderCapabilities.Sqlite"/> is <c>true</c>,
330+
/// the runtime routes SQLite queries through the provider instead of using a local database file.
331+
/// </summary>
332+
public SessionFsSetProviderCapabilities? Capabilities { get; init; }
326333
}
327334

328335
/// <summary>

0 commit comments

Comments
 (0)