33 *--------------------------------------------------------------------------------------------*/
44
55using GitHub . Copilot . Rpc ;
6+ using System . Diagnostics . CodeAnalysis ;
67using System . Text . Json ;
78
89namespace GitHub . Copilot ;
@@ -27,6 +28,23 @@ public sealed class SessionFsSqliteResult
2728 public long ? LastInsertRowid { get ; set ; }
2829}
2930
31+ /// <summary>
32+ /// One statement in an atomic SQLite transaction passed to
33+ /// <see cref="ISessionFsSqliteTransactionProvider.TransactionAsync"/>.
34+ /// </summary>
35+ [ Experimental ( Diagnostics . Experimental ) ]
36+ public sealed class SessionFsSqliteStatement
37+ {
38+ /// <summary>How to execute: <c>"exec"</c>, <c>"query"</c>, or <c>"run"</c>.</summary>
39+ public SessionFsSqliteQueryType QueryType { get ; set ; }
40+
41+ /// <summary>SQL statement to execute.</summary>
42+ public string Query { get ; set ; } = string . Empty ;
43+
44+ /// <summary>Optional named bind parameters.</summary>
45+ public IDictionary < string , object ? > ? Params { get ; set ; }
46+ }
47+
3048/// <summary>
3149/// Optional interface for <see cref="SessionFsProvider"/> subclasses that support
3250/// per-session SQLite databases. Implement this interface on your provider to enable
@@ -55,6 +73,52 @@ public interface ISessionFsSqliteProvider
5573 Task < bool > ExistsAsync ( CancellationToken cancellationToken ) ;
5674}
5775
76+ /// <summary>
77+ /// Optional capability for session filesystem providers that support atomic SQLite transactions.
78+ /// </summary>
79+ public interface ISessionFsSqliteTransactionProvider
80+ {
81+ /// <summary>
82+ /// Executes <paramref name="statements"/> atomically against the per-session database.
83+ /// </summary>
84+ /// <param name="statements">Statements to execute in order, inside a single transaction.</param>
85+ /// <param name="cancellationToken">Cancellation token.</param>
86+ /// <returns>One result per statement, in the same order as <paramref name="statements"/>.</returns>
87+ /// <exception cref="SessionFsSqliteTransactionException">
88+ /// Thrown to tell the runtime how the failure should be classified. Any other exception
89+ /// is reported as <see cref="SessionFsSqliteTransactionErrorClass.Fatal"/>.
90+ /// </exception>
91+ Task < IList < SessionFsSqliteResult > > TransactionAsync (
92+ IList < SessionFsSqliteStatement > statements ,
93+ CancellationToken cancellationToken ) ;
94+ }
95+
96+ /// <summary>
97+ /// Thrown by an <see cref="ISessionFsSqliteTransactionProvider"/> to classify a failed SQLite transaction.
98+ /// <see cref="SessionFsSqliteTransactionErrorClass.BusyOrLocked"/> guarantees the transaction
99+ /// rolled back and is safe to retry; <see cref="SessionFsSqliteTransactionErrorClass.PostCommitAmbiguous"/>
100+ /// must never be retried.
101+ /// </summary>
102+ [ Experimental ( Diagnostics . Experimental ) ]
103+ public sealed class SessionFsSqliteTransactionException : Exception
104+ {
105+ /// <summary>Initializes a new instance of the <see cref="SessionFsSqliteTransactionException"/> class.</summary>
106+ /// <param name="message">Human-readable failure description.</param>
107+ /// <param name="errorClass">How the runtime should classify the failure.</param>
108+ /// <param name="innerException">Optional underlying exception.</param>
109+ public SessionFsSqliteTransactionException (
110+ string message ,
111+ SessionFsSqliteTransactionErrorClass errorClass ,
112+ Exception ? innerException = null )
113+ : base ( message , innerException )
114+ {
115+ ErrorClass = errorClass ;
116+ }
117+
118+ /// <summary>Gets the failure classification reported to the runtime.</summary>
119+ public SessionFsSqliteTransactionErrorClass ErrorClass { get ; }
120+ }
121+
58122/// <summary>
59123/// Base class for session filesystem providers. Subclasses override the
60124/// virtual methods and use normal C# patterns (return values, throw exceptions).
@@ -297,7 +361,7 @@ async Task<SessionFsSqliteQueryResult> ISessionFsHandler.SqliteQueryAsync(Sessio
297361 {
298362 Rows = result ? . Rows ? . Select ( row => ( IDictionary < string , JsonElement > ) row . ToDictionary (
299363 kvp => kvp . Key ,
300- kvp => CopilotClient . ToJsonElementForWire ( kvp . Value ) ! . Value ) ) . ToList ( ) ?? [ ] ,
364+ kvp => ToJsonElement ( kvp . Value ) ) ) . ToList ( ) ?? [ ] ,
301365 Columns = result ? . Columns ?? [ ] ,
302366 RowsAffected = result ? . RowsAffected ?? 0 ,
303367 LastInsertRowid = result ? . LastInsertRowid ,
@@ -309,6 +373,78 @@ async Task<SessionFsSqliteQueryResult> ISessionFsHandler.SqliteQueryAsync(Sessio
309373 }
310374 }
311375
376+ async Task < SessionFsSqliteTransactionResult > ISessionFsHandler . SqliteTransactionAsync ( SessionFsSqliteTransactionRequest request , CancellationToken cancellationToken )
377+ {
378+ if ( this is not ISessionFsSqliteTransactionProvider transactionProvider )
379+ {
380+ return new SessionFsSqliteTransactionResult
381+ {
382+ Error = new SessionFsSqliteTransactionError
383+ {
384+ ErrorClass = SessionFsSqliteTransactionErrorClass . Fatal ,
385+ Message = "SQLite is not supported by this provider." ,
386+ } ,
387+ } ;
388+ }
389+
390+ IList < SessionFsSqliteResult > results ;
391+ try
392+ {
393+ var statements = request . Statements . Select ( statement => new SessionFsSqliteStatement
394+ {
395+ QueryType = statement . QueryType ,
396+ Query = statement . Query ,
397+ Params = statement . Params ? . ToDictionary ( kvp => kvp . Key , kvp => JsonElementToValue ( kvp . Value ) ) ,
398+ } ) . ToList ( ) ;
399+ results = await transactionProvider . TransactionAsync ( statements , cancellationToken ) . ConfigureAwait ( false ) ;
400+ }
401+ catch ( SessionFsSqliteTransactionException ex )
402+ {
403+ return new SessionFsSqliteTransactionResult
404+ {
405+ Error = new SessionFsSqliteTransactionError { ErrorClass = ex . ErrorClass , Message = ex . Message } ,
406+ } ;
407+ }
408+ catch ( Exception ex )
409+ {
410+ return new SessionFsSqliteTransactionResult
411+ {
412+ Error = new SessionFsSqliteTransactionError
413+ {
414+ ErrorClass = SessionFsSqliteTransactionErrorClass . Fatal ,
415+ Message = ex . Message ,
416+ } ,
417+ } ;
418+ }
419+
420+ try
421+ {
422+ return new SessionFsSqliteTransactionResult
423+ {
424+ Results = results . Select ( result => new SessionFsSqliteQueryResult
425+ {
426+ Rows = result . Rows ? . Select ( row => ( IDictionary < string , JsonElement > ) row . ToDictionary (
427+ kvp => kvp . Key ,
428+ kvp => ToJsonElement ( kvp . Value ) ) ) . ToList ( ) ?? [ ] ,
429+ Columns = result . Columns ?? [ ] ,
430+ RowsAffected = result . RowsAffected ,
431+ LastInsertRowid = result . LastInsertRowid ,
432+ } ) . ToList ( ) ,
433+ } ;
434+ }
435+ catch ( Exception ex )
436+ {
437+ return new SessionFsSqliteTransactionResult
438+ {
439+ Error = new SessionFsSqliteTransactionError
440+ {
441+ ErrorClass = SessionFsSqliteTransactionErrorClass . PostCommitAmbiguous ,
442+ Message = ex . Message ,
443+ } ,
444+ } ;
445+ }
446+ }
447+
312448 async Task < SessionFsSqliteExistsResult > ISessionFsHandler . SqliteExistsAsync ( SessionFsSqliteExistsRequest request , CancellationToken cancellationToken )
313449 {
314450 if ( this is not ISessionFsSqliteProvider sqliteProvider )
@@ -336,6 +472,9 @@ private static SessionFsError ToSessionFsError(Exception ex)
336472 return new SessionFsError { Code = code , Message = ex . Message } ;
337473 }
338474
475+ private static JsonElement ToJsonElement ( object ? value ) =>
476+ CopilotClient . ToJsonElementForWire ( value ) ?? JsonElement . Parse ( "null" ) ;
477+
339478 private static object ? JsonElementToValue ( JsonElement element ) => element . ValueKind switch
340479 {
341480 JsonValueKind . Null => null ,
0 commit comments