sql5 4.1.0

SQLite compatible database with CJK FTS5 full-text search and vector similarity
Documentation
# v3.7 - SQLite 相容性提升

## 日期:2026-05-10

## 修改概述

本版本致力於提升 sql5 與原生 sqlite3 API 的相容性,確保兩者返回相同的資料結構。

---

## 已完成的修改

### 1. ResultSet 結構擴充(executor.rs:51-100)

```rust
pub struct ResultSet {
    pub columns: Vec<String>,     // 欄位名稱列表
    pub rows:    Vec<Vec<Value>>, // 查詢結果列
    pub affected: i64,            // 受影響的行數(INSERT/UPDATE/DELETE)
    pub lastrowid: Option<i64>,   // 最後插入行的 ID(AUTOINCREMENT)
}

impl ResultSet {
    /// DML 結果集(無回傳資料,但有受影響行數和 lastrowid)
    pub fn dml(count: i64, lastrowid: Option<i64>) -> Self
}
```

### 2. DML 函式修改(executor.rs)

- `exec_insert()`: 使用 `ResultSet::dml(count, last_autoinc)`
- `exec_update()`: 使用 `ResultSet::dml(count as i64, None)`
- `exec_delete()`: 使用 `ResultSet::dml(count as i64, None)`

### 3. Server JSON 輸出(server.rs:185-209)

```rust
let lastrowid = rs.lastrowid.map(|n| serde_json::Value::Number(n.into()));
let json = serde_json::json!({
    "ok": true,
    "columns": columns,
    "rows": rows,
    "affected": rs.affected,
    "lastrowid": lastrowid,
});
```

### 4. Python Cursor 類別(client.py:43-76)

```python
class Cursor:
    def __init__(self, data: dict):
        self.columns = data.get("columns", [])
        self.rows = data.get("rows", [])
        self.affected = data.get("affected", 0)
        self.lastrowid = data.get("lastrowid")

        # DB-API 2.0 compatibility
        if self.columns:
            self.description = tuple((name, None, None, None, None, None, None) for name in self.columns)
            self.rowcount = -1
        else:
            self.description = None
            self.rowcount = self.affected if self.affected > 0 else -1
```

### 5. Python subprocess 通訊修正(client.py:143-156)

改用 binary mode 避免 text mode 換行符號問題。

### 6. FTS5 格式修正(server.rs:304-331)

FTS5 查詢結果格式與 sqlite3 一致(不自動包含 rowid, score):

```rust
// 輸出欄位:與原始 FTS 表格一致(與 sqlite 相同)
// rowid 和 score 僅在明確 SELECT rowid, score 時包含
let rows: Vec<Vec<serde_json::Value>> = results.into_iter().map(|(_, _, vals)| {
    vals.into_iter().map(|v| serde_json::Value::String(v)).collect()
}).collect();
```

### 7. :memory: 路徑修正(main.rs, client.py)

`:memory:` 視為記憶體模式,不建立磁碟檔案:

- `main.rs`: 檢查 path 是否為 `:memory:`,是則使用記憶體模式
- `client.py`: 檢查 self.path 是否為 `:memory:`,是則不傳遞路徑

---

## API 相容性對照表

| 項目 | sqlite3 | sql5(修正前) | sql5(修正後) | 狀態 |
|------|---------|---------------|---------------|------|
| INSERT 返回 columns | `None` | `["result"]` | `[]` | ✅ |
| INSERT 返回 rows | `[]` | `[["N row(s) inserted"]]` | `[]` | ✅ |
| INSERT 返回 affected | `N` | `0` | `N` | ✅ |
| INSERT 返回 lastrowid | `id` | 不支援 | `id` | ✅ |
| UPDATE 返回 columns | `None` | `["result"]` | `[]` | ✅ |
| UPDATE 返回 rows | `[]` | `[["N row(s) updated"]]` | `[]` | ✅ |
| UPDATE 返回 affected | `N` | `0` | `N` | ✅ |
| DELETE 返回 columns | `None` | `["result"]` | `[]` | ✅ |
| DELETE 返回 rows | `[]` | `[["N row(s) deleted"]]` | `[]` | ✅ |
| DELETE 返回 affected | `N` | `0` | `N` | ✅ |
| SELECT cursor.description | 有 | 無 | 有 | ✅ |
| SELECT cursor.rowcount | `-1` | 無 | `-1` | ✅ |
| SELECT rows 類型 | `list` | `list` | `list` | ✅ |

---

## 測試結果

### 比較測試(test_compare_sqlite.py)

```
tests/test_compare_sqlite.py::TestCursorDescription ... PASSED
tests/test_compare_sqlite.py::TestRowTypes ... PASSED
tests/test_compare_sqlite.py::TestRowCount ... PASSED
tests/test_compare_sqlite.py::TestLastRowId ... PASSED
tests/test_compare_sqlite.py::TestAffectedRows ... PASSED
tests/test_compare_sqlite.py::TestNullHandling ... PASSED
tests/test_compare_sqlite.py::TestDataTypes ... PASSED
tests/test_compare_sqlite.py::TestEmptyResults ... PASSED
tests/test_compare_sqlite.py::TestCursorAttributes ... PASSED
tests/test_compare_sqlite.py::TestIterateCursor ... PASSED
tests/test_compare_sqlite.py::TestFTS5Format ... PASSED

============================== 22 passed ==============================
```

### 完整測試(test.sh)

```
[PASS] Rust unit tests (cargo test)      — 353 passed
[PASS] CLI integration tests (rutest.sh) — 113 passed
[PASS] Python pytest tests               — 26 passed, 5 skipped
[PASS] Python client test (subprocess)   — PASSED
[PASS] WebSocket test (v3.0)             — PASSED
```

---

## 已知限制

### 1. FTS5 全文檢索格式

FTS5 是 sql5 特有的功能,其返回格式與 sqlite 不同:
- sql5 FTS 返回:`rowid`, `score`, 原始欄位
- sqlite3 FTS 返回:原始欄位

這是預期行為,不需要與 sqlite 一致。

### 2. 多列 INSERT

sql5 不支援 `INSERT INTO t VALUES (1), (2), (3)` 語法。需分開執行多次 INSERT。

### 3. 記憶體模式穩定性

`:memory:` 路徑在某些環境下可能出現問題。建議使用不帶路徑的 `connect()`。

---

## 待優化項目(未來版本)

1. **支援多列 INSERT** - `INSERT INTO t VALUES (...), (...)`
2. **修復 :memory: 模式穩定性**
3. **添加 JOIN 支援**(現已 skip)
4. **添加更多聚合函式測試**

---

## 參考資源

- [Python DB-API 2.0 規格]https://www.python.org/dev/peps/pep-0249/
- [sqlite3 Python 模組]https://docs.python.org/3/library/sqlite3.html

---

## 修訂歷史

- 2026-05-10:v3.7 初始版本
  - ResultSet 新增 affected, lastrowid 欄位
  - DML 函式改用 ResultSet::dml()
  - server.rs JSON 回應包含 affected/lastrowid
  - Python Cursor 新增 description, rowcount, lastrowid
  - 20/20 比較測試通過