/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
using GitHub.Copilot.Rpc;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
namespace GitHub.Copilot;
///
/// Result of a SQLite query execution via .
/// Same shape as but without the Error field,
/// since providers signal errors by throwing.
///
public sealed class SessionFsSqliteResult
{
/// Column names from the result set.
public IList Columns { get; set; } = [];
/// For SELECT: rows as column-keyed dictionaries. For others: empty.
public IList> Rows { get; set; } = [];
/// Number of rows affected (for INSERT/UPDATE/DELETE).
public long RowsAffected { get; set; }
/// Last inserted row ID (for INSERT).
public long? LastInsertRowid { get; set; }
}
///
/// One statement in an atomic SQLite transaction passed to
/// .
///
[Experimental(Diagnostics.Experimental)]
public sealed class SessionFsSqliteStatement
{
/// How to execute: "exec", "query", or "run".
public SessionFsSqliteQueryType QueryType { get; set; }
/// SQL statement to execute.
public string Query { get; set; } = string.Empty;
/// Optional named bind parameters.
public IDictionary? Params { get; set; }
}
///
/// Optional interface for subclasses that support
/// per-session SQLite databases. Implement this interface on your provider to enable
/// the runtime's SQL tool to route queries through your SessionFs implementation.
///
public interface ISessionFsSqliteProvider
{
///
/// Executes a SQLite query against the per-session database.
///
/// How to execute: "exec" for DDL/multi-statement, "query" for SELECT, "run" for INSERT/UPDATE/DELETE.
/// SQL query to execute.
/// Optional named bind parameters.
/// Cancellation token.
/// The query result, or null for exec-type queries.
Task QueryAsync(
SessionFsSqliteQueryType queryType,
string query,
IDictionary? bindParams,
CancellationToken cancellationToken);
///
/// Checks whether the per-session SQLite database already exists, without creating it.
///
/// Cancellation token.
Task ExistsAsync(CancellationToken cancellationToken);
}
///
/// Optional capability for session filesystem providers that support atomic SQLite transactions.
///
public interface ISessionFsSqliteTransactionProvider
{
///
/// Executes atomically against the per-session database.
///
/// Statements to execute in order, inside a single transaction.
/// Cancellation token.
/// One result per statement, in the same order as .
///
/// Thrown to tell the runtime how the failure should be classified. Any other exception
/// is reported as .
///
Task> TransactionAsync(
IList statements,
CancellationToken cancellationToken);
}
///
/// Thrown by an to classify a failed SQLite transaction.
/// guarantees the transaction
/// rolled back and is safe to retry;
/// must never be retried.
///
[Experimental(Diagnostics.Experimental)]
public sealed class SessionFsSqliteTransactionException : Exception
{
/// Initializes a new instance of the class.
/// Human-readable failure description.
/// How the runtime should classify the failure.
/// Optional underlying exception.
public SessionFsSqliteTransactionException(
string message,
SessionFsSqliteTransactionErrorClass errorClass,
Exception? innerException = null)
: base(message, innerException)
{
ErrorClass = errorClass;
}
/// Gets the failure classification reported to the runtime.
public SessionFsSqliteTransactionErrorClass ErrorClass { get; }
}
///
/// Base class for session filesystem providers. Subclasses override the
/// virtual methods and use normal C# patterns (return values, throw exceptions).
/// The base class catches exceptions and converts them to
/// results expected by the runtime.
/// To add SQLite support, also implement .
///
public abstract class SessionFsProvider : ISessionFsHandler
{
/// Reads the full content of a file. Throw if the file does not exist.
/// SessionFs-relative path.
/// Cancellation token.
/// The file content as a UTF-8 string.
protected abstract Task ReadFileAsync(string path, CancellationToken cancellationToken);
/// Writes content to a file, creating it (and parent directories) if needed.
/// SessionFs-relative path.
/// Content to write.
/// Optional POSIX-style permission mode. Null means use OS default.
/// Cancellation token.
protected abstract Task WriteFileAsync(string path, string content, int? mode, CancellationToken cancellationToken);
/// Appends content to a file, creating it (and parent directories) if needed.
/// SessionFs-relative path.
/// Content to append.
/// Optional POSIX-style permission mode. Null means use OS default.
/// Cancellation token.
protected abstract Task AppendFileAsync(string path, string content, int? mode, CancellationToken cancellationToken);
/// Checks whether a path exists.
/// SessionFs-relative path.
/// Cancellation token.
/// true if the path exists, false otherwise.
protected abstract Task ExistsAsync(string path, CancellationToken cancellationToken);
/// Gets metadata about a file or directory. Throw if the path does not exist.
/// SessionFs-relative path.
/// Cancellation token.
protected abstract Task StatAsync(string path, CancellationToken cancellationToken);
/// Creates a directory (and optionally parents). Does not fail if it already exists.
/// SessionFs-relative path.
/// Whether to create parent directories.
/// Optional POSIX-style permission mode (e.g., 0x1FF for 0777). Null means use OS default.
/// Cancellation token.
protected abstract Task MakeDirectoryAsync(string path, bool recursive, int? mode, CancellationToken cancellationToken);
/// Lists entry names in a directory. Throw if the directory does not exist.
/// SessionFs-relative path.
/// Cancellation token.
protected abstract Task> ReadDirectoryAsync(string path, CancellationToken cancellationToken);
/// Lists entries with type info in a directory. Throw if the directory does not exist.
/// SessionFs-relative path.
/// Cancellation token.
protected abstract Task> ReadDirectoryWithTypesAsync(string path, CancellationToken cancellationToken);
/// Removes a file or directory. Throw if the path does not exist (unless is true).
/// SessionFs-relative path.
/// Whether to remove directory contents recursively.
/// If true, do not throw when the path does not exist.
/// Cancellation token.
protected abstract Task RemoveAsync(string path, bool recursive, bool force, CancellationToken cancellationToken);
/// Renames/moves a file or directory.
/// Source path.
/// Destination path.
/// Cancellation token.
protected abstract Task RenameAsync(string src, string dest, CancellationToken cancellationToken);
// ---- ISessionFsHandler implementation (private, handles error mapping) ----
async Task ISessionFsHandler.ReadFileAsync(SessionFsReadFileRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
var content = await ReadFileAsync(request.Path, cancellationToken).ConfigureAwait(false);
return new SessionFsReadFileResult { Content = content };
}
catch (Exception ex)
{
return new SessionFsReadFileResult { Error = ToSessionFsError(ex) };
}
}
async Task ISessionFsHandler.WriteFileAsync(SessionFsWriteFileRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await WriteFileAsync(request.Path, request.Content, (int?)request.Mode, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task ISessionFsHandler.AppendFileAsync(SessionFsAppendFileRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await AppendFileAsync(request.Path, request.Content, (int?)request.Mode, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task ISessionFsHandler.ExistsAsync(SessionFsExistsRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
var exists = await ExistsAsync(request.Path, cancellationToken).ConfigureAwait(false);
return new SessionFsExistsResult { Exists = exists };
}
catch
{
return new SessionFsExistsResult { Exists = false };
}
}
async Task ISessionFsHandler.StatAsync(SessionFsStatRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
return await StatAsync(request.Path, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
return new SessionFsStatResult { Error = ToSessionFsError(ex) };
}
}
async Task ISessionFsHandler.MkdirAsync(SessionFsMkdirRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await MakeDirectoryAsync(request.Path, request.Recursive ?? false, (int?)request.Mode, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task ISessionFsHandler.ReaddirAsync(SessionFsReaddirRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
var entries = await ReadDirectoryAsync(request.Path, cancellationToken).ConfigureAwait(false);
return new SessionFsReaddirResult { Entries = entries };
}
catch (Exception ex)
{
return new SessionFsReaddirResult { Error = ToSessionFsError(ex) };
}
}
async Task ISessionFsHandler.ReaddirWithTypesAsync(SessionFsReaddirWithTypesRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
var entries = await ReadDirectoryWithTypesAsync(request.Path, cancellationToken).ConfigureAwait(false);
return new SessionFsReaddirWithTypesResult { Entries = entries };
}
catch (Exception ex)
{
return new SessionFsReaddirWithTypesResult { Error = ToSessionFsError(ex) };
}
}
async Task ISessionFsHandler.RmAsync(SessionFsRmRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await RemoveAsync(request.Path, request.Recursive ?? false, request.Force ?? false, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task ISessionFsHandler.RenameAsync(SessionFsRenameRequest request, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(request);
try
{
await RenameAsync(request.Src, request.Dest, cancellationToken).ConfigureAwait(false);
return null;
}
catch (Exception ex)
{
return ToSessionFsError(ex);
}
}
async Task ISessionFsHandler.SqliteQueryAsync(SessionFsSqliteQueryRequest request, CancellationToken cancellationToken)
{
if (this is not ISessionFsSqliteProvider sqliteProvider)
{
return new SessionFsSqliteQueryResult
{
Error = new SessionFsError { Code = SessionFsErrorCode.UNKNOWN, Message = "SQLite is not supported by this provider." },
};
}
try
{
var bindParams = request.Params?.ToDictionary(
kvp => kvp.Key,
kvp => JsonElementToValue(kvp.Value));
var result = await sqliteProvider.QueryAsync(request.QueryType, request.Query, bindParams, cancellationToken).ConfigureAwait(false);
return new SessionFsSqliteQueryResult
{
Rows = result?.Rows?.Select(row => (IDictionary)row.ToDictionary(
kvp => kvp.Key,
kvp => ToJsonElement(kvp.Value))).ToList() ?? [],
Columns = result?.Columns ?? [],
RowsAffected = result?.RowsAffected ?? 0,
LastInsertRowid = result?.LastInsertRowid,
};
}
catch (Exception ex)
{
return new SessionFsSqliteQueryResult { Error = ToSessionFsError(ex) };
}
}
async Task 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 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,
},
};
}
try
{
return new SessionFsSqliteTransactionResult
{
Results = results.Select(result => new SessionFsSqliteQueryResult
{
Rows = result.Rows?.Select(row => (IDictionary)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 ISessionFsHandler.SqliteExistsAsync(SessionFsSqliteExistsRequest request, CancellationToken cancellationToken)
{
if (this is not ISessionFsSqliteProvider sqliteProvider)
{
return new SessionFsSqliteExistsResult { Exists = false };
}
try
{
var exists = await sqliteProvider.ExistsAsync(cancellationToken).ConfigureAwait(false);
return new SessionFsSqliteExistsResult { Exists = exists };
}
catch
{
return new SessionFsSqliteExistsResult { Exists = false };
}
}
private static SessionFsError ToSessionFsError(Exception ex)
{
var code = ex is FileNotFoundException or DirectoryNotFoundException
? SessionFsErrorCode.ENOENT
: SessionFsErrorCode.UNKNOWN;
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,
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.String => element.GetString(),
JsonValueKind.Number => element.TryGetInt64(out var l) ? l : element.GetDouble(),
_ => element.GetRawText(),
};
}