Forge
pythonf8555a15
1"""SQLite-like SQL facade over witdb (bridge or native, ADR-0004)."""
2
3from __future__ import annotations
4
5from collections.abc import Sequence
6from typing import Any, Protocol
7
8from pywitdb._bridge.client import BridgeClient, bridge_available
9from pywitdb.exceptions import BridgeNotAvailableError, WitDbStoreError
10
11Backend = str
12
13
14class Row:
15 """Mapping row keyed by column name (sqlite3.Row subset)."""
16
17 __slots__ = ("_columns", "_values")
18
19 def __init__(self, columns: Sequence[str], values: Sequence[Any]) -> None:
20 self._columns = tuple(columns)
21 self._values = tuple(values)
22
23 def __getitem__(self, key: int | str) -> Any:
24 if isinstance(key, int):
25 return self._values[key]
26 try:
27 index = self._columns.index(key)
28 except ValueError as exc:
29 raise KeyError(key) from exc
30 return self._values[index]
31
32 def keys(self) -> tuple[str, ...]:
33 return self._columns
34
35
36class _SqlConnection(Protocol):
37 _last_insert_rowid: int
38 _last_rows_affected: int
39
40 def close(self) -> None: ...
41 def last_insert_rowid(self) -> int: ...
42
43
44class Cursor:
45 def __init__(
46 self,
47 conn: _SqlConnection,
48 *,
49 columns: Sequence[str],
50 rows: Sequence[Sequence[Any]],
51 ) -> None:
52 self._conn = conn
53 self._columns = list(columns)
54 self._rows = [tuple(row) for row in rows]
55 self._index = 0
56 self.rowcount = conn._last_rows_affected
57
58 def fetchone(self) -> Row | None:
59 if self._index >= len(self._rows):
60 return None
61 row = Row(self._columns, self._rows[self._index])
62 self._index += 1
63 return row
64
65 def fetchall(self) -> list[Row]:
66 out: list[Row] = []
67 while True:
68 row = self.fetchone()
69 if row is None:
70 break
71 out.append(row)
72 return out
73
74 @property
75 def lastrowid(self) -> int:
76 return self._conn._last_insert_rowid
77
78
79class BridgeConnection:
80 def __init__(self, client: BridgeClient, path: str) -> None:
81 self._client = client
82 self._path = path
83 self._closed = False
84 self._last_insert_rowid = 0
85 self._last_rows_affected = 0
86 self.row_factory: type[Row] | None = Row
87
88 def execute(self, sql: str, params: Sequence[Any] | None = None) -> Cursor:
89 sql_stripped = sql.strip().upper()
90 if sql_stripped.startswith("SELECT") or sql_stripped.startswith("WITH"):
91 return self._query(sql, params)
92 return self._exec(sql, params)
93
94 def executescript(self, sql: str) -> None:
95 self._exec(sql, None)
96
97 def commit(self) -> None:
98 self._client.ensure_ok(self._client.request("sql_commit"))
99
100 def rollback(self) -> None:
101 self._client.ensure_ok(self._client.request("sql_rollback"))
102
103 def close(self) -> None:
104 if self._closed:
105 return
106 self._closed = True
107 self._client.close()
108
109 def last_insert_rowid(self) -> int:
110 return self._last_insert_rowid
111
112 def _exec(self, sql: str, params: Sequence[Any] | None) -> Cursor:
113 payload: dict[str, Any] = {"sql": sql}
114 if params:
115 payload["params"] = list(params)
116 resp = self._client.ensure_ok(self._client.request("sql_exec", **payload))
117 self._last_rows_affected = int(resp.get("rows_affected") or 0)
118 if "last_insert_rowid" in resp:
119 self._last_insert_rowid = int(resp["last_insert_rowid"])
120 return Cursor(self, columns=(), rows=())
121
122 def _query(self, sql: str, params: Sequence[Any] | None) -> Cursor:
123 payload: dict[str, Any] = {"sql": sql}
124 if params:
125 payload["params"] = list(params)
126 resp = self._client.ensure_ok(self._client.request("sql_query", **payload))
127 columns = resp.get("columns") or []
128 rows = resp.get("rows") or []
129 return Cursor(self, columns=columns, rows=rows)
130
131 def __enter__(self) -> BridgeConnection:
132 return self
133
134 def __exit__(self, *exc: object) -> None:
135 self.close()
136
137
138def connect(
139 path: str,
140 *,
141 password: str | None = None,
142 backend: Backend = "auto",
143 create: bool = True,
144) -> BridgeConnection | Any:
145 """Open a `.witdb` file and attach the SQL engine (native or bridge)."""
146 if backend in ("native", "auto"):
147 try:
148 from pywitdb._native.sql import open_native_sql, sql_native_available
149
150 if sql_native_available():
151 return open_native_sql(path, password=password, create=create)
152 except WitDbStoreError:
153 if backend == "native":
154 raise
155
156 if backend == "native":
157 raise WitDbStoreError(
158 "native SQL unavailable; publish witdb.dll with witdb_sql_* exports"
159 )
160
161 if not bridge_available():
162 raise BridgeNotAvailableError(
163 "witdb-bridge not found; build bridge/WitDbBridge or set WITDB_BRIDGE_PATH"
164 )
165
166 client = BridgeClient.start()
167 try:
168 client.open(path, password=password, create=create)
169 except Exception:
170 client.close()
171 raise
172 return BridgeConnection(client, path)
173
174
175# Back-compat alias
176Connection = BridgeConnection
177
View only · write via MCP/CIDE