/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ using System.Collections.Concurrent; using GitHub.Copilot; using GitHub.Copilot.Rpc; using Microsoft.Data.Sqlite; namespace GitHub.Copilot.Test.E2E; internal record SqliteCall(string SessionId, string QueryType, string Query); /// /// A SessionFsProvider that implements with a real /// in-memory SQLite database, and uses a simple /// for file operations instead of touching disk. /// internal sealed class InMemorySessionFsSqliteHandler(string sessionId, List sqliteCalls) : SessionFsProvider, ISessionFsSqliteProvider, ISessionFsSqliteTransactionProvider { internal ConcurrentDictionary Files { get; } = new(); private readonly ConcurrentDictionary _directories = new(); private SqliteConnection? _db; private SqliteConnection GetOrCreateDb() { if (_db is not null) { return _db; } _db = new SqliteConnection("Data Source=:memory:"); _db.Open(); using var cmd = _db.CreateCommand(); cmd.CommandText = "PRAGMA busy_timeout = 5000"; cmd.ExecuteNonQuery(); return _db; } // ---- ISessionFsSqliteProvider ---- public Task QueryAsync( SessionFsSqliteQueryType queryType, string query, IDictionary? bindParams, CancellationToken cancellationToken) { return Task.FromResult(RunStatement(GetOrCreateDb(), null, queryType, query, bindParams)); } public Task> TransactionAsync( IList statements, CancellationToken cancellationToken) { var db = GetOrCreateDb(); using var transaction = db.BeginTransaction(); try { IList 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); } } private SessionFsSqliteResult? RunStatement( SqliteConnection db, SqliteTransaction? transaction, SessionFsSqliteQueryType queryType, string query, IDictionary? bindParams) { sqliteCalls.Add(new SqliteCall(sessionId, queryType.Value, query)); var trimmed = query.Trim(); if (trimmed.Length == 0) { return null; } if (queryType == SessionFsSqliteQueryType.Exec) { using var cmd = db.CreateCommand(); cmd.Transaction = transaction; cmd.CommandText = trimmed; cmd.ExecuteNonQuery(); return null; } if (queryType == SessionFsSqliteQueryType.Query) { using var cmd = db.CreateCommand(); cmd.Transaction = transaction; cmd.CommandText = trimmed; AddParams(cmd, bindParams); using var reader = cmd.ExecuteReader(); var columns = new List(); for (var i = 0; i < reader.FieldCount; i++) { columns.Add(reader.GetName(i)); } var rows = new List>(); while (reader.Read()) { var row = new Dictionary(reader.FieldCount); for (var i = 0; i < reader.FieldCount; i++) { row[columns[i]] = reader.IsDBNull(i) ? null! : reader.GetValue(i); } rows.Add(row); } 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 new SessionFsSqliteResult { Columns = [], Rows = [], RowsAffected = rowsAffected, LastInsertRowid = lastRowid is long l ? l : null, }; } throw new ArgumentException($"Unknown queryType: {queryType}"); } public Task ExistsAsync(CancellationToken cancellationToken) { return Task.FromResult(_db is not null); } private static void AddParams(SqliteCommand cmd, IDictionary? bindParams) { if (bindParams is null) return; foreach (var (key, value) in bindParams) { cmd.Parameters.AddWithValue(key.StartsWith(':') || key.StartsWith('$') || key.StartsWith('@') ? key : $":{key}", value ?? DBNull.Value); } } // ---- File operations (in-memory) ---- private string Resolve(string path) => $"/{sessionId}{(path.StartsWith('/') ? path : "/" + path)}"; protected override Task ReadFileAsync(string path, CancellationToken cancellationToken) { var key = Resolve(path); if (!Files.TryGetValue(key, out var content)) throw new FileNotFoundException($"File not found: {path}"); return Task.FromResult(content); } protected override Task WriteFileAsync(string path, string content, int? mode, CancellationToken cancellationToken) { Files[Resolve(path)] = content; return Task.CompletedTask; } protected override Task AppendFileAsync(string path, string content, int? mode, CancellationToken cancellationToken) { Files.AddOrUpdate(Resolve(path), content, (_, existing) => existing + content); return Task.CompletedTask; } protected override Task ExistsAsync(string path, CancellationToken cancellationToken) { var key = Resolve(path); return Task.FromResult(Files.ContainsKey(key) || _directories.ContainsKey(key)); } protected override Task StatAsync(string path, CancellationToken cancellationToken) { var key = Resolve(path); if (Files.TryGetValue(key, out var fileContent)) return Task.FromResult(new SessionFsStatResult { IsFile = true, IsDirectory = false, Size = fileContent.Length }); if (_directories.ContainsKey(key)) return Task.FromResult(new SessionFsStatResult { IsFile = false, IsDirectory = true, Size = 0 }); throw new FileNotFoundException($"Path does not exist: {path}"); } protected override Task MakeDirectoryAsync(string path, bool recursive, int? mode, CancellationToken cancellationToken) { _directories[Resolve(path)] = 0; return Task.CompletedTask; } protected override Task> ReadDirectoryAsync(string path, CancellationToken cancellationToken) => Task.FromResult>([]); protected override Task> ReadDirectoryWithTypesAsync(string path, CancellationToken cancellationToken) => Task.FromResult>([]); protected override Task RemoveAsync(string path, bool recursive, bool force, CancellationToken cancellationToken) { var key = Resolve(path); Files.TryRemove(key, out _); _directories.TryRemove(key, out _); return Task.CompletedTask; } protected override Task RenameAsync(string src, string dest, CancellationToken cancellationToken) { var srcKey = Resolve(src); var destKey = Resolve(dest); if (Files.TryRemove(srcKey, out var content)) Files[destKey] = content; return Task.CompletedTask; } }