Skip to content

Commit c47cccc

Browse files
committed
feat(redis): 支援依索引安全刪除 list 元素
Redis 沒有依索引刪除 list 元素的單一命令,直接使用 LREM 會在重複值時刪錯位置。 刪除前比對載入時的索引和值,再於 WATCH/MULTI/EXEC 交易中以唯一標記定位。補上重複值、外部修改、索引消失與交易中止測試。
1 parent 25cd01d commit c47cccc

9 files changed

Lines changed: 105 additions & 10 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### 🚀 新增功能
66

7+
- **Redis list 元素安全刪除**:key 編輯器可依選取索引刪除 list 元素;刪除前會比對載入時的值,並在同一筆 WATCH/MULTI/EXEC 交易中以唯一標記定位,內容相同的其他元素不會被誤刪,外部修改或交易中止時也會保留原清單。
78
- **資料表約束與 SQL Server XML 索引**:Table Designer 新增外鍵與 CHECK 約束分頁,可讀取並預覽 MySQL、PostgreSQL、SQL Server、Oracle、SQLite 的既有約束;新增或修改時會產生各 provider 對應的 DDL。SQL Server 索引頁也可建立 PRIMARY/PATH/VALUE/PROPERTY XML 索引,並正確保留次要 XML 索引的父索引關係。
89
- **跨資料庫欄位屬性**:Table Designer 會讀取並呈現 MySQL/MariaDB 的字元集、排序規則與 generated column,PostgreSQL 的 identity、generated、storage、compression 與 collation,SQLite 的 generated column/collation,Oracle 與 Snowflake 的 identity、虛擬欄位及 collation。SQL 預覽會保留可安全重建的屬性;既有資料表不支援直接 ALTER 的進階變更會明確阻擋,避免靜默遺失設定。
910
- **Linux/macOS 連線 URI 與完整 TLS 模式**:新增/編輯連線時可貼上 MySQL/MariaDB、PostgreSQL、SQL Server 或 SQLite URI,自動套用 provider、主機、port、認證、資料庫、逾時及 provider 原生的 Prefer/Require/VerifyCA/VerifyFull/Mandatory/Strict 等 TLS 語意;舊 `useSsl` 會依 provider 遷移成等價模式。URI 輸入會遮蔽並在成功後立即清除,未知/重複參數、未編碼空白、dot-segment、Unicode 控制/noncharacter、過長內容與不相容模式一律 fail closed,錯誤訊息不回顯密碼,`connections.json` 仍不保存密碼。

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ SQLite / PostgreSQL / SQL Server database rename 實機矩陣(需先啟動 Doc
238238
| SQL Server | 可用 | 支援 metadata、資料瀏覽、資料編輯、DDL、Dump、Table Designer;`dbo` 以外的 schema 會以 `schema.table` 顯示並可用於主要資料表操作。 |
239239
| Oracle | 部分可用 | 支援 schema/table/view metadata、資料瀏覽、資料編輯、DDL、Dump、Table Designer;部分 DDL 仍受權限、語法與物件型態限制。 |
240240
| MongoDB | 第三期可用 | 支援一般與 SRV 連線、URI 匯入、database/collection/view metadata、抽樣 schema 推斷、索引與統計資訊、分頁文件瀏覽與 JSON find 查詢;文件檢視器提供可展開文件樹、單一文件安全編輯(`_id` 鎖定+樂觀並行比對)、文件新增與安全刪除。standalone 4.4/7.0/8.0 已通過實機矩陣;Atlas/SRV 驗證環境與 Aggregation Pipeline 仍在後續階段。 |
241-
| Redis / Microsoft Garnet | 第三期可用 | 支援 `redis://`/`rediss://`、ACL/密碼驗證、logical db、TLS 直連、key 型別/TTL/摘要瀏覽與 pattern/type/單一 key 受限查詢;key 編輯器依型別切換,string 值、hash 欄位、list 元素(含尾端新增)、set 成員與 zset 分數都以 WATCH/MULTI/EXEC 並行保護寫入,另有 TTL 設定/移除與刪除 key。Redis 6.2、Redis 7 與 Garnet standalone 各 39 項實機矩陣通過;Cluster/Sentinel、list 元素刪除、Pub/Sub 與監控仍待補。 |
241+
| Redis / Microsoft Garnet | 第三期可用 | 支援 `redis://`/`rediss://`、ACL/密碼驗證、logical db、TLS 直連、key 型別/TTL/摘要瀏覽與 pattern/type/單一 key 受限查詢;key 編輯器依型別切換,string 值、hash 欄位、list 元素(含依索引安全刪除與尾端新增)、set 成員與 zset 分數都以 WATCH/MULTI/EXEC 並行保護寫入,另有 TTL 設定/移除與刪除 key。Redis 6.2、Redis 7 與 Garnet standalone 既有各 39 項實機矩陣通過,新增的 list 刪除案例待重跑;Cluster/Sentinel、Pub/Sub 與監控仍待補。 |
242242
| Snowflake | 第二期可用 | 以 SQL REST API v2 直連(Programmatic Access Token 或 OAuth token),支援 SHOW DATABASES、INFORMATION_SCHEMA metadata、schema.table/view 瀏覽、欄位與列數、分頁資料檢視、SELECT/SHOW,以及查詢編輯器的單一 DML/DDL;URI 匯入與設定保存沿用既有流程。所有值以字串呈現;真實帳戶實機驗收、key-pair JWT、參數綁定、資料網格寫回與 bulk load 仍待補。 |
243243
| SQL 查詢 | 可用 | 支援 SELECT/SHOW/EXPLAIN/DESC/WITH 類結果顯示、多格式匯出、語法格式化、查詢歷史;MySQL/MariaDB、PostgreSQL、SQL Server、Oracle、SQLite 都可產生唯讀原生執行計畫,以樹狀、原始資料、文字與可用成本統計檢視。 |
244244
| SQL 編輯輔助 | 可用 | 自動完成會解析目前 statement 的資料表與 alias,提供欄位、`alias.column`、資料表、關鍵字與片段捷徑;metadata 依 provider/database 快取。`Ctrl+Shift+P` 可搜尋、插入、新增、刪除自訂 SQL 片段,並以 JSON 匯入/匯出。 |
@@ -278,7 +278,7 @@ SQLite / PostgreSQL / SQL Server database rename 實機矩陣(需先啟動 Doc
278278
- 資料分析預設使用前 10,000 筆樣本;切換成全表會執行 COUNT/DISTINCT/GROUP BY,對大型資料表可能耗時。BLOB、JSON、geometry 等型別會略過資料庫不支援的極值或分佈統計。
279279
- ER 圖與結構差異報告目前都只讀取 metadata,不會回寫資料庫;ER 圖每張卡片先顯示前 16 個欄位,還沒有拖曳位置保存、關聯篩選或從模型回寫資料庫,結構差異也還不會產生同步 DDL。
280280
- MongoDB 查詢分頁維持唯讀,寫入只能走文件檢視器的安全編輯、新增與刪除;schema 由前 100 筆文件推斷。尚未提供 Aggregation Pipeline 視覺設計、SSH Tunnel 或 Atlas 專用驗證;standalone 4.4/7.0/8.0 實機矩陣已通過,Atlas/SRV 與帳號驗證環境仍待驗收。
281-
- Redis / Garnet 目前只支援 standalone 直連;寫入僅限 key 編輯器(string 值、hash 欄位、list 元素編輯與尾端新增、set 成員、zset 分數、TTL 與刪除 key),list 元素刪除因 Redis 無依索引刪除命令暫不提供,值含非 UTF-8 位元組時不開放編輯。SCAN 的 `COUNT` 只是提示,程式會在單次 traversal 內去重,但資料同時變更時跨頁仍可能變動。Cluster、Sentinel、Pub/Sub 與監控尚未提供;Redis 6.2/7 與 Garnet standalone 實機矩陣可用 `tests/Run-RedisLiveMatrixTests.ps1` 重跑。
281+
- Redis / Garnet 目前只支援 standalone 直連;寫入僅限 key 編輯器(string 值、hash 欄位、list 元素編輯/刪除與尾端新增、set 成員、zset 分數、TTL 與刪除 key),值含非 UTF-8 位元組時不開放編輯。SCAN 的 `COUNT` 只是提示,程式會在單次 traversal 內去重,但資料同時變更時跨頁仍可能變動。Cluster、Sentinel、Pub/Sub 與監控尚未提供;Redis 6.2/7 與 Garnet standalone 實機矩陣可用 `tests/Run-RedisLiveMatrixTests.ps1` 重跑。
282282
- Snowflake 結果集入口僅接受 SELECT/SHOW/DESC/EXPLAIN/WITH,DML/DDL 必須從查詢編輯器執行;以 SQL API JSON 讀取時所有欄位值以字串顯示,物件名稱採 schema.table 拆解(名稱內含點號者不支援)。查詢會使用連線設定的 database/warehouse/role;尚未對真實 Snowflake 帳戶實機驗收,key-pair JWT、參數綁定、資料網格寫回、暫存區與 bulk load 未提供。
283283

284284
各功能做完的細節紀錄在 [`docs/FEATURE_NOTES.md`](docs/FEATURE_NOTES.md)。

‎docs/FEATURE_NOTES.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,13 @@
6464
- 刪除會在 WATCH 後比對畫面載入時的分數,分數改變、成員消失或 EXEC 被中止都回報衝突;分數以數值比較,`7` 與 `7.0` 視為相同。
6565
- 回歸案例涵蓋過期分數、等值文字、成員消失及交易中止;Redis/Garnet 實機矩陣另加入外部改分數後拒絕刪除的案例。
6666

67+
- **Redis list 元素安全刪除**
68+
- 刪除前以 WATCH 鎖定 key 並比對畫面載入時的索引和值;交易中先把該索引換成兩組 GUID 組成的唯一標記,再只移除一次標記,因此 list 內有重複值也不會刪到別的位置。
69+
- 索引消失、值被外部修改或 EXEC 中止都會回報衝突且保留清單;loopback 測試涵蓋重複值、過期值、越界索引與交易中止,Redis/Garnet 實機矩陣也會驗證重複值及外部修改。
70+
6771
- **Redis/Microsoft Garnet provider 🟡 第三期完成(集合型別編輯)**
68-
- key 編輯器依型別切換:hash/list/set/zset 以項目網格+輸入列操作——hash 欄位新增/更新/刪除、list 既有元素編輯與尾端新增(RPUSH)、set 成員新增/移除、zset 成員分數新增/更新/移除;TTL 與刪除 key 對所有型別可用。
69-
- 並行安全:所有集合寫入共用 WATCH+MULTI/EXEC 交易——型別被改變、項目被其他連線建立/刪除/改值、或 EXEC 落空都會回報衝突且不寫入;zset 分數以數值比較避免字串表示差異誤判。list 因 Redis 無「依索引刪除」命令,元素刪除留待後續。
72+
- key 編輯器依型別切換:hash/list/set/zset 以項目網格+輸入列操作——hash 欄位新增/更新/刪除、list 既有元素編輯/刪除與尾端新增(RPUSH)、set 成員新增/移除、zset 成員分數新增/更新/移除;TTL 與刪除 key 對所有型別可用。
73+
- 並行安全:所有集合寫入共用 WATCH+MULTI/EXEC 交易——型別被改變、項目被其他連線建立/刪除/改值、或 EXEC 落空都會回報衝突且不寫入;zset 分數以數值比較避免字串表示差異誤判,list 刪除則以交易內唯一標記鎖定索引,避免重複值誤刪。
7074
- 實機矩陣(2026-08-27):Redis 6.2、Redis 7、Garnet 各通過 39 項檢查(原 26 項+13 項集合寫入:欄位/元素/成員的更新、新增、刪除、stale 衝突不覆蓋與型別衝突)。
7175

7276
- **Redis/Microsoft Garnet provider 🟡 第二期完成(string 安全編輯與實機矩陣)**

‎docs/ROADMAP.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
| MongoDB Aggregation Pipeline 視覺設計 | 📋 | MongoDB provider 後續階段:拖放 stage、逐步預覽與結果驗證。 |
3131
| 專注模式 | ✅ | F11/檢視選單可隱藏工具列、導覽與資訊窗格。 |
3232
| Snowflake | 🟡 | 第二期完成:SQL REST API 直連(PAT/OAuth token)、SHOW DATABASES 與 INFORMATION_SCHEMA metadata、schema.table 瀏覽、分頁資料檢視、SELECT/SHOW,以及查詢編輯器單一 DML/DDL;待補實機驗收、key-pair JWT、參數綁定、資料網格寫回、模型與 BI 能力。 |
33-
| Redis standalone/Cluster/Sentinel、Microsoft Garnet | 🟡 | RESP2 standalone 第三期完成:瀏覽、受限查詢,加上 key 編輯器的 string 與 hash/list/set/zset 安全編輯(WATCH/MULTI/EXEC)、TTL 與刪除;Redis 6.2/7 與 Garnet 各 39 項實機矩陣通過。待補 Cluster、Sentinel、list 元素刪除、監控與 Pub/Sub。 |
33+
| Redis standalone/Cluster/Sentinel、Microsoft Garnet | 🟡 | RESP2 standalone 第三期完成:瀏覽、受限查詢,加上 key 編輯器的 string 與 hash/list/set/zset 安全編輯(WATCH/MULTI/EXEC)、TTL 與刪除;list 可依索引安全刪除且不會誤刪重複值。待補 Cluster、Sentinel、監控與 Pub/Sub。 |
3434
| Linux ARM | ✅ | 已建立 .NET 8 Core 與 Avalonia 桌面預覽版;CI/Release 會在 `ubuntu-24.04` x64 與 `ubuntu-24.04-arm` ARM64 原生 runner 分別建立 self-contained 安裝壓縮檔,並完成安裝、Xvfb UI 啟動、安全更新、rollback 與解除安裝。跨平台 SQL Server 的 provider 實機 round-trip 保留在支援其容器映像的 Linux x64 runner。 |
3535

3636
## Navicat Premium 功能頁對照
@@ -41,7 +41,7 @@
4141
| 物件設計器 | 🟡 | 五種 provider 已能建表與主要 ALTER;進階 constraint/索引仍需更多實機矩陣。 |
4242
| RDBMS 資料編輯器(網格) | ✅ | Windows 完整版具備分頁瀏覽、篩選、排序、欄顯示、寫回、無主鍵安全模式與多格式匯出;Linux / macOS 預覽版已補 Primary Key 穩定分頁、metadata 白名單參數化篩選與欄位排序、依連線與 Table 安全保存的欄位顯示控制、安全寫回,以及保留目前篩選/排序與可見欄位的 CSV/TSV/JSON 本頁匯出。 |
4343
| MongoDB 資料編輯器(網格/樹/JSON) | 🆕 | 文件檢視器提供可展開文件樹與 Canonical Extended JSON 編輯;儲存會鎖定 `_id` 並以完整原始文件做並行比對,並支援文件新增(自動 `_id`)與安全刪除;view 與缺 `_id` 文件唯讀。待補網格內編輯。 |
44-
| Redis 資料編輯器 | ✅ | key 編輯器依型別切換:string 值編輯、hash 欄位、list 元素/尾端新增、set 成員、zset 分數都有並行衝突保護,另有 TTL 設定/移除與刪除 key;list 元素刪除因 Redis 無對應命令留待後續。 |
44+
| Redis 資料編輯器 | ✅ | key 編輯器依型別切換:string 值編輯、hash 欄位、list 元素編輯/刪除/尾端新增、set 成員、zset 分數都有並行衝突保護,另有 TTL 設定/移除與刪除 key。 |
4545
| 資料分析與互動圖表 | 🆕 | 已完成欄位摘要、Top 值比例與值鑽取的第一版。 |
4646
| 自動完成程式碼 | ✅ | 已能解析目前 statement 的 FROM/JOIN/UPDATE/INTO 來源與 alias;支援欄位、`alias.column`、資料表、關鍵字與片段捷徑,並依 provider/database 快取資料表、View 與欄位 metadata。 |
4747
| 程式碼片段 | ✅ | `Ctrl+Shift+P` 開啟片段管理器;支援 8 組內建片段、自訂片段 CRUD、全文搜尋、`$CURSOR$` 定位、保留縮排插入,以及 JSON 匯入/匯出工作區格式。 |
@@ -80,7 +80,7 @@
8080
| MySQL/MariaDB | ✅ | 共用 MySQL provider,已有實機版本矩陣。 |
8181
| PostgreSQL、SQL Server、Oracle、SQLite | 🟡 | 核心 metadata/查詢/編輯/DDL/備份可用,進階功能持續對等化。 |
8282
| MongoDB | 🟡 | 第三期完成:連線、metadata、JSON find 查詢、文件樹、安全編輯與文件新增/刪除都已具備;standalone 4.4/7.0/8.0 實機矩陣通過。待補 Atlas/SRV 驗證環境矩陣與 Aggregation Pipeline。 |
83-
| Redis/Garnet | 🟡 | 第三期完成:URI、ACL/密碼、TLS、logical db、key 瀏覽、受限查詢與五種型別的安全編輯/TTL/刪除;Redis 6.2、Redis 7 與 Garnet standalone 各 39 項實機矩陣通過。Cluster/Sentinel、監控與 Pub/Sub 待補。 |
83+
| Redis/Garnet | 🟡 | 第三期完成:URI、ACL/密碼、TLS、logical db、key 瀏覽、受限查詢與五種型別的安全編輯/TTL/刪除,list 也可依索引安全刪除。Redis 6.2、Redis 7 與 Garnet standalone 既有各 39 項實機矩陣通過,新增的 list 刪除案例待重跑;Cluster/Sentinel、監控與 Pub/Sub 待補。 |
8484
| Snowflake | 🟡 | 第二期 provider 完成(SQL REST API、PAT/OAuth、metadata、分頁瀏覽、SELECT/SHOW 與查詢編輯器單一 DML/DDL);真實帳戶實機矩陣、key-pair JWT、參數綁定、網格寫回與 bulk load 待補。 |
8585
| AWS、Microsoft Azure、Google Cloud、Oracle Cloud、MongoDB Atlas、Redis Enterprise Cloud、Alibaba Cloud、Tencent Cloud、Huawei Cloud | 🟡 | RDBMS、MongoDB 與 Redis 可先用標準主機連線;待補各家 IAM/SSO/MFA 與雲端專用驗證。 |
8686
| OceanBase、PingCAP/TiDB、Dameng、Fujitsu、Kingbase、HighGo | 📋 | 建立實機相容矩陣;能沿用 MySQL/PostgreSQL 協定者先驗證差異,其餘再建立專用 provider。 |

‎mySQLPunk/Localization.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,7 @@ static Localization()
862862
Add("Redis.EntryNotSelected", "請先在清單中選取要操作的項目。", "Select an entry in the list first.");
863863
Add("Redis.EntrySaved", "項目已儲存。", "The entry was saved.");
864864
Add("Redis.EntryDeleted", "項目已刪除。", "The entry was deleted.");
865-
Add("Redis.ListEditNote", "list 支援編輯既有元素與尾端新增;元素刪除因 Redis 無單一索引刪除命令,留待後續。", "Lists support editing existing elements and appending; element deletion is deferred because Redis has no delete-by-index command.");
865+
Add("Redis.ListEditNote", "list 支援編輯、刪除既有元素與尾端新增;內容已變更時會要求重新載入。", "Lists support editing or deleting existing elements and appending; reload if the content has changed.");
866866
Add("Snowflake.ConnectionStringRequired", "Snowflake 連線字串不可為空。", "The Snowflake connection string is required.");
867867
Add("Snowflake.ConnectionStringInvalid", "Snowflake 連線字串必須是 snowflake:// URI。", "The Snowflake connection string must be a snowflake:// URI.");
868868
Add("Snowflake.AccountRequired", "請輸入 Snowflake 帳戶識別碼。", "Enter the Snowflake account identifier.");

‎mySQLPunk/RedisKeyEditorForm.cs‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,7 @@ private void ConfigureForType()
272272
_entryNameBox.ReadOnly = true;
273273
_entryValueBox.Visible = _entryValueLabel.Visible = true;
274274
_addUpdateButton.Text = Localization.T("Redis.UpdateEntry");
275-
_addUpdateButton.Visible = true;
276-
_deleteEntryButton.Visible = false;
275+
_addUpdateButton.Visible = _deleteEntryButton.Visible = true;
277276
break;
278277
case "set":
279278
_entryNameLabel.Text = Localization.T("Redis.EntryMemberColon");
@@ -501,6 +500,13 @@ private async Task DeleteEntryAsync()
501500
await Task.Run(() => _db.DeleteHashField(_databaseName, _key, field, expected));
502501
break;
503502
}
503+
case "list":
504+
{
505+
long index = Convert.ToInt64(view.Row["index"], CultureInfo.InvariantCulture);
506+
string expected = Convert.ToString(view.Row["value"], CultureInfo.InvariantCulture);
507+
await Task.Run(() => _db.DeleteListElement(_databaseName, _key, index, expected));
508+
break;
509+
}
504510
case "set":
505511
{
506512
string member = Convert.ToString(view.Row["member"], CultureInfo.InvariantCulture);

‎mySQLPunk/lib/my_redis.cs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,28 @@ public void AppendListElement(string databaseName, string key, string newValue)
477477
() => client.Execute("RPUSH", key, newValue ?? string.Empty));
478478
}
479479

480+
/// <summary>
481+
/// 刪除 list 指定索引的元素;先比對載入值,再於同一交易以唯一標記取代並移除,
482+
/// 避免 LREM 直接依值刪除時誤傷內容相同的其他元素。
483+
/// </summary>
484+
public void DeleteListElement(string databaseName, string key, long index, string expectedValue)
485+
{
486+
if (index < 0) throw new ArgumentOutOfRangeException("index");
487+
RejectBinary(expectedValue);
488+
string marker = "\0mysqlpunk:list-delete:" + Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N");
489+
RunWatchedWrite(databaseName, key, "list",
490+
() =>
491+
{
492+
string current = client.Execute("LINDEX", key, index.ToString(CultureInfo.InvariantCulture)) as string;
493+
ValidateEntryExpectation(current, expectedValue, true);
494+
},
495+
() =>
496+
{
497+
client.Execute("LSET", key, index.ToString(CultureInfo.InvariantCulture), marker);
498+
client.Execute("LREM", key, "1", marker);
499+
});
500+
}
501+
480502
/// <summary>加入 set 成員(SADD;已存在時為 no-op)。</summary>
481503
public void AddSetMember(string databaseName, string key, string member)
482504
{

0 commit comments

Comments
 (0)