Forge
csharpf8555a15
1using System.Security.Cryptography;
2using System.Text.Json;
3using System.Text.Json.Nodes;
4using OutWit.Database.Core.BouncyCastle;
5using OutWit.Database.Core.Builder;
6using OutWit.Database.Core.Exceptions;
7using OutWit.Database.Core.Interfaces;
8using OutWit.Database.Core.Providers;
9using OutWit.Database.Engine;
10
11namespace WitDbBridge;
12
13internal sealed class BridgeSession : IDisposable
14{
15 private WitDatabase? m_db;
16 private WitSqlEngine? m_engine;
17 private ITransaction? m_txn;
18 private bool m_disposed;
19
20 public void Dispose()
21 {
22 if (m_disposed)
23 {
24 return;
25 }
26
27 m_disposed = true;
28 m_txn?.Dispose();
29 m_txn = null;
30 m_engine?.Dispose();
31 m_engine = null;
32 m_db?.Dispose();
33 m_db = null;
34 }
35
36 public JsonDocument Handle(JsonDocument request)
37 {
38 if (!request.RootElement.TryGetProperty("id", out var idEl) || idEl.ValueKind != JsonValueKind.Number)
39 {
40 return Error(0, "invalid_request", "missing id");
41 }
42
43 var id = idEl.GetInt32();
44 if (!request.RootElement.TryGetProperty("op", out var opEl) || opEl.ValueKind != JsonValueKind.String)
45 {
46 return Error(id, "invalid_request", "missing op");
47 }
48
49 try
50 {
51 return opEl.GetString() switch
52 {
53 "open" => Open(id, request.RootElement),
54 "close" => Close(id),
55 "get" => Get(id, request.RootElement),
56 "put" => Put(id, request.RootElement),
57 "delete" => Delete(id, request.RootElement),
58 "txn_begin" => TxnBegin(id),
59 "txn_commit" => TxnCommit(id),
60 "txn_rollback" => TxnRollback(id),
61 "sql_exec" => SqlExec(id, request.RootElement),
62 "sql_query" => SqlQuery(id, request.RootElement),
63 "sql_commit" => SqlCommit(id),
64 "sql_rollback" => SqlRollback(id),
65 _ => Error(id, "invalid_request", $"unknown op '{opEl.GetString()}'"),
66 };
67 }
68 catch (Exception ex)
69 {
70 return MapException(id, ex);
71 }
72 }
73
74 private JsonDocument Open(int id, JsonElement root)
75 {
76 EnsureNoDb();
77 BouncyCastleProviderRegistration.EnsureRegistered();
78
79 if (!root.TryGetProperty("path", out var pathEl) || pathEl.ValueKind != JsonValueKind.String)
80 {
81 return Error(id, "invalid_request", "path is required");
82 }
83
84 var path = pathEl.GetString();
85 if (string.IsNullOrWhiteSpace(path))
86 {
87 return Error(id, "invalid_request", "path is required");
88 }
89
90 string? password = null;
91 if (root.TryGetProperty("password", out var pwEl) && pwEl.ValueKind == JsonValueKind.String)
92 {
93 password = pwEl.GetString();
94 }
95
96 var create = !root.TryGetProperty("create", out var createEl) || createEl.ValueKind != JsonValueKind.False;
97
98 m_db = create
99 ? password is null ? WitDatabase.CreateOrOpen(path) : WitDatabase.CreateOrOpen(path, password)
100 : password is null ? WitDatabase.Open(path) : WitDatabase.Open(path, password);
101
102 var detection = StorageDetector.Detect(path);
103 var store = new JsonObject
104 {
105 ["path"] = path,
106 ["store_provider"] = detection.StoreType ?? "btree",
107 ["encryption_provider"] = detection.EncryptionProvider ?? "",
108 ["features"] = FeaturesArray(detection),
109 };
110
111 return Ok(id, new JsonObject { ["store"] = store });
112 }
113
114 private JsonDocument Close(int id)
115 {
116 m_txn?.Dispose();
117 m_txn = null;
118 m_engine?.Dispose();
119 m_engine = null;
120 m_db?.Dispose();
121 m_db = null;
122 return Ok(id);
123 }
124
125 private JsonDocument Get(int id, JsonElement root)
126 {
127 var key = RequireKey(root);
128 var value = m_txn is not null ? m_txn.Get(key) : RequireDb().Get(key);
129 if (value is null)
130 {
131 return Ok(id, new JsonObject { ["found"] = false });
132 }
133
134 return Ok(id, new JsonObject
135 {
136 ["found"] = true,
137 ["value"] = Convert.ToBase64String(value),
138 });
139 }
140
141 private JsonDocument Put(int id, JsonElement root)
142 {
143 var key = RequireKey(root);
144 var value = RequireValue(root);
145 if (m_txn is not null)
146 {
147 m_txn.Put(key, value);
148 }
149 else
150 {
151 RequireDb().Put(key, value);
152 }
153
154 return Ok(id);
155 }
156
157 private JsonDocument Delete(int id, JsonElement root)
158 {
159 var key = RequireKey(root);
160 var deleted = m_txn is not null ? m_txn.Delete(key) : RequireDb().Delete(key);
161 return Ok(id, new JsonObject { ["deleted"] = deleted });
162 }
163
164 private JsonDocument TxnBegin(int id)
165 {
166 var db = RequireDb();
167 if (m_txn is not null)
168 {
169 return Error(id, "txn_active", "transaction already active");
170 }
171
172 if (!db.SupportsTransactions)
173 {
174 return Error(id, "txn_not_supported", "transactions are not enabled");
175 }
176
177 m_txn = db.BeginTransaction();
178 return Ok(id);
179 }
180
181 private JsonDocument TxnCommit(int id)
182 {
183 var txn = RequireTxn();
184 txn.Commit();
185 m_txn = null;
186 return Ok(id);
187 }
188
189 private JsonDocument TxnRollback(int id)
190 {
191 var txn = RequireTxn();
192 txn.Rollback();
193 m_txn = null;
194 return Ok(id);
195 }
196
197 private JsonDocument SqlExec(int id, JsonElement root)
198 {
199 var sql = RequireSql(root);
200 JsonElement? paramsEl = root.TryGetProperty("params", out var p) ? p : null;
201 var (boundSql, parameters) = SqlBridgeHelpers.BindSql(sql, paramsEl);
202 var engine = RequireEngine();
203
204 using var result = engine.Execute(boundSql, parameters);
205 return Ok(id, new JsonObject
206 {
207 ["rows_affected"] = result.RowsAffected,
208 ["last_insert_rowid"] = engine.LastInsertRowId,
209 });
210 }
211
212 private JsonDocument SqlQuery(int id, JsonElement root)
213 {
214 var sql = RequireSql(root);
215 JsonElement? paramsEl = root.TryGetProperty("params", out var p) ? p : null;
216 var (boundSql, parameters) = SqlBridgeHelpers.BindSql(sql, paramsEl);
217 var engine = RequireEngine();
218
219 using var result = engine.Execute(boundSql, parameters);
220 if (!result.HasRows)
221 {
222 return Ok(id, new JsonObject
223 {
224 ["columns"] = new JsonArray(),
225 ["rows"] = new JsonArray(),
226 });
227 }
228
229 var columns = result.Columns.Select(c => c.Name).ToList();
230 var rows = result.ReadAll();
231 return Ok(id, new JsonObject
232 {
233 ["columns"] = new JsonArray(columns.Select(c => JsonValue.Create(c)).ToArray()),
234 ["rows"] = SqlBridgeHelpers.RowsToJson(rows, columns),
235 });
236 }
237
238 private JsonDocument SqlCommit(int id)
239 {
240 var engine = RequireEngine();
241 engine.Commit();
242 RequireDb().Flush();
243 return Ok(id);
244 }
245
246 private JsonDocument SqlRollback(int id)
247 {
248 RequireEngine().Rollback();
249 return Ok(id);
250 }
251
252 private WitSqlEngine RequireEngine()
253 {
254 if (m_engine is null)
255 {
256 m_engine = new WitSqlEngine(RequireDb(), ownsStore: false);
257 }
258
259 return m_engine;
260 }
261
262 private static string RequireSql(JsonElement root)
263 {
264 if (!root.TryGetProperty("sql", out var sqlEl) || sqlEl.ValueKind != JsonValueKind.String)
265 {
266 throw new ArgumentException("sql is required");
267 }
268
269 var sql = sqlEl.GetString();
270 if (string.IsNullOrWhiteSpace(sql))
271 {
272 throw new ArgumentException("sql is required");
273 }
274
275 return sql;
276 }
277
278 private WitDatabase RequireDb()
279 {
280 if (m_db is null)
281 {
282 throw new InvalidOperationException("database is not open");
283 }
284
285 return m_db;
286 }
287
288 private ITransaction RequireTxn()
289 {
290 if (m_txn is null)
291 {
292 throw new InvalidOperationException("no active transaction");
293 }
294
295 return m_txn;
296 }
297
298 private void EnsureNoDb()
299 {
300 if (m_db is not null)
301 {
302 throw new InvalidOperationException("database already open in this bridge process");
303 }
304 }
305
306 private static byte[] RequireKey(JsonElement root)
307 {
308 if (!root.TryGetProperty("key", out var keyEl) || keyEl.ValueKind != JsonValueKind.String)
309 {
310 throw new ArgumentException("key is required");
311 }
312
313 return Convert.FromBase64String(keyEl.GetString()!);
314 }
315
316 private static byte[] RequireValue(JsonElement root)
317 {
318 if (!root.TryGetProperty("value", out var valEl) || valEl.ValueKind != JsonValueKind.String)
319 {
320 throw new ArgumentException("value is required");
321 }
322
323 return Convert.FromBase64String(valEl.GetString()!);
324 }
325
326 private static JsonArray FeaturesArray(StorageDetectionResult detection)
327 {
328 var features = new JsonArray();
329 if (detection.RequiresPassword || !string.IsNullOrEmpty(detection.EncryptionProvider))
330 {
331 features.Add("encryption");
332 }
333
334 if (detection.HasTransactions)
335 {
336 features.Add("transactions");
337 }
338
339 if (detection.HasFileLocking)
340 {
341 features.Add("file_locking");
342 }
343
344 if (detection.HasMvcc)
345 {
346 features.Add("mvcc");
347 }
348
349 return features;
350 }
351
352 private static JsonDocument Ok(int id, JsonObject? extra = null)
353 {
354 var obj = new JsonObject { ["id"] = id, ["ok"] = true };
355 if (extra is not null)
356 {
357 foreach (var prop in extra)
358 {
359 obj[prop.Key] = JsonNode.Parse(prop.Value?.ToJsonString() ?? "null");
360 }
361 }
362
363 return JsonDocument.Parse(obj.ToJsonString());
364 }
365
366 private static JsonDocument Error(int id, string code, string message)
367 {
368 var obj = new JsonObject
369 {
370 ["id"] = id,
371 ["ok"] = false,
372 ["error"] = new JsonObject
373 {
374 ["code"] = code,
375 ["message"] = message,
376 },
377 };
378 return JsonDocument.Parse(obj.ToJsonString());
379 }
380
381 private static JsonDocument MapException(int id, Exception ex)
382 {
383 if (ex is InvalidOperationException && ex.Message.Contains("not open", StringComparison.OrdinalIgnoreCase))
384 {
385 return Error(id, "not_open", ex.Message);
386 }
387
388 var code = ex switch
389 {
390 FileNotFoundException => "file_not_found",
391 InvalidDataException => "password_required",
392 ConfigurationMismatchException => "configuration_mismatch",
393 ProviderNotFoundException => "unknown_provider",
394 CryptographicException => "wrong_password",
395 ArgumentException => "invalid_request",
396 InvalidOperationException when ex.Message.Contains("SQL", StringComparison.OrdinalIgnoreCase)
397 || ex.Message.Contains("parse", StringComparison.OrdinalIgnoreCase)
398 || ex.Message.Contains("syntax", StringComparison.OrdinalIgnoreCase) => "sql_error",
399 InvalidOperationException => "sql_error",
400 _ => "store_error",
401 };
402
403 return Error(id, code, ex.Message);
404 }
405}
406
View only · write via MCP/CIDE