Skip to content

Commit 6ab9e8e

Browse files
Align Go/Python/Rust SQLite provider APIs with Node.js/.NET design
- Remove session_id parameter from sqlite_query/sqlite_exists (providers are already session-scoped) - Add clean SessionFsSqliteQueryResult types without error field (errors signaled by raising/returning errors, not via result field) - Update adapters to convert clean result types to generated RPC types - Update all tests to use new signatures and clean result types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0a5f123 commit 6ab9e8e

9 files changed

Lines changed: 114 additions & 68 deletions

File tree

go/internal/e2e/session_fs_sqlite_e2e_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -195,12 +195,12 @@ func (p *inMemorySqliteProvider) Rename(src string, dest string) error {
195195
return nil
196196
}
197197

198-
func (p *inMemorySqliteProvider) SqliteQuery(sessionID string, query string, queryType rpc.SessionFsSqliteQueryType, params map[string]any) (*rpc.SessionFsSqliteQueryResult, error) {
198+
func (p *inMemorySqliteProvider) SqliteQuery(query string, queryType rpc.SessionFsSqliteQueryType, params map[string]any) (*copilot.SessionFsSqliteQueryResult, error) {
199199
p.mu.Lock()
200200
defer p.mu.Unlock()
201201
p.hadQuery = true
202202
*p.sqliteCalls = append(*p.sqliteCalls, sqliteCall{
203-
SessionID: sessionID,
203+
SessionID: "stub",
204204
QueryType: string(queryType),
205205
Query: query,
206206
})
@@ -212,28 +212,28 @@ func (p *inMemorySqliteProvider) SqliteQuery(sessionID string, query string, que
212212
upper := strings.ToUpper(strings.TrimSpace(query))
213213
switch queryType {
214214
case rpc.SessionFsSqliteQueryTypeExec:
215-
return &rpc.SessionFsSqliteQueryResult{Columns: []string{}, Rows: []map[string]any{}}, nil
215+
return &copilot.SessionFsSqliteQueryResult{Columns: []string{}, Rows: []map[string]any{}}, nil
216216
case rpc.SessionFsSqliteQueryTypeRun:
217217
lastID := float64(1)
218-
return &rpc.SessionFsSqliteQueryResult{
218+
return &copilot.SessionFsSqliteQueryResult{
219219
Columns: []string{},
220220
Rows: []map[string]any{},
221221
RowsAffected: 1,
222222
LastInsertRowid: &lastID,
223223
}, nil
224224
case rpc.SessionFsSqliteQueryTypeQuery:
225225
if strings.Contains(upper, "SELECT") {
226-
return &rpc.SessionFsSqliteQueryResult{
226+
return &copilot.SessionFsSqliteQueryResult{
227227
Columns: []string{"id", "name"},
228228
Rows: []map[string]any{{"id": "a1", "name": "Widget"}},
229229
}, nil
230230
}
231-
return &rpc.SessionFsSqliteQueryResult{Columns: []string{}, Rows: []map[string]any{}}, nil
231+
return &copilot.SessionFsSqliteQueryResult{Columns: []string{}, Rows: []map[string]any{}}, nil
232232
}
233-
return &rpc.SessionFsSqliteQueryResult{Columns: []string{}, Rows: []map[string]any{}}, nil
233+
return &copilot.SessionFsSqliteQueryResult{Columns: []string{}, Rows: []map[string]any{}}, nil
234234
}
235235

236-
func (p *inMemorySqliteProvider) SqliteExists(sessionID string) (bool, error) {
236+
func (p *inMemorySqliteProvider) SqliteExists() (bool, error) {
237237
p.mu.Lock()
238238
defer p.mu.Unlock()
239239
return p.hadQuery, nil

go/session_fs_provider.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,24 @@ type SessionFsProvider interface {
5252
// may also implement to support per-session SQLite databases. The adapter
5353
// checks for this interface at runtime using a type assertion. If the
5454
// provider does not implement it, SQLite requests return an "unsupported" error.
55+
//
56+
// Providers are already session-scoped (created per session by the factory),
57+
// so these methods do not take a session ID parameter.
5558
type SessionFsSqliteProvider interface {
5659
// SqliteQuery executes a SQLite query against the provider's per-session database.
57-
SqliteQuery(sessionID string, query string, queryType rpc.SessionFsSqliteQueryType, params map[string]any) (*rpc.SessionFsSqliteQueryResult, error)
60+
SqliteQuery(query string, queryType rpc.SessionFsSqliteQueryType, params map[string]any) (*SessionFsSqliteQueryResult, error)
5861
// SqliteExists checks whether the provider has a SQLite database for the session.
59-
SqliteExists(sessionID string) (bool, error)
62+
SqliteExists() (bool, error)
63+
}
64+
65+
// SessionFsSqliteQueryResult holds the result of a SQLite query execution.
66+
// Same shape as the generated RPC type but without the Error field,
67+
// since providers signal errors by returning a Go error.
68+
type SessionFsSqliteQueryResult struct {
69+
Columns []string `json:"columns"`
70+
Rows []map[string]any `json:"rows"`
71+
RowsAffected int64 `json:"rowsAffected"`
72+
LastInsertRowid *float64 `json:"lastInsertRowid,omitempty"`
6073
}
6174

6275
// SessionFsFileInfo holds file metadata returned by SessionFsProvider.Stat.
@@ -188,7 +201,7 @@ func (a *sessionFsAdapter) SqliteQuery(request *rpc.SessionFsSqliteQueryRequest)
188201
Error: &rpc.SessionFsError{Code: rpc.SessionFsErrorCodeUNKNOWN, Message: &msg},
189202
}, nil
190203
}
191-
result, err := sp.SqliteQuery(request.SessionID, request.Query, request.QueryType, request.Params)
204+
result, err := sp.SqliteQuery(request.Query, request.QueryType, request.Params)
192205
if err != nil {
193206
return &rpc.SessionFsSqliteQueryResult{
194207
Columns: []string{},
@@ -197,15 +210,20 @@ func (a *sessionFsAdapter) SqliteQuery(request *rpc.SessionFsSqliteQueryRequest)
197210
Error: toSessionFsError(err),
198211
}, nil
199212
}
200-
return result, nil
213+
return &rpc.SessionFsSqliteQueryResult{
214+
Columns: result.Columns,
215+
Rows: result.Rows,
216+
RowsAffected: result.RowsAffected,
217+
LastInsertRowid: result.LastInsertRowid,
218+
}, nil
201219
}
202220

203221
func (a *sessionFsAdapter) SqliteExists(request *rpc.SessionFsSqliteExistsRequest) (*rpc.SessionFsSqliteExistsResult, error) {
204222
sp, ok := a.provider.(SessionFsSqliteProvider)
205223
if !ok {
206224
return &rpc.SessionFsSqliteExistsResult{Exists: false}, nil
207225
}
208-
exists, err := sp.SqliteExists(request.SessionID)
226+
exists, err := sp.SqliteExists()
209227
if err != nil {
210228
return &rpc.SessionFsSqliteExistsResult{Exists: false}, nil
211229
}

python/copilot/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
SessionFsFileInfo,
4444
SessionFsProvider,
4545
SessionFsSqliteProvider,
46+
SessionFsSqliteQueryResult,
4647
create_session_fs_adapter,
4748
)
4849
from .tools import (
@@ -88,6 +89,7 @@
8889
"SessionFsFileInfo",
8990
"SessionFsProvider",
9091
"SessionFsSqliteProvider",
92+
"SessionFsSqliteQueryResult",
9193
"create_session_fs_adapter",
9294
"SessionUiApi",
9395
"SessionUiCapabilities",

python/copilot/session_fs_provider.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@
3333
SessionFSReaddirWithTypesResult,
3434
SessionFSReadFileResult,
3535
SessionFSSqliteExistsResult,
36-
SessionFSSqliteQueryResult,
3736
SessionFSSqliteQueryType,
3837
SessionFSStatResult,
3938
)
39+
from .generated.rpc import (
40+
SessionFSSqliteQueryResult as _GeneratedSqliteQueryResult,
41+
)
4042

4143

4244
@dataclass
@@ -110,21 +112,37 @@ class MyProvider(SessionFsProvider, SessionFsSqliteProvider): ...
110112
111113
The adapter checks ``isinstance(provider, SessionFsSqliteProvider)`` at
112114
runtime to decide whether SQLite calls should be dispatched.
115+
116+
Providers are already session-scoped (created per session by the factory),
117+
so these methods do not take a ``session_id`` parameter.
113118
"""
114119

115120
@abc.abstractmethod
116121
async def sqlite_query(
117122
self,
118-
session_id: str,
119123
query: str,
120124
query_type: SessionFSSqliteQueryType,
121125
params: dict[str, float | str | None] | None = None,
122-
) -> SessionFSSqliteQueryResult:
126+
) -> SessionFsSqliteQueryResult:
123127
"""Execute a SQLite query against the provider's per-session database."""
124128

125129
@abc.abstractmethod
126-
async def sqlite_exists(self, session_id: str) -> bool:
127-
"""Return whether the provider has a SQLite database for *session_id*."""
130+
async def sqlite_exists(self) -> bool:
131+
"""Return whether the provider has a SQLite database for this session."""
132+
133+
134+
@dataclass
135+
class SessionFsSqliteQueryResult:
136+
"""Result of a SQLite query execution.
137+
138+
Same shape as the generated RPC type but without the ``error`` field,
139+
since providers signal errors by raising exceptions.
140+
"""
141+
142+
columns: list[str]
143+
rows: list[dict[str, Any]]
144+
rows_affected: int
145+
last_insert_rowid: float | None = None
128146

129147

130148
def create_session_fs_adapter(provider: SessionFsProvider) -> SessionFsHandler:
@@ -240,12 +258,12 @@ async def rename(self, params: Any) -> SessionFSError | None:
240258
except Exception as exc:
241259
return _to_session_fs_error(exc)
242260

243-
async def sqlite_query(self, params: Any) -> SessionFSSqliteQueryResult:
261+
async def sqlite_query(self, params: Any) -> _GeneratedSqliteQueryResult:
244262
# SQLite methods intentionally skip toSessionFsError wrapping — FS errno
245263
# mapping (ENOENT) isn't meaningful for SQL errors and the JSON-RPC layer
246264
# already handles uncaught exceptions.
247265
if not isinstance(self._p, SessionFsSqliteProvider):
248-
return SessionFSSqliteQueryResult(
266+
return _GeneratedSqliteQueryResult(
249267
columns=[],
250268
rows=[],
251269
rows_affected=0,
@@ -254,18 +272,23 @@ async def sqlite_query(self, params: Any) -> SessionFSSqliteQueryResult:
254272
message="SQLite is not supported by this SessionFs provider",
255273
),
256274
)
257-
return await self._p.sqlite_query(
258-
params.session_id,
275+
result = await self._p.sqlite_query(
259276
params.query,
260277
params.query_type,
261278
getattr(params, "params", None),
262279
)
280+
return _GeneratedSqliteQueryResult(
281+
columns=result.columns,
282+
rows=result.rows,
283+
rows_affected=result.rows_affected,
284+
last_insert_rowid=result.last_insert_rowid,
285+
)
263286

264287
async def sqlite_exists(self, params: Any) -> SessionFSSqliteExistsResult:
265288
if not isinstance(self._p, SessionFsSqliteProvider):
266289
return SessionFSSqliteExistsResult.from_dict({"exists": False})
267290
try:
268-
result = await self._p.sqlite_exists(params.session_id) # type: ignore[attr-defined]
291+
result = await self._p.sqlite_exists()
269292
return SessionFSSqliteExistsResult.from_dict({"exists": result})
270293
except Exception:
271294
return SessionFSSqliteExistsResult.from_dict({"exists": False})

python/e2e/test_session_fs_sqlite_e2e.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717
from copilot.generated.rpc import (
1818
SessionFSReaddirWithTypesEntry,
1919
SessionFSReaddirWithTypesEntryType,
20-
SessionFSSqliteQueryResult,
2120
SessionFSSqliteQueryType,
2221
)
2322
from copilot.session import PermissionHandler
2423
from copilot.session_fs_provider import (
2524
SessionFsFileInfo,
2625
SessionFsProvider,
2726
SessionFsSqliteProvider,
27+
SessionFsSqliteQueryResult,
2828
)
2929

3030
from .testharness import DEFAULT_GITHUB_TOKEN, E2ETestContext
@@ -148,14 +148,12 @@ async def rename(self, src: str, dest: str) -> None:
148148

149149
async def sqlite_query(
150150
self,
151-
session_id: str,
152151
query: str,
153152
query_type: SessionFSSqliteQueryType,
154153
params: dict[str, float | str | None] | None = None,
155-
) -> SessionFSSqliteQueryResult:
154+
) -> SessionFsSqliteQueryResult:
156155
self._sqlite_calls.append(
157156
{
158-
"sessionId": session_id,
159157
"queryType": query_type.value,
160158
"query": query,
161159
}
@@ -164,30 +162,30 @@ async def sqlite_query(
164162
db = self._get_or_create_db()
165163
trimmed = query.strip()
166164
if not trimmed:
167-
return SessionFSSqliteQueryResult(columns=[], rows=[], rows_affected=0)
165+
return SessionFsSqliteQueryResult(columns=[], rows=[], rows_affected=0)
168166

169167
if query_type == SessionFSSqliteQueryType.EXEC:
170168
db.executescript(trimmed)
171169
db.commit()
172-
return SessionFSSqliteQueryResult(columns=[], rows=[], rows_affected=0)
170+
return SessionFsSqliteQueryResult(columns=[], rows=[], rows_affected=0)
173171

174172
if query_type == SessionFSSqliteQueryType.QUERY:
175173
cursor = db.execute(trimmed, params or {})
176174
columns = [desc[0] for desc in cursor.description] if cursor.description else []
177175
rows = [dict(zip(columns, row)) for row in cursor.fetchall()]
178-
return SessionFSSqliteQueryResult(columns=columns, rows=rows, rows_affected=0)
176+
return SessionFsSqliteQueryResult(columns=columns, rows=rows, rows_affected=0)
179177

180178
# run (INSERT/UPDATE/DELETE)
181179
cursor = db.execute(trimmed, params or {})
182180
db.commit()
183-
return SessionFSSqliteQueryResult(
181+
return SessionFsSqliteQueryResult(
184182
columns=[],
185183
rows=[],
186184
rows_affected=cursor.rowcount,
187185
last_insert_rowid=float(cursor.lastrowid) if cursor.lastrowid else None,
188186
)
189187

190-
async def sqlite_exists(self, session_id: str) -> bool:
188+
async def sqlite_exists(self) -> bool:
191189
return self._db is not None
192190

193191

rust/src/session_fs.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ use crate::generated::api_types::{
4848
SessionFsError, SessionFsErrorCode, SessionFsReaddirWithTypesEntry,
4949
SessionFsReaddirWithTypesEntryType, SessionFsSetProviderConventions, SessionFsStatResult,
5050
};
51-
pub use crate::generated::api_types::{SessionFsSqliteQueryResult, SessionFsSqliteQueryType};
51+
pub use crate::generated::api_types::SessionFsSqliteQueryType;
5252

5353
/// Optional capabilities declared by a session filesystem provider.
5454
#[non_exhaustive]
@@ -388,6 +388,9 @@ pub trait SessionFsProvider: Send + Sync + 'static {
388388

389389
/// Optional trait for providers that support SQLite operations.
390390
///
391+
/// Providers are already session-scoped (created per session by the factory),
392+
/// so these methods do not take a `session_id` parameter.
393+
///
391394
/// To opt in, implement this trait on your provider and override
392395
/// [`SessionFsProvider::sqlite`] to return `Some(self)`:
393396
///
@@ -407,14 +410,29 @@ pub trait SessionFsSqliteProvider: Send + Sync {
407410
/// Execute a SQLite query against the provider's per-session database.
408411
async fn sqlite_query(
409412
&self,
410-
session_id: &str,
411413
query: &str,
412414
query_type: SessionFsSqliteQueryType,
413415
params: Option<&HashMap<String, serde_json::Value>>,
414416
) -> Result<SessionFsSqliteQueryResult, FsError>;
415417

416-
/// Check whether the provider has a SQLite database for the session.
417-
async fn sqlite_exists(&self, session_id: &str) -> Result<bool, FsError>;
418+
/// Check whether the provider has a SQLite database for this session.
419+
async fn sqlite_exists(&self) -> Result<bool, FsError>;
420+
}
421+
422+
/// Result of a SQLite query execution via [`SessionFsSqliteProvider::sqlite_query`].
423+
///
424+
/// Same shape as the generated RPC type but without the `error` field,
425+
/// since providers signal errors by returning `Err`.
426+
#[derive(Debug, Clone, Default)]
427+
pub struct SessionFsSqliteQueryResult {
428+
/// Column names from the result set.
429+
pub columns: Vec<String>,
430+
/// For SELECT: array of row objects. For others: empty array.
431+
pub rows: Vec<HashMap<String, serde_json::Value>>,
432+
/// Number of rows affected (for INSERT/UPDATE/DELETE).
433+
pub rows_affected: i64,
434+
/// Last inserted row ID (for INSERT).
435+
pub last_insert_rowid: Option<f64>,
418436
}
419437

420438
#[cfg(test)]

0 commit comments

Comments
 (0)