Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### 🚀 新增功能

- **SQL Server 降版相容搬移(Windows,有限範圍預覽版)**:選取實際目標版本,預覽結構與列數並輸入目標名稱確認後,只寫入另建的空資料庫。僅 dbo 一般 heap 與白名單型別;identity、constraints、indexes、triggers、procs、schema 相依與進階功能明確阻擋,定序必須相同。分批寫入單一交易,內容與結構驗證通過才提交,不自動重試;2016–2025 尚僅離線/mock 驗證,不宣稱真引擎矩陣通過。

- **系統資料庫規格書樣版(Windows,待最終匯出驗收)**:DOCX 新增第三種樣版,提供 A4 直式資料表清單、主鍵與索引摘要、淡藍色可編輯欄位表,以及 PK/NN 和長度/精度欄;中文名稱沿用資料庫註解。
- **Windows 建置與啟動腳本**:根目錄新增 `build.bat` 與 `run.bat`,支援 `debug` / `release` 且預設 Debug;自動尋找 MSBuild 並還原套件,`run.bat` 只在編譯成功後啟動對應版本。
- **匯出/匯入工作區(協同合作第一階段,Windows)**:「檔案」選單新增「匯出工作區...」與「匯入工作區...」。匯出把目前設定檔的連線(含群組、星號與顏色;一律移除密碼、SSH/SSL 祕密、權杖與本機憑證參照,登入帳號預設不含)、SQL 片段、AI 動作、自動執行作業(Webhook 網址可能含權杖,預設不含)與自訂資料產生器字典寫成資料夾:JSON 鍵排序、固定順序、LF 換行、沒有時間戳記,重複匯出內容完全相同,並附 `.gitattributes`,可直接提交到 Git 與隊友共用。匯入先比較每個項目為「新增/變更/相同」並讓你勾選要套用的項目;自動執行作業取代時保留本機的 Webhook;接著開啟既有的連線匯入預覽,工作區的連線一律以合併方式匯入並沿用本機密碼,沒有帳號的連線會對應到本機相同的連線而不重複新增。
Expand Down Expand Up @@ -51,6 +53,12 @@

### 🛠️ 問題修正與優化

- SQL Server 批次新增依 2,100 參數與 1,000 VALUES 列上限切批,整次呼叫共用交易,中途失敗不留下前批資料。
- 桌面連線測試、取得資料庫或取消失敗時釋放尚未移交的 session,避免 SSH tunnel 洩漏。
- 桌面 SQL 快捷鍵與操作共用互斥 gate,取消後等待舊操作結束才接受新操作,取消結果不再覆蓋畫面。
- 資料傳輸列數不符明確標為驗證失敗,不視為完成;不確定批次及無主鍵續傳禁止自動重試,保留目標供核對。
- SQL Server 原生還原先確認備份與目標引擎版本,拒絕新版備份還原至舊引擎,以及未知、複數或非完整備份。

- Windows 資料庫報表精靈的資料表清單顯示並可搜尋註解,PDF 標準目錄同步附上表註解;Punky 移到確認頁並放大,開始匯出自動循環、首輪播放原音效,提供靜音。樣板放大預覽改用高解析圖片,支援符合寬度、捲動、縮放;返回修改選項時會清除過期的完成狀態。
- Windows 資料同步寫入 PostgreSQL `SERIAL` 主鍵的明確值後不會推進序列,之後以預設值新增資料列可能撞主鍵;現在 identity 與 `DEFAULT nextval` 欄位寫入明確值後都會把序列移到最大值之後。
- Linux/macOS 連 MySQL/MariaDB 時,`CHAR(36)` 欄位只要存了非 GUID 文字,整張資料表就無法瀏覽(驅動程式預設把 `CHAR(36)` 當 GUID 解析);現在一律以一般字串讀寫,`BINARY(16)` 也維持位元組。
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ SQLite / PostgreSQL / SQL Server database rename 實機矩陣(需先啟動 Doc
| Table/View 複製 | 可用 | 跨 provider 複製 Table/View;View SQL 無法安全轉換時會改用 table snapshot。 |
| SQL Dump | 可用 | 支援多 provider Table dump;SQLite 欄位註解 sidecar metadata 會隨結構匯出;各 provider 的 DDL 細節仍會依 metadata 能力不同而有差異。 |
| 匯出 / Dump / Backup service | 可用 | 查詢結果多格式匯出、SQL dump 與邏輯 SQL 備份已抽出 service,Form UI 只負責觸發、檔案對話框與狀態呈現。 |
| SQL Server 降版相容搬移 | 有限範圍預覽版(Windows) | 明確選版本、預檢與確認後,只搬移一般 dbo heap 到新的空目標;分批交易與內容驗證。identity/索引/constraints/routines 等拒絕;2016–2025 尚無真引擎矩陣驗證,詳見 [範圍與矩陣](docs/MSSQL_COMPATIBILITY_MIGRATION.md)。 |
| 自動執行作業 | 可用 | 「工具 > 自動執行作業」可建立唯讀查詢、查詢結果匯出與 SQL 備份;支援立即執行、每日 Windows 工作排程與 JSON 執行紀錄。排程使用同一個已登入的 Windows 帳號讀取 Credential Manager,不把帳密寫進作業檔。詳見 [`docs/AUTOMATION.md`](docs/AUTOMATION.md)。 |
| 連線與 metadata service | 可用 | 連線開啟、retry 判斷與 database metadata snapshot 已抽出 service,Form UI 保留 TreeView 呈現與錯誤提示。 |
| 選項中心 | 部分可用 | 已補齊主要分類頁與 `application-options.json` 保存;查詢視窗已套用記錄限制、編輯器字型/換行/Tab 空格、自動完成開關、大型 SQL 停用編輯器輔助、資料表儲存自動交易、SQL 檔案位置、匯出位置、還原差異抽樣列數、結果網格字型與列高度、日期/時間與數字格式、工具提示顯示開關、診斷記錄、自動復原草稿、索引標籤開啟偏好、HTTP 代理與進階註冊設定。 |
Expand Down
3 changes: 3 additions & 0 deletions docs/FEATURE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,3 +615,6 @@
- 已知情境:Oracle 階層查詢、MySQL 專用 View 語法、帶 OFFSET 且缺少穩定排序的 SQL Server 轉換、無法解析的 SELECT SQL 仍會改用 table snapshot。
- 測試覆蓋:`tests/SmokeTests.cs` 已加入 TOP / LIMIT / LIMIT OFFSET / OFFSET FETCH / FETCH FIRST / ROWNUM、日期格式與解析(含 SQL Server `CONVERT` style 23/120、`TO_DATE`、`TO_TIMESTAMP`、`STR_TO_DATE`)、目前日期/時間(含 `GETUTCDATE()` / `UTC_TIMESTAMP()` / `SYSDATE` / `SYSTIMESTAMP`)、`DATE_TRUNC` 與 Oracle `TRUNC(..., 'MM'/'YYYY'/'HH24'/'MI')` 日期截斷、`NVL2` 空值條件、`DATE` / `TRUNC` 日期截斷、`DATEDIFF` / `TIMESTAMPDIFF` / `MONTHS_BETWEEN` 年/月/日/時分秒差、`DATEADD` / `DATE_ADD` / `DATE_SUB` / `ADD_MONTHS` 年/月/日/時加減、`EOMONTH` / `LAST_DAY` 月末日期、`DATEFROMPARTS` 日期組裝與 `YEAR` / `MONTH` / `DAY` / `HOUR` / `MINUTE` / `SECOND` / `DATEPART` / `DATE_PART` / `EXTRACT` 日期部分函式、`IF` / `IIF` / `DECODE` 條件函式、`CEIL` / `CEILING` 數值進位、`MOD` 取餘數、`POW` / `POWER` 次方、`GREATEST` / `LEAST` 最大/最小值比較、`TRUE` / `FALSE` 布林常值、PostgreSQL `欄位::型別` cast operator、SQL Server `TRY_CAST` / `TRY_CONVERT`、`NULLS FIRST/LAST` 空值排序、`RAND` / `RANDOM` / `DBMS_RANDOM.VALUE` 隨機數、PostgreSQL `ILIKE`、`REGEXP_LIKE` / PostgreSQL `~` 正規表示式比對、字串聚合、`CONCAT` 與 `||` 字串串接、`LEN` / `LENGTH` / `CHAR_LENGTH` / `CHARACTER_LENGTH` 字串長度、`TRIM` / `LTRIM` / `RTRIM` 字串修剪、`SUBSTR` / `SUBSTRING` / `SUBSTRING ... FROM ... FOR` / `LEFT` / `RIGHT` / `LPAD` / `RPAD` 字串擷取與補齊、`LOCATE` / `CHARINDEX` / `INSTR` / `POSITION` 字串位置、JSON 純量讀取(含 `JSON_EXTRACT` / `JSON_UNQUOTE(JSON_EXTRACT(...))` / `JSON_VALUE` / MySQL `->>` / PostgreSQL `->>` / `#>>` 轉 PostgreSQL / MySQL / Oracle / SQL Server)、JSON 片段讀取(含 `JSON_QUERY` / MySQL `->` / PostgreSQL `->` / `#>` 轉 MySQL / PostgreSQL / SQLite)、JSON path 存在判斷(含 `JSON_EXISTS` / `JSON_CONTAINS_PATH` 轉 MySQL / PostgreSQL / SQLite / SQL Server / Oracle)、JSON 陣列長度(含 `JSON_LENGTH` / `JSON_ARRAY_LENGTH` 轉 MySQL / PostgreSQL / SQLite / SQL Server / Oracle)、JSON 物件/陣列建構(含 MySQL、PostgreSQL、SQLite、Oracle 互轉)、簡單 `JSON_TABLE` 轉 PostgreSQL / SQL Server / SQLite、`JSON_TABLE FOR ORDINALITY` 轉 PostgreSQL / SQLite 與 SQL Server 不支援原因、CTE/window 保留、`WITH RECURSIVE` 關鍵字轉換與不支援轉換原因的可重跑案例。
- 後續方向:若需要更高相容性,可再逐 provider 擴充 JSON table 與更多 provider 專用內建函式等更複雜 SQL 方言轉換規則。
## SQL Server 搬移安全性

新增受限的 SQL Server 邏輯降版預檢與 Windows 確認介面;來源/目標版本、空目標、定序、feature 白名單、全交易及內容驗證的詳細範圍與未驗證矩陣,見 [MSSQL_COMPATIBILITY_MIGRATION.md](MSSQL_COMPATIBILITY_MIGRATION.md)。原生 RESTORE 的版本 guard 不等於完整邏輯降版;一般資料傳輸的列數驗證也不等於內容驗證。
56 changes: 56 additions & 0 deletions docs/MSSQL_COMPATIBILITY_MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# SQL Server 降版相容搬移(有限範圍預覽版)

這是 Windows 資料庫右鍵的獨立邏輯搬移工具,不是完整資料庫降版器。一般資料傳輸仍只驗證列數,不能當成降版驗證。

## 引擎矩陣與驗證狀態

| 來源引擎 | 可選舊版目標 | 狀態 |
| --- | --- | --- |
| 2025 (17) | 2022 (16)、2019 (15)、2017 (14)、2016 (13) | 預檢與交易流程只有 mock 驗證 |
| 2022 (16) | 2019 (15)、2017 (14)、2016 (13) | 同一受限實作;沒有真引擎矩陣證據 |
| 2019 (15) | 2017 (14)、2016 (13) | 同上 |
| 2017 (14) | 2016 (13) | 同上 |
| 2016 (13) 或更早/未知 | 無 | 拒絕;不猜測最低版本 |

UI 不預選引擎,必須明確選取目標,並與 SERVERPROPERTY 實際版本一致。這張表代表程式允許預檢的範圍,不代表各版本已通過真 SQL Server 驗證,也不宣稱生產支援。Linux/macOS 目前只有共用核心 mock 測試,尚無此搬移 UI。

## 操作與邊界

1. 先由使用者另建空目標資料庫,再從目前已展開的 SQL Server 連線選取來源與目標;工具不建立資料庫、不自動清空或覆蓋。
2. 唯讀預檢顯示伺服器/資料庫識別、引擎 major、定序、表名與列數、CREATE TABLE 語句及阻擋原因,不顯示資料值。
3. 來源與目標不得為同一資料庫;定序必須完全相同。只使用連線既有的 CONTROL 與 VIEW DEFINITION 權限,缺少即停止,不變更權限。
4. 輸入完整目標名稱並按確認後才開啟交易。交易內重做預檢;識別、名稱、版本、結構、列數或空目標改變就拒絕。
5. 每批最多 500 列;INSERT 再依 2,100 參數及 1,000 VALUES 列限制切批。所有建立與寫入使用同一個目標交易。
6. 來源以 SERIALIZABLE 讀取,目標以 SERIALIZABLE 加工具專用 applock;可能阻擋其他寫入。請使用隔離副本或安排維護時段,這不是線上無停機搬移。
7. 逐列以型別、NULL、值的 SHA-256 多重集合核對內容與列數,並重新檢查來源及目標結構。全部通過才提交。
8. 每次最多 100,000 列,單一欄位編碼最多 1 MiB;不是大型資料庫 bulk migration。失敗/取消回滾,不自動重試。提交回覆遺失、回滾或清理失敗時,明確回報結果不確定,必須由 DBA 核對再決定下一步。

獨立 SQL 連線沿用目前連線設定。若驅動程式已移除 SQL authentication 密碼,獨立連線可能無法建立;工具不讀取密碼檔或秘密庫來繞過,請以可建立獨立連線的正常認證方式重新連線。

## 支援與 fail closed

只搬移 dbo、PRIMARY filegroup、未壓縮的一般 heap 的欄位與資料。保留名稱、型別長度/精度/scale、NULL。明確阻擋:

- identity、computed、default/check/PK/FK/unique constraints、所有索引(包含停用索引)、triggers、procedures、functions、views、sequences。
- 自訂 schema/型別、CLR、分割、非 PRIMARY 儲存、extended properties、change tracking、CDC、replication、RLS、temporal、memory optimized、FileTable、graph、ledger。
- sparse、column set、rowguid、FILESTREAM、hidden、masked、generated、encrypted、ANSI_PADDING OFF 欄位,以及不同於資料庫定序的欄位。
- max 長度、xml、float/real、datetimeoffset、rowversion、sql_variant、spatial、vector 等不在白名單的型別;不能確認的 catalog 欄位或查詢錯誤也停止。

型別白名單:bigint/int/smallint/tinyint、bit、date/datetime/smalldatetime、datetime2(0–7)、time(0–7)、money/smallmoney、uniqueidentifier、decimal/numeric(precision 1–38,scale 不大於 precision)、char/varchar/binary/varbinary(1–8000)、nchar/nvarchar(1–4000)。decimal 超出 .NET decimal 可表示範圍時讀取失敗並回滾,不截斷。

不複製資料庫/伺服器設定、登入、使用者、角色、授權、作業或應用程式相依。非白名單 feature 不會被略過後假報成功。錯誤報告包含階段、表名與例外類型,不包含資料值、連線字串或驅動程式原始錯誤內容。

## 備份與替代路徑

Microsoft 的 RESTORE Compatibility support 明定新版備份不能還原到舊版引擎;降低 compatibility level 並不重寫備份格式。原生還原現在先讀 HEADERONLY 與目標 ProductVersion,未知 metadata、複數 backup sets、非完整備份或來源較新一律在寫入前拒絕。

不在本工具範圍的資料庫,請由 DBA 在隔離環境產生針對精確目標版本的邏輯結構腳本,審核型別、定序、identity、constraints/indexes/triggers/routines 與相依順序,再搬資料並做內容、結構和應用程式測試。bacpac 有自身的模型與目標相容限制,不能視為無限制降版方案。

官方依據:
- https://learn.microsoft.com/en-us/sql/t-sql/statements/restore-statements-transact-sql?view=sql-server-ver17#compatibility-support
- https://learn.microsoft.com/en-us/sql/sql-server/maximum-capacity-specifications-for-sql-server?view=sql-server-ver17
- https://learn.microsoft.com/en-us/sql/t-sql/queries/table-value-constructor-transact-sql?view=sql-server-ver17

## 驗證

離線 SQLite 測試覆蓋 INSERT 邊界、中途失敗整批回滾與重試不重複;mock 覆蓋版本、確認、空目標、定序、feature 阻擋、內容/結構不符、取消、Begin/寫入/提交/回滾失敗。Windows UI 編譯與基本 smoke 交由 Windows CI;尚未以真 SQL Server 2016–2025 引擎或人工 Windows 點選驗證。
22 changes: 22 additions & 0 deletions mySQLPunk.Core/Services/OperationGate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace MySqlPunk.Core.Services;

public sealed class OperationGate
{
private int _active;

public IDisposable? TryEnter()
{
return Interlocked.CompareExchange(ref _active, 1, 0) == 0 ? new Lease(this) : null;
}

private sealed class Lease(OperationGate owner) : IDisposable
{
private OperationGate? _owner = owner;

public void Dispose()
{
var current = Interlocked.Exchange(ref _owner, null);
if (current is not null) Interlocked.Exchange(ref current._active, 0);
}
}
}
23 changes: 23 additions & 0 deletions mySQLPunk.Core/Services/SessionLease.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace MySqlPunk.Core.Services;

public sealed class SessionLease<TSession>(TSession session) : IDisposable where TSession : class, IDisposable
{
private TSession? _session = session;

public TSession Session => _session ?? throw new ObjectDisposedException(nameof(SessionLease<TSession>));

public TSession Transfer(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var owned = Session;
_session = null;
return owned;
}

public void Dispose()
{
var owned = _session;
_session = null;
owned?.Dispose();
}
}
81 changes: 81 additions & 0 deletions mySQLPunk.CrossPlatform.SmokeTests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@

var tests = new List<(string Name, Func<Task> Run)>
{
("SQL Server 分批交易與重試", BatchInsertAsync),
("SQL Server 原生還原版本預檢", RestoreCompatibilityAsync),
("SQL Server 降版預檢與交易 mock", MigrationAsync),
("桌面 session 失敗與取消所有權", SessionOwnershipAsync),
("桌面操作互斥與取消結果防護", OperationOwnershipAsync),
("連線設定不保存密碼", ProfileStoreDoesNotPersistPasswordsAsync),
("連線 URI 安全匯入", ConnectionUriImportAsync),
("TLS 憑證檔案安全套用", TlsCertificateFilesAsync),
Expand All @@ -37,6 +42,76 @@
("Provider 驗證與工廠", ProviderFactoryValidatesProfilesAsync)
};

static Task BatchInsertAsync()
{
using var connection = new Microsoft.Data.Sqlite.SqliteConnection("Data Source=:memory:");
connection.Open();
SqlServerBatchInsertTests.Run(connection);
return Task.CompletedTask;
}

static Task RestoreCompatibilityAsync()
{
SqlServerRestoreCompatibilityTests.Run();
return Task.CompletedTask;
}

static Task MigrationAsync()
{
SqlServerMigrationTests.Run();
return Task.CompletedTask;
}

static async Task OperationOwnershipAsync()
{
var gate = new OperationGate();
var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
using var cancellation = new CancellationTokenSource();
var published = false;
var running = Task.Run(async () =>
{
using var operation = gate.TryEnter();
Assert(operation is not null, "First operation admitted");
entered.SetResult();
await release.Task;
cancellation.Token.ThrowIfCancellationRequested();
published = true;
});
await entered.Task;
Assert(gate.TryEnter() is null, "Duplicate SQL rejected");
cancellation.Cancel();
Assert(gate.TryEnter() is null, "Cancellation does not release gate before provider returns");
release.SetResult();
try { await running; throw new Exception("Cancellation ignored"); } catch (OperationCanceledException) { }
Assert(!published, "Cancelled provider result not published");
using var next = gate.TryEnter();
Assert(next is not null, "Gate reusable after completion");
}

static async Task SessionOwnershipAsync()
{
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
foreach (var failure in new[] { "test", "databases", "cancel" })
{
var session = new DisposableSession();
try
{
using var lease = new SessionLease<DisposableSession>(session);
await Task.Yield();
if (failure == "cancel") lease.Transfer(cancellation.Token);
throw new InvalidOperationException(failure);
}
catch (Exception exception) when (exception is InvalidOperationException or OperationCanceledException) { }
Assert(session.Disposals == 1, "Failed preparation disposes session exactly once");
}
var owned = new DisposableSession();
using (var lease = new SessionLease<DisposableSession>(owned)) lease.Transfer(CancellationToken.None);
Assert(owned.Disposals == 0, "Successful transfer retains session");
owned.Dispose();
}

static Task ConnectionUriImportAsync()
{
var mysql = ConnectionUriImportService.Parse(
Expand Down Expand Up @@ -11187,3 +11262,9 @@ enum LiveSyncTarget
TiDb,
OceanBase
}

sealed class DisposableSession : IDisposable
{
public int Disposals { get; private set; }
public void Dispose() => Disposals++;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="../mySQLPunk/lib/SqlServerBatchInsert.cs" Link="SqlServerBatchInsert.cs" />
<Compile Include="../tests/SqlServerBatchInsertTests.cs" Link="SqlServerBatchInsertTests.cs" />
<Compile Include="../mySQLPunk/lib/SqlServerCompatibilityMigration.cs" Link="SqlServerCompatibilityMigration.cs" />
<Compile Include="../mySQLPunk/lib/SqlServerRestoreCompatibility.cs" Link="SqlServerRestoreCompatibility.cs" />
<Compile Include="../tests/SqlServerRestoreCompatibilityTests.cs" Link="SqlServerRestoreCompatibilityTests.cs" />
<Compile Include="../tests/SqlServerMigrationTests.cs" Link="SqlServerMigrationTests.cs" />
<ProjectReference Include="../mySQLPunk.Core/mySQLPunk.Core.csproj" />
</ItemGroup>

Expand Down
Loading
Loading