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="ISessionFsSqliteProvider.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
@@ -48,13 +66,53 @@ public interface ISessionFsSqliteProvider
4866 IDictionary < string , object ? > ? bindParams ,
4967 CancellationToken cancellationToken ) ;
5068
69+ /// <summary>
70+ /// Executes <paramref name="statements"/> atomically against the per-session database.
71+ /// </summary>
72+ /// <param name="statements">Statements to execute in order, inside a single transaction.</param>
73+ /// <param name="cancellationToken">Cancellation token.</param>
74+ /// <returns>One result per statement, in the same order as <paramref name="statements"/>.</returns>
75+ /// <exception cref="SessionFsSqliteTransactionException">
76+ /// Thrown to tell the runtime how the failure should be classified. Any other exception
77+ /// is reported as <see cref="SessionFsSqliteTransactionErrorClass.Fatal"/>.
78+ /// </exception>
79+ Task < IList < SessionFsSqliteResult > > TransactionAsync (
80+ IList < SessionFsSqliteStatement > statements ,
81+ CancellationToken cancellationToken ) ;
82+
5183 /// <summary>
5284 /// Checks whether the per-session SQLite database already exists, without creating it.
5385 /// </summary>
5486 /// <param name="cancellationToken">Cancellation token.</param>
5587 Task < bool > ExistsAsync ( CancellationToken cancellationToken ) ;
5688}
5789
90+ /// <summary>
91+ /// Thrown by an <see cref="ISessionFsSqliteProvider"/> to classify a failed SQLite transaction.
92+ /// <see cref="SessionFsSqliteTransactionErrorClass.BusyOrLocked"/> guarantees the transaction
93+ /// rolled back and is safe to retry; <see cref="SessionFsSqliteTransactionErrorClass.PostCommitAmbiguous"/>
94+ /// must never be retried.
95+ /// </summary>
96+ [ Experimental ( Diagnostics . Experimental ) ]
97+ public sealed class SessionFsSqliteTransactionException : Exception
98+ {
99+ /// <summary>Initializes a new instance of the <see cref="SessionFsSqliteTransactionException"/> class.</summary>
100+ /// <param name="message">Human-readable failure description.</param>
101+ /// <param name="errorClass">How the runtime should classify the failure.</param>
102+ /// <param name="innerException">Optional underlying exception.</param>
103+ public SessionFsSqliteTransactionException (
104+ string message ,
105+ SessionFsSqliteTransactionErrorClass errorClass ,
106+ Exception ? innerException = null )
107+ : base ( message , innerException )
108+ {
109+ ErrorClass = errorClass ;
110+ }
111+
112+ /// <summary>Gets the failure classification reported to the runtime.</summary>
113+ public SessionFsSqliteTransactionErrorClass ErrorClass { get ; }
114+ }
115+
58116/// <summary>
59117/// Base class for session filesystem providers. Subclasses override the
60118/// virtual methods and use normal C# patterns (return values, throw exceptions).
@@ -309,6 +367,64 @@ async Task<SessionFsSqliteQueryResult> ISessionFsHandler.SqliteQueryAsync(Sessio
309367 }
310368 }
311369
370+ async Task < SessionFsSqliteTransactionResult > ISessionFsHandler . SqliteTransactionAsync ( SessionFsSqliteTransactionRequest request , CancellationToken cancellationToken )
371+ {
372+ if ( this is not ISessionFsSqliteProvider sqliteProvider )
373+ {
374+ return new SessionFsSqliteTransactionResult
375+ {
376+ Error = new SessionFsSqliteTransactionError
377+ {
378+ ErrorClass = SessionFsSqliteTransactionErrorClass . Fatal ,
379+ Message = "SQLite is not supported by this provider." ,
380+ } ,
381+ } ;
382+ }
383+
384+ IList < SessionFsSqliteResult > results ;
385+ try
386+ {
387+ var statements = request . Statements . Select ( statement => new SessionFsSqliteStatement
388+ {
389+ QueryType = statement . QueryType ,
390+ Query = statement . Query ,
391+ Params = statement . Params ? . ToDictionary ( kvp => kvp . Key , kvp => JsonElementToValue ( kvp . Value ) ) ,
392+ } ) . ToList ( ) ;
393+ results = await sqliteProvider . TransactionAsync ( statements , cancellationToken ) . ConfigureAwait ( false ) ;
394+ }
395+ catch ( SessionFsSqliteTransactionException ex )
396+ {
397+ return new SessionFsSqliteTransactionResult
398+ {
399+ Error = new SessionFsSqliteTransactionError { ErrorClass = ex . ErrorClass , Message = ex . Message } ,
400+ } ;
401+ }
402+ catch ( Exception ex )
403+ {
404+ return new SessionFsSqliteTransactionResult
405+ {
406+ Error = new SessionFsSqliteTransactionError
407+ {
408+ ErrorClass = SessionFsSqliteTransactionErrorClass . Fatal ,
409+ Message = ex . Message ,
410+ } ,
411+ } ;
412+ }
413+
414+ return new SessionFsSqliteTransactionResult
415+ {
416+ Results = results . Select ( result => new SessionFsSqliteQueryResult
417+ {
418+ Rows = result . Rows ? . Select ( row => ( IDictionary < string , JsonElement > ) row . ToDictionary (
419+ kvp => kvp . Key ,
420+ kvp => CopilotClient . ToJsonElementForWire ( kvp . Value ) ! . Value ) ) . ToList ( ) ?? [ ] ,
421+ Columns = result . Columns ?? [ ] ,
422+ RowsAffected = result . RowsAffected ,
423+ LastInsertRowid = result . LastInsertRowid ,
424+ } ) . ToList ( ) ,
425+ } ;
426+ }
427+
312428 async Task < SessionFsSqliteExistsResult > ISessionFsHandler . SqliteExistsAsync ( SessionFsSqliteExistsRequest request , CancellationToken cancellationToken )
313429 {
314430 if ( this is not ISessionFsSqliteProvider sqliteProvider )
0 commit comments