Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
.github/workflows/*.lock.yml linguist-generated=true merge=ours

# Cross-platform tools rewrite these files, so keep their output deterministic.
java/**/*.java text eol=lf

# Generated files — keep LF line endings so codegen output is deterministic across platforms.
nodejs/src/generated/* eol=lf linguist-generated=true
dotnet/src/Generated/* eol=lf linguist-generated=true
Expand Down
20,502 changes: 12,113 additions & 8,389 deletions dotnet/src/Generated/Rpc.cs

Large diffs are not rendered by default.

555 changes: 551 additions & 4 deletions dotnet/src/Generated/SessionEvents.cs

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1145,7 +1145,7 @@ internal void SetCanvasHandler(ICanvasHandler? handler)
ClientSessionApis.Canvas = handler is null ? null : new CanvasHandlerAdapter(handler);
}

private static readonly JsonElement NullJsonElement = JsonDocument.Parse("null").RootElement.Clone();
private static readonly JsonElement NullJsonElement = JsonElement.Parse("null");

private static JsonElement SerializeActionResult(object? value)
{
Expand Down Expand Up @@ -1823,6 +1823,7 @@ await Rpc.Model.SwitchToAsync(
null,
options.ModelCapabilities,
options.ContextTier,
null,
cancellationToken);
}

Expand Down
141 changes: 140 additions & 1 deletion dotnet/src/SessionFsProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*--------------------------------------------------------------------------------------------*/

using GitHub.Copilot.Rpc;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;

namespace GitHub.Copilot;
Expand All @@ -27,6 +28,23 @@ public sealed class SessionFsSqliteResult
public long? LastInsertRowid { get; set; }
}

/// <summary>
/// One statement in an atomic SQLite transaction passed to
/// <see cref="ISessionFsSqliteTransactionProvider.TransactionAsync"/>.
/// </summary>
[Experimental(Diagnostics.Experimental)]
public sealed class SessionFsSqliteStatement
{
/// <summary>How to execute: <c>"exec"</c>, <c>"query"</c>, or <c>"run"</c>.</summary>
public SessionFsSqliteQueryType QueryType { get; set; }

/// <summary>SQL statement to execute.</summary>
public string Query { get; set; } = string.Empty;

/// <summary>Optional named bind parameters.</summary>
public IDictionary<string, object?>? Params { get; set; }
}

/// <summary>
/// Optional interface for <see cref="SessionFsProvider"/> subclasses that support
/// per-session SQLite databases. Implement this interface on your provider to enable
Expand Down Expand Up @@ -55,6 +73,52 @@ public interface ISessionFsSqliteProvider
Task<bool> ExistsAsync(CancellationToken cancellationToken);
}

/// <summary>
/// Optional capability for session filesystem providers that support atomic SQLite transactions.
/// </summary>
public interface ISessionFsSqliteTransactionProvider
{
/// <summary>
/// Executes <paramref name="statements"/> atomically against the per-session database.
/// </summary>
/// <param name="statements">Statements to execute in order, inside a single transaction.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>One result per statement, in the same order as <paramref name="statements"/>.</returns>
/// <exception cref="SessionFsSqliteTransactionException">
/// Thrown to tell the runtime how the failure should be classified. Any other exception
/// is reported as <see cref="SessionFsSqliteTransactionErrorClass.Fatal"/>.
/// </exception>
Task<IList<SessionFsSqliteResult>> TransactionAsync(
IList<SessionFsSqliteStatement> statements,
CancellationToken cancellationToken);
}

/// <summary>
/// Thrown by an <see cref="ISessionFsSqliteTransactionProvider"/> to classify a failed SQLite transaction.
/// <see cref="SessionFsSqliteTransactionErrorClass.BusyOrLocked"/> guarantees the transaction
/// rolled back and is safe to retry; <see cref="SessionFsSqliteTransactionErrorClass.PostCommitAmbiguous"/>
/// must never be retried.
/// </summary>
[Experimental(Diagnostics.Experimental)]
public sealed class SessionFsSqliteTransactionException : Exception
{
/// <summary>Initializes a new instance of the <see cref="SessionFsSqliteTransactionException"/> class.</summary>
/// <param name="message">Human-readable failure description.</param>
/// <param name="errorClass">How the runtime should classify the failure.</param>
/// <param name="innerException">Optional underlying exception.</param>
public SessionFsSqliteTransactionException(
string message,
SessionFsSqliteTransactionErrorClass errorClass,
Exception? innerException = null)
: base(message, innerException)
{
ErrorClass = errorClass;
}

/// <summary>Gets the failure classification reported to the runtime.</summary>
public SessionFsSqliteTransactionErrorClass ErrorClass { get; }
}

/// <summary>
/// Base class for session filesystem providers. Subclasses override the
/// virtual methods and use normal C# patterns (return values, throw exceptions).
Expand Down Expand Up @@ -297,7 +361,7 @@ async Task<SessionFsSqliteQueryResult> ISessionFsHandler.SqliteQueryAsync(Sessio
{
Rows = result?.Rows?.Select(row => (IDictionary<string, JsonElement>)row.ToDictionary(
kvp => kvp.Key,
kvp => CopilotClient.ToJsonElementForWire(kvp.Value)!.Value)).ToList() ?? [],
kvp => ToJsonElement(kvp.Value))).ToList() ?? [],
Columns = result?.Columns ?? [],
RowsAffected = result?.RowsAffected ?? 0,
LastInsertRowid = result?.LastInsertRowid,
Expand All @@ -309,6 +373,78 @@ async Task<SessionFsSqliteQueryResult> ISessionFsHandler.SqliteQueryAsync(Sessio
}
}

async Task<SessionFsSqliteTransactionResult> ISessionFsHandler.SqliteTransactionAsync(SessionFsSqliteTransactionRequest request, CancellationToken cancellationToken)
{
if (this is not ISessionFsSqliteTransactionProvider transactionProvider)
{
return new SessionFsSqliteTransactionResult
{
Error = new SessionFsSqliteTransactionError
{
ErrorClass = SessionFsSqliteTransactionErrorClass.Fatal,
Message = "SQLite is not supported by this provider.",
},
};
}

IList<SessionFsSqliteResult> results;
try
{
var statements = request.Statements.Select(statement => new SessionFsSqliteStatement
{
QueryType = statement.QueryType,
Query = statement.Query,
Params = statement.Params?.ToDictionary(kvp => kvp.Key, kvp => JsonElementToValue(kvp.Value)),
}).ToList();
results = await transactionProvider.TransactionAsync(statements, cancellationToken).ConfigureAwait(false);
}
catch (SessionFsSqliteTransactionException ex)
{
return new SessionFsSqliteTransactionResult
{
Error = new SessionFsSqliteTransactionError { ErrorClass = ex.ErrorClass, Message = ex.Message },
};
}
catch (Exception ex)
{
return new SessionFsSqliteTransactionResult
{
Error = new SessionFsSqliteTransactionError
{
ErrorClass = SessionFsSqliteTransactionErrorClass.Fatal,
Message = ex.Message,
},
};
}
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

try
{
return new SessionFsSqliteTransactionResult
{
Results = results.Select(result => new SessionFsSqliteQueryResult
{
Rows = result.Rows?.Select(row => (IDictionary<string, JsonElement>)row.ToDictionary(
kvp => kvp.Key,
kvp => ToJsonElement(kvp.Value))).ToList() ?? [],
Columns = result.Columns ?? [],
RowsAffected = result.RowsAffected,
LastInsertRowid = result.LastInsertRowid,
}).ToList(),
};
}
catch (Exception ex)
{
return new SessionFsSqliteTransactionResult
{
Error = new SessionFsSqliteTransactionError
{
ErrorClass = SessionFsSqliteTransactionErrorClass.PostCommitAmbiguous,
Message = ex.Message,
},
};
}
}

async Task<SessionFsSqliteExistsResult> ISessionFsHandler.SqliteExistsAsync(SessionFsSqliteExistsRequest request, CancellationToken cancellationToken)
{
if (this is not ISessionFsSqliteProvider sqliteProvider)
Expand Down Expand Up @@ -336,6 +472,9 @@ private static SessionFsError ToSessionFsError(Exception ex)
return new SessionFsError { Code = code, Message = ex.Message };
}

private static JsonElement ToJsonElement(object? value) =>
CopilotClient.ToJsonElementForWire(value) ?? JsonElement.Parse("null");

private static object? JsonElementToValue(JsonElement element) => element.ValueKind switch
{
JsonValueKind.Null => null,
Expand Down
8 changes: 4 additions & 4 deletions dotnet/test/E2E/CommandsE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public async Task Session_Commands_List_Returns_Builtins_And_Respects_Client_Com
await TestHelper.WaitForConditionAsync(
async () =>
{
clientCommands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
clientCommands = await session.Rpc.Commands.ListAsync(new SessionCommandsListRequest
{
IncludeBuiltins = false,
IncludeClientCommands = true,
Expand All @@ -45,7 +45,7 @@ await TestHelper.WaitForConditionAsync(
Assert.Contains(clientCommands.Commands, c => IsCommand(c, "rollback", SlashCommandKind.Client));
Assert.DoesNotContain(clientCommands.Commands, c => c.Kind == SlashCommandKind.Builtin);

var builtinCommands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
var builtinCommands = await session.Rpc.Commands.ListAsync(new SessionCommandsListRequest
{
IncludeBuiltins = true,
IncludeClientCommands = false,
Expand All @@ -64,7 +64,7 @@ public async Task Session_Commands_Invoke_Known_Builtin_Returns_Expected_Result(
{
var session = await CreateSessionAsync();

var builtinCommands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
var builtinCommands = await session.Rpc.Commands.ListAsync(new SessionCommandsListRequest
{
IncludeBuiltins = true,
IncludeClientCommands = false,
Expand Down Expand Up @@ -128,7 +128,7 @@ public async Task Session_Commands_Execute_Runs_Registered_Command_Handler()
await TestHelper.WaitForConditionAsync(
async () =>
{
var commands = await session.Rpc.Commands.ListAsync(new CommandsListRequest
var commands = await session.Rpc.Commands.ListAsync(new SessionCommandsListRequest
{
IncludeBuiltins = false,
IncludeClientCommands = true,
Expand Down
74 changes: 65 additions & 9 deletions dotnet/test/E2E/InMemorySessionFsSqliteHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ internal record SqliteCall(string SessionId, string QueryType, string Query);
/// for file operations instead of touching disk.
/// </summary>
internal sealed class InMemorySessionFsSqliteHandler(string sessionId, List<SqliteCall> sqliteCalls)
: SessionFsProvider, ISessionFsSqliteProvider
: SessionFsProvider, ISessionFsSqliteProvider, ISessionFsSqliteTransactionProvider
{
internal ConcurrentDictionary<string, string> Files { get; } = new();
private readonly ConcurrentDictionary<string, byte> _directories = new();
Expand Down Expand Up @@ -45,28 +45,82 @@ private SqliteConnection GetOrCreateDb()
string query,
IDictionary<string, object?>? bindParams,
CancellationToken cancellationToken)
{
return Task.FromResult(RunStatement(GetOrCreateDb(), null, queryType, query, bindParams));
}

public Task<IList<SessionFsSqliteResult>> TransactionAsync(
IList<SessionFsSqliteStatement> statements,
CancellationToken cancellationToken)
{
var db = GetOrCreateDb();
using var transaction = db.BeginTransaction();
try
{
IList<SessionFsSqliteResult> results = statements
.Select(statement => RunStatement(db, transaction, statement.QueryType, statement.Query, statement.Params)
?? new SessionFsSqliteResult())
.ToList();
try
{
transaction.Commit();
}
catch (Exception ex)
{
throw new SessionFsSqliteTransactionException(
ex.Message,
SessionFsSqliteTransactionErrorClass.PostCommitAmbiguous,
ex);
}
return Task.FromResult(results);
}
catch (SessionFsSqliteTransactionException)
{
throw;
}
catch (SqliteException ex)
{
transaction.Rollback();
var errorClass = ex.SqliteErrorCode is 5 or 6
? SessionFsSqliteTransactionErrorClass.BusyOrLocked
: SessionFsSqliteTransactionErrorClass.Fatal;
throw new SessionFsSqliteTransactionException(ex.Message, errorClass, ex);
}
catch (Exception ex)
{
transaction.Rollback();
throw new SessionFsSqliteTransactionException(ex.Message, SessionFsSqliteTransactionErrorClass.Fatal, ex);
}
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
}

private SessionFsSqliteResult? RunStatement(
SqliteConnection db,
SqliteTransaction? transaction,
SessionFsSqliteQueryType queryType,
string query,
IDictionary<string, object?>? bindParams)
{
sqliteCalls.Add(new SqliteCall(sessionId, queryType.Value, query));

var trimmed = query.Trim();
if (trimmed.Length == 0)
{
return Task.FromResult<SessionFsSqliteResult?>(null);
return null;
}

var db = GetOrCreateDb();

if (queryType == SessionFsSqliteQueryType.Exec)
{
using var cmd = db.CreateCommand();
cmd.Transaction = transaction;
cmd.CommandText = trimmed;
cmd.ExecuteNonQuery();
return Task.FromResult<SessionFsSqliteResult?>(null);
return null;
}

if (queryType == SessionFsSqliteQueryType.Query)
{
using var cmd = db.CreateCommand();
cmd.Transaction = transaction;
cmd.CommandText = trimmed;
AddParams(cmd, bindParams);

Expand All @@ -88,33 +142,35 @@ private SqliteConnection GetOrCreateDb()
rows.Add(row);
}

return Task.FromResult<SessionFsSqliteResult?>(new SessionFsSqliteResult
return new SessionFsSqliteResult
{
Columns = columns,
Rows = rows,
RowsAffected = 0,
});
};
}

if (queryType == SessionFsSqliteQueryType.Run)
{
using var cmd = db.CreateCommand();
cmd.Transaction = transaction;
cmd.CommandText = trimmed;
AddParams(cmd, bindParams);

var rowsAffected = cmd.ExecuteNonQuery();

using var rowidCmd = db.CreateCommand();
rowidCmd.Transaction = transaction;
rowidCmd.CommandText = "SELECT last_insert_rowid()";
var lastRowid = rowidCmd.ExecuteScalar();

return Task.FromResult<SessionFsSqliteResult?>(new SessionFsSqliteResult
return new SessionFsSqliteResult
{
Columns = [],
Rows = [],
RowsAffected = rowsAffected,
LastInsertRowid = lastRowid is long l ? l : null,
});
};
}

throw new ArgumentException($"Unknown queryType: {queryType}");
Expand Down
Loading
Loading